diff --git a/doc/hotspot-style.html b/doc/hotspot-style.html index 7be6867b3ca..f1c25dab7f4 100644 --- a/doc/hotspot-style.html +++ b/doc/hotspot-style.html @@ -1859,8 +1859,6 @@ difference.

Additional Undecided Features


* 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/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java index a2c7a072769..9f42d610c4d 100644 --- a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java +++ b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java @@ -285,6 +285,10 @@ final class P11SecretKeyFactory extends SecretKeyFactorySpi { putKeyInfo(new TLSKeyInfo("TlsServerAppTrafficSecret")); putKeyInfo(new TLSKeyInfo("TlsServerHandshakeTrafficSecret")); putKeyInfo(new TLSKeyInfo("TlsUpdateNplus1")); + // QUIC-specific + putKeyInfo(new TLSKeyInfo("TlsInitialSecret")); + putKeyInfo(new TLSKeyInfo("TlsClientInitialTrafficSecret")); + putKeyInfo(new TLSKeyInfo("TlsServerInitialTrafficSecret")); putKeyInfo(new KeyInfo("Generic", CKK_GENERIC_SECRET, CKM_GENERIC_SECRET_KEY_GEN)); diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index 5d6a4bbfecb..3202ca2ba25 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -890,7 +890,9 @@ public final class Float16 * is {@code true} if and only if the argument is not * {@code null} and is a {@code Float16} object that * represents a {@code Float16} that has the same value as the - * {@code double} represented by this object. + * {@code Float16} represented by this object. + * {@linkplain Double##repEquivalence Representation + * equivalence} is used to compare the {@code Float16} values. * * @jls 15.21.1 Numerical Equality Operators == and != */ @@ -988,6 +990,13 @@ public final class Float16 /** * Compares the two specified {@code Float16} values. * + * @apiNote + * One idiom to implement {@linkplain Double##repEquivalence + * representation equivalence} on {@code Float16} values is + * {@snippet lang="java" : + * Float16.compare(a, b) == 0 + * } + * * @param f1 the first {@code Float16} to compare * @param f2 the second {@code Float16} to compare * @return the value {@code 0} if {@code f1} is diff --git a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java index fd08361f682..641dd4676ae 100644 --- a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java +++ b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java @@ -43,9 +43,6 @@ final class HotSpotMetaspaceConstantImpl implements HotSpotMetaspaceConstant, VM private HotSpotMetaspaceConstantImpl(MetaspaceObject metaspaceObject, boolean compressed) { this.metaspaceObject = metaspaceObject; this.compressed = compressed; - if (compressed && !canBeStoredInCompressibleMetaSpace()) { - throw new IllegalArgumentException("constant cannot be compressed: " + metaspaceObject); - } } @Override @@ -88,19 +85,7 @@ final class HotSpotMetaspaceConstantImpl implements HotSpotMetaspaceConstant, VM @Override public boolean isCompressible() { - if (compressed) { - return false; - } - return canBeStoredInCompressibleMetaSpace(); - } - - private boolean canBeStoredInCompressibleMetaSpace() { - if (!HotSpotVMConfig.config().useClassMetaspaceForAllClasses && metaspaceObject instanceof HotSpotResolvedJavaType t && !t.isArray()) { - // As of JDK-8338526, interface and abstract types are not stored - // in compressible metaspace. - return !t.isInterface() && !t.isAbstract(); - } - return true; + return !compressed; } @Override diff --git a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java index 449b315e467..e4e23c6d8b8 100644 --- a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java +++ b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java @@ -67,8 +67,6 @@ class HotSpotVMConfig extends HotSpotVMConfigAccess { final boolean useCompressedOops = getFlag("UseCompressedOops", Boolean.class); - final boolean useClassMetaspaceForAllClasses = getFlag("UseClassMetaspaceForAllClasses", Boolean.class); - final int objectAlignment = getFlag("ObjectAlignmentInBytes", Integer.class); final int klassOffsetInBytes = getFieldValue("CompilerToVM::Data::oopDesc_klass_offset_in_bytes", Integer.class, "int"); diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c b/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c index 4b037142d52..bfeffe85678 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c +++ b/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c @@ -2317,8 +2317,9 @@ threadControl_setPendingInterrupt(jthread thread) debugMonitorEnter(threadLock); node = findRunningThread(thread); - JDI_ASSERT(node != NULL); - node->pendingInterrupt = JNI_TRUE; + if (node != NULL) { + node->pendingInterrupt = JNI_TRUE; + } debugMonitorExit(threadLock); } diff --git a/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp b/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp index fe59cd94b84..8b3c3bb6aef 100644 --- a/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp +++ b/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp @@ -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 @@ -256,6 +256,16 @@ bool needRestartLauncher(AppLauncher& appLauncher, CfgFile& cfgFile) { } +void enableConsoleCtrlHandler(bool enable) { + if (!SetConsoleCtrlHandler(NULL, enable ? FALSE : TRUE)) { + JP_THROW(SysError(tstrings::any() << "SetConsoleCtrlHandler(NULL, " + << (enable ? "FALSE" : "TRUE") + << ") failed", + SetConsoleCtrlHandler)); + } +} + + void launchApp() { // [RT-31061] otherwise UI can be left in back of other windows. ::AllowSetForegroundWindow(ASFW_ANY); @@ -310,6 +320,19 @@ void launchApp() { exec.arg(arg); }); + exec.afterProcessCreated([&](HANDLE pid) { + // + // Ignore Ctrl+C in the current process. + // This will prevent child process termination without allowing + // it to handle Ctrl+C events. + // + // Disable the default Ctrl+C handler *after* the child process + // has been created as it is inheritable and we want the child + // process to have the default handler. + // + enableConsoleCtrlHandler(false); + }); + DWORD exitCode = RunExecutorWithMsgLoop::apply(exec); exit(exitCode); diff --git a/src/jdk.jpackage/windows/native/common/Executor.cpp b/src/jdk.jpackage/windows/native/common/Executor.cpp index edb850afdbb..dfb6b299e5d 100644 --- a/src/jdk.jpackage/windows/native/common/Executor.cpp +++ b/src/jdk.jpackage/windows/native/common/Executor.cpp @@ -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 @@ -161,6 +161,10 @@ UniqueHandle Executor::startProcess(UniqueHandle* threadHandle) const { } } + if (afterProcessCreatedCallback) { + afterProcessCreatedCallback(processInfo.hProcess); + } + // Return process handle. return UniqueHandle(processInfo.hProcess); } diff --git a/src/jdk.jpackage/windows/native/common/Executor.h b/src/jdk.jpackage/windows/native/common/Executor.h index a6edcbd4f76..09c9f85bac6 100644 --- a/src/jdk.jpackage/windows/native/common/Executor.h +++ b/src/jdk.jpackage/windows/native/common/Executor.h @@ -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 @@ -26,6 +26,8 @@ #ifndef EXECUTOR_H #define EXECUTOR_H +#include + #include "tstrings.h" #include "UniqueHandle.h" @@ -97,6 +99,14 @@ public: */ int execAndWaitForExit() const; + /** + * Call provided function after the process hass been created. + */ + Executor& afterProcessCreated(const std::function& v) { + afterProcessCreatedCallback = v; + return *this; + } + private: UniqueHandle startProcess(UniqueHandle* threadHandle=0) const; @@ -106,6 +116,7 @@ private: HANDLE jobHandle; tstring_array argsArray; std::wstring appPath; + std::function afterProcessCreatedCallback; }; #endif // #ifndef EXECUTOR_H diff --git a/test/docs/TEST.ROOT b/test/docs/TEST.ROOT index 5ca9b1f144f..bcbfd717dc0 100644 --- a/test/docs/TEST.ROOT +++ b/test/docs/TEST.ROOT @@ -38,7 +38,7 @@ groups=TEST.groups # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Use new module options useNewOptions=true diff --git a/test/failure_handler/src/share/conf/linux.properties b/test/failure_handler/src/share/conf/linux.properties index 08e4ea8bd87..5cd625ce95a 100644 --- a/test/failure_handler/src/share/conf/linux.properties +++ b/test/failure_handler/src/share/conf/linux.properties @@ -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 @@ -108,7 +108,7 @@ system.sysctl.args=-a process.top.app=top process.top.args=-b -n 1 process.ps.app=ps -process.ps.args=-Leo pid,pcpu,cputime,start,pmem,vsz,rssize,stackp,stat,sgi_p,wchan,user,args +process.ps.args=-eo pid,pcpu,cputime,start,pmem,vsz,rssize,stackp,stat,sgi_p,wchan,user,args memory.free.app=free memory.free.args=-h diff --git a/test/hotspot/gtest/aarch64/aarch64-asmtest.py b/test/hotspot/gtest/aarch64/aarch64-asmtest.py index e1abddf3e1c..bf4f2111999 100644 --- a/test/hotspot/gtest/aarch64/aarch64-asmtest.py +++ b/test/hotspot/gtest/aarch64/aarch64-asmtest.py @@ -2135,6 +2135,7 @@ generate(SpecialCases, [["ccmn", "__ ccmn(zr, zr, 3u, Assembler::LE);", ["punpkhi", "__ sve_punpkhi(p1, p0);", "punpkhi\tp1.h, p0.b"], ["compact", "__ sve_compact(z16, __ S, z16, p1);", "compact\tz16.s, p1, z16.s"], ["compact", "__ sve_compact(z16, __ D, z16, p1);", "compact\tz16.d, p1, z16.d"], + ["movprfx", "__ sve_movprfx(z17, z1);", "movprfx\tz17, z1"], ["ext", "__ sve_ext(z17, z16, 63);", "ext\tz17.b, z17.b, z16.b, #63"], ["facgt", "__ sve_fac(Assembler::GT, p1, __ H, p2, z4, z5);", "facgt\tp1.h, p2/z, z4.h, z5.h"], ["facgt", "__ sve_fac(Assembler::GT, p1, __ S, p2, z4, z5);", "facgt\tp1.s, p2/z, z4.s, z5.s"], diff --git a/test/hotspot/gtest/aarch64/asmtest.out.h b/test/hotspot/gtest/aarch64/asmtest.out.h index 7a3225eaed4..352ea33750e 100644 --- a/test/hotspot/gtest/aarch64/asmtest.out.h +++ b/test/hotspot/gtest/aarch64/asmtest.out.h @@ -1148,6 +1148,7 @@ __ sve_punpkhi(p1, p0); // punpkhi p1.h, p0.b __ sve_compact(z16, __ S, z16, p1); // compact z16.s, p1, z16.s __ sve_compact(z16, __ D, z16, p1); // compact z16.d, p1, z16.d + __ sve_movprfx(z17, z1); // movprfx z17, z1 __ sve_ext(z17, z16, 63); // ext z17.b, z17.b, z16.b, #63 __ sve_fac(Assembler::GT, p1, __ H, p2, z4, z5); // facgt p1.h, p2/z, z4.h, z5.h __ sve_fac(Assembler::GT, p1, __ S, p2, z4, z5); // facgt p1.s, p2/z, z4.s, z5.s @@ -1444,30 +1445,30 @@ 0x9101a1a0, 0xb10a5cc8, 0xd10810aa, 0xf10fd061, 0x120cb166, 0x321764bc, 0x52174681, 0x720c0227, 0x9241018e, 0xb25a2969, 0xd278b411, 0xf26aad01, - 0x14000000, 0x17ffffd7, 0x140004b6, 0x94000000, - 0x97ffffd4, 0x940004b3, 0x3400000a, 0x34fffa2a, - 0x3400960a, 0x35000008, 0x35fff9c8, 0x350095a8, - 0xb400000b, 0xb4fff96b, 0xb400954b, 0xb500001d, - 0xb5fff91d, 0xb50094fd, 0x10000013, 0x10fff8b3, - 0x10009493, 0x90000013, 0x36300016, 0x3637f836, - 0x36309416, 0x3758000c, 0x375ff7cc, 0x375893ac, + 0x14000000, 0x17ffffd7, 0x140004b7, 0x94000000, + 0x97ffffd4, 0x940004b4, 0x3400000a, 0x34fffa2a, + 0x3400962a, 0x35000008, 0x35fff9c8, 0x350095c8, + 0xb400000b, 0xb4fff96b, 0xb400956b, 0xb500001d, + 0xb5fff91d, 0xb500951d, 0x10000013, 0x10fff8b3, + 0x100094b3, 0x90000013, 0x36300016, 0x3637f836, + 0x36309436, 0x3758000c, 0x375ff7cc, 0x375893cc, 0x128313a0, 0x528a32c7, 0x7289173b, 0x92ab3acc, 0xd2a0bf94, 0xf2c285e8, 0x9358722f, 0x330e652f, 0x53067f3b, 0x93577c53, 0xb34a1aac, 0xd35a4016, 0x13946c63, 0x93c3dbc8, 0x54000000, 0x54fff5a0, - 0x54009180, 0x54000001, 0x54fff541, 0x54009121, - 0x54000002, 0x54fff4e2, 0x540090c2, 0x54000002, - 0x54fff482, 0x54009062, 0x54000003, 0x54fff423, - 0x54009003, 0x54000003, 0x54fff3c3, 0x54008fa3, - 0x54000004, 0x54fff364, 0x54008f44, 0x54000005, - 0x54fff305, 0x54008ee5, 0x54000006, 0x54fff2a6, - 0x54008e86, 0x54000007, 0x54fff247, 0x54008e27, - 0x54000008, 0x54fff1e8, 0x54008dc8, 0x54000009, - 0x54fff189, 0x54008d69, 0x5400000a, 0x54fff12a, - 0x54008d0a, 0x5400000b, 0x54fff0cb, 0x54008cab, - 0x5400000c, 0x54fff06c, 0x54008c4c, 0x5400000d, - 0x54fff00d, 0x54008bed, 0x5400000e, 0x54ffefae, - 0x54008b8e, 0x5400000f, 0x54ffef4f, 0x54008b2f, + 0x540091a0, 0x54000001, 0x54fff541, 0x54009141, + 0x54000002, 0x54fff4e2, 0x540090e2, 0x54000002, + 0x54fff482, 0x54009082, 0x54000003, 0x54fff423, + 0x54009023, 0x54000003, 0x54fff3c3, 0x54008fc3, + 0x54000004, 0x54fff364, 0x54008f64, 0x54000005, + 0x54fff305, 0x54008f05, 0x54000006, 0x54fff2a6, + 0x54008ea6, 0x54000007, 0x54fff247, 0x54008e47, + 0x54000008, 0x54fff1e8, 0x54008de8, 0x54000009, + 0x54fff189, 0x54008d89, 0x5400000a, 0x54fff12a, + 0x54008d2a, 0x5400000b, 0x54fff0cb, 0x54008ccb, + 0x5400000c, 0x54fff06c, 0x54008c6c, 0x5400000d, + 0x54fff00d, 0x54008c0d, 0x5400000e, 0x54ffefae, + 0x54008bae, 0x5400000f, 0x54ffef4f, 0x54008b4f, 0xd40658e1, 0xd4014d22, 0xd4046543, 0xd4273f60, 0xd44cad80, 0xd503201f, 0xd503203f, 0xd503205f, 0xd503209f, 0xd50320bf, 0xd503219f, 0xd50323bf, @@ -1686,66 +1687,66 @@ 0x25d8e184, 0x2518e407, 0x05214800, 0x05614800, 0x05a14800, 0x05e14800, 0x05214c00, 0x05614c00, 0x05a14c00, 0x05e14c00, 0x05304001, 0x05314001, - 0x05a18610, 0x05e18610, 0x05271e11, 0x6545e891, - 0x6585e891, 0x65c5e891, 0x6545c891, 0x6585c891, - 0x65c5c891, 0x45b0c210, 0x45f1c231, 0x1e601000, - 0x1e603000, 0x1e621000, 0x1e623000, 0x1e641000, - 0x1e643000, 0x1e661000, 0x1e663000, 0x1e681000, - 0x1e683000, 0x1e6a1000, 0x1e6a3000, 0x1e6c1000, - 0x1e6c3000, 0x1e6e1000, 0x1e6e3000, 0x1e701000, - 0x1e703000, 0x1e721000, 0x1e723000, 0x1e741000, - 0x1e743000, 0x1e761000, 0x1e763000, 0x1e781000, - 0x1e783000, 0x1e7a1000, 0x1e7a3000, 0x1e7c1000, - 0x1e7c3000, 0x1e7e1000, 0x1e7e3000, 0xf8268267, - 0xf82d023c, 0xf8301046, 0xf83d2083, 0xf8263290, - 0xf82d528c, 0xf8284299, 0xf8337160, 0xf8386286, - 0xf8bf820e, 0xf8a600e0, 0xf8af1353, 0xf8a922ea, - 0xf8b53396, 0xf8a251e3, 0xf8b340f4, 0xf8a470fd, - 0xf8a06209, 0xf8f48097, 0xf8f002ea, 0xf8eb10d9, - 0xf8ff21b0, 0xf8f7302c, 0xf8ee52a9, 0xf8f041fa, - 0xf8e471e4, 0xf8e863c6, 0xf864823d, 0xf87d013a, - 0xf86f1162, 0xf87d20e3, 0xf86132bb, 0xf870510e, - 0xf8704336, 0xf86572b4, 0xf8706217, 0xb83e8294, - 0xb8200264, 0xb8381284, 0xb8242358, 0xb8333102, - 0xb828530e, 0xb83042df, 0xb824703f, 0xb82a6194, - 0xb8a080e9, 0xb8b80090, 0xb8bb1146, 0xb8bb21b8, - 0xb8b032df, 0xb8b653f4, 0xb8bd41c9, 0xb8b47287, - 0xb8bc6169, 0xb8ee828c, 0xb8e10138, 0xb8f3126d, - 0xb8f020b0, 0xb8e03183, 0xb8e851ef, 0xb8f041e4, - 0xb8fe7005, 0xb8ea6376, 0xb8638120, 0xb873015d, - 0xb8781284, 0xb86723b8, 0xb86e3175, 0xb87b51ed, - 0xb87f41d1, 0xb863721e, 0xb87660f4, 0xce216874, - 0xce104533, 0xce648c15, 0xce8e3302, 0xce6e82ab, - 0xce6c87d1, 0xcec08063, 0xce638937, 0x25e0c358, - 0x25a1c7d3, 0x0580785a, 0x05426328, 0x05009892, - 0x25a0cc29, 0x2561cec8, 0x058044b3, 0x05401c99, - 0x05006b49, 0x25e0d6f7, 0x2561c528, 0x0583c8bc, - 0x0542522f, 0x05001ec0, 0x25e0de65, 0x25a1c113, - 0x05803cad, 0x0540f3c0, 0x0500ab15, 0x2560c28c, - 0x2561d7c0, 0x05801ed7, 0x0542633b, 0x05003696, - 0x2560d4b4, 0x25e1c918, 0x058021ff, 0x05400e15, - 0x0500f3de, 0x0473025a, 0x04bd05ab, 0x658e0025, - 0x658a08e2, 0x659a0493, 0x043e1062, 0x04f418b4, - 0x046d15bd, 0x04611fce, 0x04d6a07c, 0x04001929, - 0x041a09da, 0x04d098f4, 0x04db10d4, 0x0459a3ad, - 0x041aa029, 0x041919fb, 0x04d39e24, 0x04118302, - 0x04101dba, 0x04d7ae16, 0x04dea571, 0x04180210, - 0x05e786fc, 0x05e4915c, 0x04881cf1, 0x044a0f04, - 0x04090969, 0x048b16c4, 0x044101e4, 0x04dcbf44, - 0x65809745, 0x658d833f, 0x65c68468, 0x65c79b07, - 0x65829e38, 0x049dafca, 0x6582bba8, 0x65c0b7ff, - 0x65c1b4e0, 0x658dbadd, 0x65819a9d, 0x65ed9246, - 0x65b30815, 0x65e6263c, 0x65eebb94, 0x65bad14e, - 0x65efe178, 0x65fc5697, 0x65e07f14, 0x040c55a6, - 0x04977f4d, 0x043d3046, 0x04b733a0, 0x046830a4, - 0x04ed322d, 0x05686948, 0x05bd6c13, 0x65c88ef0, - 0x450db3d7, 0x4540b6d9, 0x043e3979, 0x445896ce, - 0x445a9005, 0x44d98069, 0x445b87ae, 0x04da348e, - 0x04982edb, 0x0499397f, 0x0408338c, 0x04ca309c, - 0x65c721e6, 0x65c63641, 0x65982882, 0x04812b8b, - 0x0e251083, 0x4e3712d5, 0x0e61101f, 0x4e6d118b, - 0x0eba1338, 0x4eb712d5, 0x2e31120f, 0x6e2e11ac, - 0x2e6810e6, 0x6e6f11cd, 0x2eaa1128, 0x6eb1120f, - + 0x05a18610, 0x05e18610, 0x0420bc31, 0x05271e11, + 0x6545e891, 0x6585e891, 0x65c5e891, 0x6545c891, + 0x6585c891, 0x65c5c891, 0x45b0c210, 0x45f1c231, + 0x1e601000, 0x1e603000, 0x1e621000, 0x1e623000, + 0x1e641000, 0x1e643000, 0x1e661000, 0x1e663000, + 0x1e681000, 0x1e683000, 0x1e6a1000, 0x1e6a3000, + 0x1e6c1000, 0x1e6c3000, 0x1e6e1000, 0x1e6e3000, + 0x1e701000, 0x1e703000, 0x1e721000, 0x1e723000, + 0x1e741000, 0x1e743000, 0x1e761000, 0x1e763000, + 0x1e781000, 0x1e783000, 0x1e7a1000, 0x1e7a3000, + 0x1e7c1000, 0x1e7c3000, 0x1e7e1000, 0x1e7e3000, + 0xf8268267, 0xf82d023c, 0xf8301046, 0xf83d2083, + 0xf8263290, 0xf82d528c, 0xf8284299, 0xf8337160, + 0xf8386286, 0xf8bf820e, 0xf8a600e0, 0xf8af1353, + 0xf8a922ea, 0xf8b53396, 0xf8a251e3, 0xf8b340f4, + 0xf8a470fd, 0xf8a06209, 0xf8f48097, 0xf8f002ea, + 0xf8eb10d9, 0xf8ff21b0, 0xf8f7302c, 0xf8ee52a9, + 0xf8f041fa, 0xf8e471e4, 0xf8e863c6, 0xf864823d, + 0xf87d013a, 0xf86f1162, 0xf87d20e3, 0xf86132bb, + 0xf870510e, 0xf8704336, 0xf86572b4, 0xf8706217, + 0xb83e8294, 0xb8200264, 0xb8381284, 0xb8242358, + 0xb8333102, 0xb828530e, 0xb83042df, 0xb824703f, + 0xb82a6194, 0xb8a080e9, 0xb8b80090, 0xb8bb1146, + 0xb8bb21b8, 0xb8b032df, 0xb8b653f4, 0xb8bd41c9, + 0xb8b47287, 0xb8bc6169, 0xb8ee828c, 0xb8e10138, + 0xb8f3126d, 0xb8f020b0, 0xb8e03183, 0xb8e851ef, + 0xb8f041e4, 0xb8fe7005, 0xb8ea6376, 0xb8638120, + 0xb873015d, 0xb8781284, 0xb86723b8, 0xb86e3175, + 0xb87b51ed, 0xb87f41d1, 0xb863721e, 0xb87660f4, + 0xce216874, 0xce104533, 0xce648c15, 0xce8e3302, + 0xce6e82ab, 0xce6c87d1, 0xcec08063, 0xce638937, + 0x25e0c358, 0x25a1c7d3, 0x0580785a, 0x05426328, + 0x05009892, 0x25a0cc29, 0x2561cec8, 0x058044b3, + 0x05401c99, 0x05006b49, 0x25e0d6f7, 0x2561c528, + 0x0583c8bc, 0x0542522f, 0x05001ec0, 0x25e0de65, + 0x25a1c113, 0x05803cad, 0x0540f3c0, 0x0500ab15, + 0x2560c28c, 0x2561d7c0, 0x05801ed7, 0x0542633b, + 0x05003696, 0x2560d4b4, 0x25e1c918, 0x058021ff, + 0x05400e15, 0x0500f3de, 0x0473025a, 0x04bd05ab, + 0x658e0025, 0x658a08e2, 0x659a0493, 0x043e1062, + 0x04f418b4, 0x046d15bd, 0x04611fce, 0x04d6a07c, + 0x04001929, 0x041a09da, 0x04d098f4, 0x04db10d4, + 0x0459a3ad, 0x041aa029, 0x041919fb, 0x04d39e24, + 0x04118302, 0x04101dba, 0x04d7ae16, 0x04dea571, + 0x04180210, 0x05e786fc, 0x05e4915c, 0x04881cf1, + 0x044a0f04, 0x04090969, 0x048b16c4, 0x044101e4, + 0x04dcbf44, 0x65809745, 0x658d833f, 0x65c68468, + 0x65c79b07, 0x65829e38, 0x049dafca, 0x6582bba8, + 0x65c0b7ff, 0x65c1b4e0, 0x658dbadd, 0x65819a9d, + 0x65ed9246, 0x65b30815, 0x65e6263c, 0x65eebb94, + 0x65bad14e, 0x65efe178, 0x65fc5697, 0x65e07f14, + 0x040c55a6, 0x04977f4d, 0x043d3046, 0x04b733a0, + 0x046830a4, 0x04ed322d, 0x05686948, 0x05bd6c13, + 0x65c88ef0, 0x450db3d7, 0x4540b6d9, 0x043e3979, + 0x445896ce, 0x445a9005, 0x44d98069, 0x445b87ae, + 0x04da348e, 0x04982edb, 0x0499397f, 0x0408338c, + 0x04ca309c, 0x65c721e6, 0x65c63641, 0x65982882, + 0x04812b8b, 0x0e251083, 0x4e3712d5, 0x0e61101f, + 0x4e6d118b, 0x0eba1338, 0x4eb712d5, 0x2e31120f, + 0x6e2e11ac, 0x2e6810e6, 0x6e6f11cd, 0x2eaa1128, + 0x6eb1120f, }; // END Generated code -- do not edit diff --git a/test/hotspot/gtest/runtime/test_atomicAccess.cpp b/test/hotspot/gtest/runtime/test_atomicAccess.cpp index 523f27ca870..6e05f429970 100644 --- a/test/hotspot/gtest/runtime/test_atomicAccess.cpp +++ b/test/hotspot/gtest/runtime/test_atomicAccess.cpp @@ -25,13 +25,14 @@ #include "runtime/atomicAccess.hpp" #include "unittest.hpp" -// These tests of Atomic only verify functionality. They don't verify atomicity. +// These tests of AtomicAccess only verify functionality. They don't verify +// atomicity. template -struct AtomicAddTestSupport { +struct AtomicAccessAddTestSupport { volatile T _test_value; - AtomicAddTestSupport() : _test_value{} {} + AtomicAccessAddTestSupport() : _test_value{} {} void test_add() { T zero = 0; @@ -52,19 +53,19 @@ struct AtomicAddTestSupport { } }; -TEST_VM(AtomicAddTest, int32) { - using Support = AtomicAddTestSupport; +TEST_VM(AtomicAccessAddTest, int32) { + using Support = AtomicAccessAddTestSupport; Support().test_add(); Support().test_fetch_add(); } -TEST_VM(AtomicAddTest, int64) { - using Support = AtomicAddTestSupport; +TEST_VM(AtomicAccessAddTest, int64) { + using Support = AtomicAccessAddTestSupport; Support().test_add(); Support().test_fetch_add(); } -TEST_VM(AtomicAddTest, ptr) { +TEST_VM(AtomicAccessAddTest, ptr) { uint _test_values[10] = {}; uint* volatile _test_value{}; @@ -84,10 +85,10 @@ TEST_VM(AtomicAddTest, ptr) { }; template -struct AtomicXchgTestSupport { +struct AtomicAccessXchgTestSupport { volatile T _test_value; - AtomicXchgTestSupport() : _test_value{} {} + AtomicAccessXchgTestSupport() : _test_value{} {} void test() { T zero = 0; @@ -99,21 +100,21 @@ struct AtomicXchgTestSupport { } }; -TEST_VM(AtomicXchgTest, int32) { - using Support = AtomicXchgTestSupport; +TEST_VM(AtomicAccessXchgTest, int32) { + using Support = AtomicAccessXchgTestSupport; Support().test(); } -TEST_VM(AtomicXchgTest, int64) { - using Support = AtomicXchgTestSupport; +TEST_VM(AtomicAccessXchgTest, int64) { + using Support = AtomicAccessXchgTestSupport; Support().test(); } template -struct AtomicCmpxchgTestSupport { +struct AtomicAccessCmpxchgTestSupport { volatile T _test_value; - AtomicCmpxchgTestSupport() : _test_value{} {} + AtomicAccessCmpxchgTestSupport() : _test_value{} {} void test() { T zero = 0; @@ -129,25 +130,25 @@ struct AtomicCmpxchgTestSupport { } }; -TEST_VM(AtomicCmpxchgTest, int32) { - using Support = AtomicCmpxchgTestSupport; +TEST_VM(AtomicAccessCmpxchgTest, int32) { + using Support = AtomicAccessCmpxchgTestSupport; Support().test(); } -TEST_VM(AtomicCmpxchgTest, int64) { +TEST_VM(AtomicAccessCmpxchgTest, int64) { // Check if 64-bit atomics are available on the machine. if (!VM_Version::supports_cx8()) return; - using Support = AtomicCmpxchgTestSupport; + using Support = AtomicAccessCmpxchgTestSupport; Support().test(); } -struct AtomicCmpxchg1ByteStressSupport { +struct AtomicAccessCmpxchg1ByteStressSupport { char _default_val; int _base; char _array[7+32+7]; - AtomicCmpxchg1ByteStressSupport() : _default_val(0x7a), _base(7), _array{} {} + AtomicAccessCmpxchg1ByteStressSupport() : _default_val(0x7a), _base(7), _array{} {} void validate(char val, char val2, int index) { for (int i = 0; i < 7; i++) { @@ -182,16 +183,16 @@ struct AtomicCmpxchg1ByteStressSupport { } }; -TEST_VM(AtomicCmpxchg1Byte, stress) { - AtomicCmpxchg1ByteStressSupport support; +TEST_VM(AtomicAccessCmpxchg1Byte, stress) { + AtomicAccessCmpxchg1ByteStressSupport support; support.test(); } template -struct AtomicEnumTestSupport { +struct AtomicAccessEnumTestSupport { volatile T _test_value; - AtomicEnumTestSupport() : _test_value{} {} + AtomicAccessEnumTestSupport() : _test_value{} {} void test_store_load(T value) { EXPECT_NE(value, AtomicAccess::load(&_test_value)); @@ -216,25 +217,25 @@ struct AtomicEnumTestSupport { } }; -namespace AtomicEnumTestUnscoped { // Scope the enumerators. +namespace AtomicAccessEnumTestUnscoped { // Scope the enumerators. enum TestEnum { A, B, C }; } -TEST_VM(AtomicEnumTest, unscoped_enum) { - using namespace AtomicEnumTestUnscoped; - using Support = AtomicEnumTestSupport; +TEST_VM(AtomicAccessEnumTest, unscoped_enum) { + using namespace AtomicAccessEnumTestUnscoped; + using Support = AtomicAccessEnumTestSupport; Support().test_store_load(B); Support().test_cmpxchg(B, C); Support().test_xchg(B, C); } -enum class AtomicEnumTestScoped { A, B, C }; +enum class AtomicAccessEnumTestScoped { A, B, C }; -TEST_VM(AtomicEnumTest, scoped_enum) { - const AtomicEnumTestScoped B = AtomicEnumTestScoped::B; - const AtomicEnumTestScoped C = AtomicEnumTestScoped::C; - using Support = AtomicEnumTestSupport; +TEST_VM(AtomicAccessEnumTest, scoped_enum) { + const AtomicAccessEnumTestScoped B = AtomicAccessEnumTestScoped::B; + const AtomicAccessEnumTestScoped C = AtomicAccessEnumTestScoped::C; + using Support = AtomicAccessEnumTestSupport; Support().test_store_load(B); Support().test_cmpxchg(B, C); @@ -242,14 +243,14 @@ TEST_VM(AtomicEnumTest, scoped_enum) { } template -struct AtomicBitopsTestSupport { +struct AtomicAccessBitopsTestSupport { volatile T _test_value; // At least one byte differs between _old_value and _old_value op _change_value. static const T _old_value = static_cast(UCONST64(0x7f5300007f530044)); static const T _change_value = static_cast(UCONST64(0x3800530038005322)); - AtomicBitopsTestSupport() : _test_value(0) {} + AtomicAccessBitopsTestSupport() : _test_value(0) {} void fetch_then_and() { AtomicAccess::store(&_test_value, _old_value); @@ -320,31 +321,31 @@ struct AtomicBitopsTestSupport { }; template -const T AtomicBitopsTestSupport::_old_value; +const T AtomicAccessBitopsTestSupport::_old_value; template -const T AtomicBitopsTestSupport::_change_value; +const T AtomicAccessBitopsTestSupport::_change_value; -TEST_VM(AtomicBitopsTest, int8) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, int8) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, uint8) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, uint8) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, int32) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, int32) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, uint32) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, uint32) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, int64) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, int64) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, uint64) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, uint64) { + AtomicAccessBitopsTestSupport()(); } diff --git a/test/hotspot/gtest/utilities/test_rbtree.cpp b/test/hotspot/gtest/utilities/test_rbtree.cpp index ff234dd764a..a351e2141e8 100644 --- a/test/hotspot/gtest/utilities/test_rbtree.cpp +++ b/test/hotspot/gtest/utilities/test_rbtree.cpp @@ -1200,6 +1200,46 @@ TEST_VM_F(RBTreeTest, VerifyItThroughStressTest) { } } +TEST_VM_F(RBTreeTest, TestCopyInto) { + { + RBTreeInt rbtree1; + RBTreeInt rbtree2; + + rbtree1.copy_into(rbtree2); + rbtree2.verify_self(); + } + + RBTreeInt rbtree1; + RBTreeInt rbtree2; + + int size = 1000; + for (int i = 0; i < size; i++) { + rbtree1.upsert(i, i); + } + + rbtree1.copy_into(rbtree2); + rbtree2.verify_self(); + + ResourceMark rm; + GrowableArray allocations(size); + int size1 = 0; + rbtree1.visit_in_order([&](RBTreeIntNode* node) { + size1++; + allocations.append(node->key()); + return true; + }); + + int size2 = 0; + rbtree2.visit_in_order([&](RBTreeIntNode* node) { + EXPECT_EQ(node->key(), allocations.at(size2++)); + return true; + }); + + EXPECT_EQ(size1, size2); + EXPECT_EQ(rbtree1.size(), rbtree2.size()); + EXPECT_EQ(size2, static_cast(rbtree2.size())); +} + struct OomAllocator { void* allocate(size_t sz) { return nullptr; diff --git a/test/hotspot/gtest/x86/asmtest.out.h b/test/hotspot/gtest/x86/asmtest.out.h index a2071bafc20..1f5aae4f636 100644 --- a/test/hotspot/gtest/x86/asmtest.out.h +++ b/test/hotspot/gtest/x86/asmtest.out.h @@ -289,667 +289,751 @@ __ exorl(rdx, Address(r10, r16, (Address::ScaleFactor)2, -0x161e1d47), 16777216, false); // {EVEX}xor edx, dword ptr [r10+r16*4-0x161e1d47], 16777216 IID265 __ exorl(rdx, Address(r29, r23, (Address::ScaleFactor)1, +0x1b34e2f8), 16777216, true); // {NF}xor edx, dword ptr [r29+r23*2+0x1b34e2f8], 16777216 IID266 __ eaddl(r19, Address(r27, r31, (Address::ScaleFactor)0, +0x1f3ce7d8), r29, false); // {EVEX}add r19d, dword ptr [r27+r31*1+0x1f3ce7d8], r29d IID267 - __ eaddl(r28, Address(r24, rcx, (Address::ScaleFactor)3, -0x6053edc2), r28, false); // {EVEX}add r28d, dword ptr [r24+rcx*8-0x6053edc2], r28d IID268 + __ eaddl(r28, Address(r24, rcx, (Address::ScaleFactor)3, -0x6053edc2), r28, false); // add r28d, dword ptr [r24+rcx*8-0x6053edc2] IID268 __ eaddl(r17, Address(r18, r24, (Address::ScaleFactor)3, -0x1bf71f78), r29, true); // {NF}add r17d, dword ptr [r18+r24*8-0x1bf71f78], r29d IID269 __ eaddl(rcx, Address(r15, r28, (Address::ScaleFactor)1, +0x15b8216), rcx, true); // {NF}add ecx, dword ptr [r15+r28*2+0x15b8216], ecx IID270 - __ eorl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false); // {EVEX}or r30d, dword ptr [rbx+rdx*8-0x463540b4], r28d IID271 - __ eorl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false); // {EVEX}or r18d, dword ptr [r28+r10*8+0x3523a73b], r18d IID272 - __ eorl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true); // {NF}or r9d, dword ptr [r15+r15*2-0x2a0bdd56], r21d IID273 - __ eorl(r16, Address(r23, -0x165064ff), r16, true); // {NF}or r16d, dword ptr [r23-0x165064ff], r16d IID274 - __ eorb(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false); // {EVEX}or r28b, byte ptr [r30+r11*1+0x17281e3a], r20b IID275 - __ eorb(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false); // {EVEX}or dl, byte ptr [rbx+r31*4+0x2477b5bb], dl IID276 - __ eorb(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true); // {NF}or r16b, byte ptr [r11+rcx*2-0x3175d1af], r24b IID277 - __ eorb(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true); // {NF}or bl, byte ptr [r11+r20*8-0x22d67bd3], bl IID278 - __ esubl(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false); // {EVEX}sub r26d, dword ptr [r27+r30*2-0x3d9bce2e], edx IID279 - __ esubl(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false); // {EVEX}sub r31d, dword ptr [r22+r29*2+0x14218519], r31d IID280 - __ esubl(r21, Address(r9, -0x1050127a), r13, true); // {NF}sub r21d, dword ptr [r9-0x1050127a], r13d IID281 - __ esubl(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true); // {NF}sub r31d, dword ptr [r9+r8*1-0xae18961], r31d IID282 - __ exorl(r15, Address(r18, +0x5c2bbce5), r12, false); // {EVEX}xor r15d, dword ptr [r18+0x5c2bbce5], r12d IID283 - __ exorl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false); // {EVEX}xor r27d, dword ptr [r25+r23*1+0x5c6078b3], r27d IID284 - __ exorl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true); // {NF}xor r18d, dword ptr [r8+rdx*8-0x9ed3881], r14d IID285 - __ exorl(r9, Address(r15, +0x775acdad), r9, true); // {NF}xor r9d, dword ptr [r15+0x775acdad], r9d IID286 - __ exorb(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false); // {EVEX}xor r21b, byte ptr [r18+r26*2+0x2fe31fd5], r23b IID287 - __ exorb(r10, Address(r27, +0xa3150de), r10, false); // {EVEX}xor r10b, byte ptr [r27+0xa3150de], r10b IID288 - __ exorb(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true); // {NF}xor r18b, byte ptr [r22+r30*8+0x1ad4e897], r24b IID289 - __ exorb(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true); // {NF}xor r8b, byte ptr [r16+r20*1+0x626eae82], r8b IID290 - __ eaddl(r21, r15, 1048576, false); // {EVEX}add r21d, r15d, 1048576 IID291 - __ eaddl(rax, r18, 1048576, false); // {EVEX}add eax, r18d, 1048576 IID292 - __ eaddl(r18, r18, 256, false); // add r18d, 256 IID293 - __ eaddl(r13, r19, 16, true); // {NF}add r13d, r19d, 16 IID294 - __ eaddl(rax, r23, 16, true); // {NF}add eax, r23d, 16 IID295 - __ eaddl(r25, r25, 16777216, true); // {NF}add r25d, r25d, 16777216 IID296 - __ eandl(r29, r18, 1048576, false); // {EVEX}and r29d, r18d, 1048576 IID297 - __ eandl(rax, r14, 1048576, false); // {EVEX}and eax, r14d, 1048576 IID298 - __ eandl(r19, r19, 65536, false); // and r19d, 65536 IID299 - __ eandl(r27, r25, 1048576, true); // {NF}and r27d, r25d, 1048576 IID300 - __ eandl(rax, r20, 1048576, true); // {NF}and eax, r20d, 1048576 IID301 - __ eandl(r28, r28, 16, true); // {NF}and r28d, r28d, 16 IID302 - __ eimull(r31, r22, 4096, false); // {EVEX}imul r31d, r22d, 4096 IID303 + __ eandl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false); // {EVEX}and r30d, dword ptr [rbx+rdx*8-0x463540b4], r28d IID271 + __ eandl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false); // and r18d, dword ptr [r28+r10*8+0x3523a73b] IID272 + __ eandl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true); // {NF}and r9d, dword ptr [r15+r15*2-0x2a0bdd56], r21d IID273 + __ eandl(r16, Address(r23, -0x165064ff), r16, true); // {NF}and r16d, dword ptr [r23-0x165064ff], r16d IID274 + __ eorl(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false); // {EVEX}or r28d, dword ptr [r30+r11*1+0x17281e3a], r20d IID275 + __ eorl(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false); // or edx, dword ptr [rbx+r31*4+0x2477b5bb] IID276 + __ eorl(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true); // {NF}or r16d, dword ptr [r11+rcx*2-0x3175d1af], r24d IID277 + __ eorl(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true); // {NF}or ebx, dword ptr [r11+r20*8-0x22d67bd3], ebx IID278 + __ eorb(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false); // {EVEX}or r26b, byte ptr [r27+r30*2-0x3d9bce2e], dl IID279 + __ eorb(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false); // or r31b, byte ptr [r22+r29*2+0x14218519] IID280 + __ eorb(r21, Address(r9, -0x1050127a), r13, true); // {NF}or r21b, byte ptr [r9-0x1050127a], r13b IID281 + __ eorb(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true); // {NF}or r31b, byte ptr [r9+r8*1-0xae18961], r31b IID282 + __ esubl(r15, Address(r18, +0x5c2bbce5), r12, false); // {EVEX}sub r15d, dword ptr [r18+0x5c2bbce5], r12d IID283 + __ esubl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false); // {EVEX}sub r27d, dword ptr [r25+r23*1+0x5c6078b3], r27d IID284 + __ esubl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true); // {NF}sub r18d, dword ptr [r8+rdx*8-0x9ed3881], r14d IID285 + __ esubl(r9, Address(r15, +0x775acdad), r9, true); // {NF}sub r9d, dword ptr [r15+0x775acdad], r9d IID286 + __ exorl(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false); // {EVEX}xor r21d, dword ptr [r18+r26*2+0x2fe31fd5], r23d IID287 + __ exorl(r10, Address(r27, +0xa3150de), r10, false); // xor r10d, dword ptr [r27+0xa3150de] IID288 + __ exorl(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true); // {NF}xor r18d, dword ptr [r22+r30*8+0x1ad4e897], r24d IID289 + __ exorl(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true); // {NF}xor r8d, dword ptr [r16+r20*1+0x626eae82], r8d IID290 + __ exorb(r16, Address(r21, r15, (Address::ScaleFactor)0, -0x1403b60d), r18, false); // {EVEX}xor r16b, byte ptr [r21+r15*1-0x1403b60d], r18b IID291 + __ exorb(r13, Address(r19, r23, (Address::ScaleFactor)2, +0x237ef1e1), r13, false); // xor r13b, byte ptr [r19+r23*4+0x237ef1e1] IID292 + __ exorb(r29, Address(r18, r14, (Address::ScaleFactor)2, +0x5cc0095b), r14, true); // {NF}xor r29b, byte ptr [r18+r14*4+0x5cc0095b], r14b IID293 + __ exorb(r27, Address(r25, r20, (Address::ScaleFactor)3, +0x1cf7b958), r27, true); // {NF}xor r27b, byte ptr [r25+r20*8+0x1cf7b958], r27b IID294 + __ eaddl(r16, r24, 16, false); // {EVEX}add r16d, r24d, 16 IID295 + __ eaddl(rax, r24, 16, false); // {EVEX}add eax, r24d, 16 IID296 + __ eaddl(r21, r21, 65536, false); // add r21d, 65536 IID297 + __ eaddl(r24, r8, 1048576, true); // {NF}add r24d, r8d, 1048576 IID298 + __ eaddl(rax, r13, 1048576, true); // {NF}add eax, r13d, 1048576 IID299 + __ eaddl(r29, r29, 16777216, true); // {NF}add r29d, r29d, 16777216 IID300 + __ eandl(r12, r12, 16, false); // and r12d, 16 IID301 + __ eandl(rax, r30, 16, false); // {EVEX}and eax, r30d, 16 IID302 + __ eandl(r24, r24, 16, false); // and r24d, 16 IID303 + __ eandl(r8, r12, 1, true); // {NF}and r8d, r12d, 1 IID304 + __ eandl(rax, r13, 1, true); // {NF}and eax, r13d, 1 IID305 + __ eandl(r25, r25, 16, true); // {NF}and r25d, r25d, 16 IID306 + __ eimull(r18, r23, 65536, false); // {EVEX}imul r18d, r23d, 65536 IID307 + __ eimull(rax, r9, 65536, false); // {EVEX}imul eax, r9d, 65536 IID308 + __ eimull(r26, r26, 268435456, false); // {EVEX}imul r26d, r26d, 268435456 IID309 + __ eimull(r25, r21, 1, true); // {NF}imul r25d, r21d, 1 IID310 + __ eimull(rax, r24, 1, true); // {NF}imul eax, r24d, 1 IID311 + __ eimull(r24, r24, 16777216, true); // {NF}imul r24d, r24d, 16777216 IID312 + __ eorl(r30, r26, 1, false); // {EVEX}or r30d, r26d, 1 IID313 + __ eorl(rax, r22, 1, false); // {EVEX}or eax, r22d, 1 IID314 + __ eorl(r17, r17, 1048576, false); // or r17d, 1048576 IID315 + __ eorl(r24, r8, 1, true); // {NF}or r24d, r8d, 1 IID316 + __ eorl(rax, r27, 1, true); // {NF}or eax, r27d, 1 IID317 #endif // _LP64 - __ eimull(rax, rbx, 4096, false); // {EVEX}imul eax, ebx, 4096 IID304 + __ eorl(rdx, rdx, 268435456, true); // {NF}or edx, edx, 268435456 IID318 #ifdef _LP64 - __ eimull(r24, r24, 1048576, false); // {EVEX}imul r24d, r24d, 1048576 IID305 - __ eimull(r21, r16, 65536, true); // {NF}imul r21d, r16d, 65536 IID306 - __ eimull(rax, r24, 65536, true); // {NF}imul eax, r24d, 65536 IID307 - __ eimull(r13, r13, 16, true); // {NF}imul r13d, r13d, 16 IID308 - __ eorl(r29, r8, 16777216, false); // {EVEX}or r29d, r8d, 16777216 IID309 - __ eorl(rax, r12, 16777216, false); // {EVEX}or eax, r12d, 16777216 IID310 - __ eorl(r30, r30, 4096, false); // or r30d, 4096 IID311 - __ eorl(r24, rdx, 16, true); // {NF}or r24d, edx, 16 IID312 - __ eorl(rax, r8, 16, true); // {NF}or eax, r8d, 16 IID313 - __ eorl(r13, r13, 4096, true); // {NF}or r13d, r13d, 4096 IID314 - __ ercll(r25, r13, 1); // {EVEX}rcl r25d, r13d, 1 IID315 - __ ercll(rax, r18, 1); // {EVEX}rcl eax, r18d, 1 IID316 - __ ercll(r9, r9, 16); // rcl r9d, 16 IID317 - __ eroll(r26, r25, 8, false); // {EVEX}rol r26d, r25d, 8 IID318 + __ ercll(r22, r22, 8); // rcl r22d, 8 IID319 + __ ercll(rax, r23, 8); // {EVEX}rcl eax, r23d, 8 IID320 + __ ercll(r19, r19, 4); // rcl r19d, 4 IID321 + __ eroll(r30, r24, 2, false); // {EVEX}rol r30d, r24d, 2 IID322 + __ eroll(rax, r29, 2, false); // {EVEX}rol eax, r29d, 2 IID323 + __ eroll(r8, r8, 2, false); // rol r8d, 2 IID324 + __ eroll(r18, r24, 16, true); // {NF}rol r18d, r24d, 16 IID325 + __ eroll(rax, r13, 16, true); // {NF}rol eax, r13d, 16 IID326 + __ eroll(r24, r24, 1, true); // {NF}rol r24d, r24d, 1 IID327 + __ erorl(r28, r17, 16, false); // {EVEX}ror r28d, r17d, 16 IID328 + __ erorl(rax, r24, 16, false); // {EVEX}ror eax, r24d, 16 IID329 + __ erorl(r17, r17, 4, false); // ror r17d, 4 IID330 + __ erorl(r24, rcx, 4, true); // {NF}ror r24d, ecx, 4 IID331 + __ erorl(rax, r16, 4, true); // {NF}ror eax, r16d, 4 IID332 + __ erorl(r15, r15, 2, true); // {NF}ror r15d, r15d, 2 IID333 + __ esall(r14, r27, 4, false); // {EVEX}sal r14d, r27d, 4 IID334 + __ esall(rax, r23, 4, false); // {EVEX}sal eax, r23d, 4 IID335 + __ esall(r30, r30, 4, false); // sal r30d, 4 IID336 + __ esall(r27, rdx, 2, true); // {NF}sal r27d, edx, 2 IID337 + __ esall(rax, r19, 2, true); // {NF}sal eax, r19d, 2 IID338 + __ esall(r20, r20, 2, true); // {NF}sal r20d, r20d, 2 IID339 + __ esarl(r21, r23, 1, false); // {EVEX}sar r21d, r23d, 1 IID340 + __ esarl(rax, r30, 1, false); // {EVEX}sar eax, r30d, 1 IID341 + __ esarl(r25, r25, 2, false); // sar r25d, 2 IID342 + __ esarl(r24, r19, 4, true); // {NF}sar r24d, r19d, 4 IID343 + __ esarl(rax, r14, 4, true); // {NF}sar eax, r14d, 4 IID344 + __ esarl(r26, r26, 16, true); // {NF}sar r26d, r26d, 16 IID345 + __ eshll(r22, r13, 8, false); // {EVEX}shl r22d, r13d, 8 IID346 + __ eshll(rax, r24, 8, false); // {EVEX}shl eax, r24d, 8 IID347 + __ eshll(r14, r14, 16, false); // shl r14d, 16 IID348 + __ eshll(r28, r25, 8, true); // {NF}shl r28d, r25d, 8 IID349 + __ eshll(rax, r10, 8, true); // {NF}shl eax, r10d, 8 IID350 + __ eshll(r20, r20, 1, true); // {NF}shl r20d, r20d, 1 IID351 + __ eshrl(r12, rbx, 4, false); // {EVEX}shr r12d, ebx, 4 IID352 + __ eshrl(rax, r23, 4, false); // {EVEX}shr eax, r23d, 4 IID353 + __ eshrl(r28, r28, 16, false); // shr r28d, 16 IID354 + __ eshrl(r24, r30, 4, true); // {NF}shr r24d, r30d, 4 IID355 + __ eshrl(rax, r31, 4, true); // {NF}shr eax, r31d, 4 IID356 + __ eshrl(r31, r31, 2, true); // {NF}shr r31d, r31d, 2 IID357 + __ esubl(r20, r10, 256, false); // {EVEX}sub r20d, r10d, 256 IID358 + __ esubl(rax, r13, 256, false); // {EVEX}sub eax, r13d, 256 IID359 + __ esubl(r25, r25, 256, false); // sub r25d, 256 IID360 + __ esubl(r23, r12, 268435456, true); // {NF}sub r23d, r12d, 268435456 IID361 + __ esubl(rax, r16, 268435456, true); // {NF}sub eax, r16d, 268435456 IID362 + __ esubl(r31, r31, 1, true); // {NF}sub r31d, r31d, 1 IID363 + __ exorl(r9, r15, 16777216, false); // {EVEX}xor r9d, r15d, 16777216 IID364 + __ exorl(rax, r13, 16777216, false); // {EVEX}xor eax, r13d, 16777216 IID365 + __ exorl(r28, r28, 16, false); // xor r28d, 16 IID366 + __ exorl(r29, r22, 16, true); // {NF}xor r29d, r22d, 16 IID367 #endif // _LP64 - __ eroll(rax, rdx, 8, false); // {EVEX}rol eax, edx, 8 IID319 + __ exorl(rax, rbx, 16, true); // {NF}xor eax, ebx, 16 IID368 #ifdef _LP64 - __ eroll(r24, r24, 16, false); // rol r24d, 16 IID320 - __ eroll(r24, rcx, 8, true); // {NF}rol r24d, ecx, 8 IID321 - __ eroll(rax, r30, 8, true); // {NF}rol eax, r30d, 8 IID322 - __ eroll(r28, r28, 16, true); // {NF}rol r28d, r28d, 16 IID323 - __ erorl(r17, r28, 4, false); // {EVEX}ror r17d, r28d, 4 IID324 + __ exorl(r8, r8, 16, true); // {NF}xor r8d, r8d, 16 IID369 + __ esubl_imm32(r16, r13, 4194304, false); // {EVEX}sub r16d, r13d, 4194304 IID370 + __ esubl_imm32(rax, r12, 4194304, false); // {EVEX}sub eax, r12d, 4194304 IID371 + __ esubl_imm32(r17, r17, 67108864, false); // sub r17d, 67108864 IID372 + __ esubl_imm32(r22, r26, 1073741824, true); // {NF}sub r22d, r26d, 1073741824 IID373 + __ esubl_imm32(rax, r10, 1073741824, true); // {NF}sub eax, r10d, 1073741824 IID374 + __ esubl_imm32(r11, r11, 1073741824, true); // {NF}sub r11d, r11d, 1073741824 IID375 + __ eaddl(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false); // {EVEX}add r19d, r12d, dword ptr [r30+r8*1+0x6a1a0a73] IID376 + __ eaddl(r30, r30, Address(r18, r19, (Address::ScaleFactor)2, +0x25f990cf), false); // add r30d, dword ptr [r18+r19*4+0x25f990cf] IID377 + __ eaddl(rcx, r25, Address(r19, r16, (Address::ScaleFactor)0, +0x482d5dbc), true); // {NF}add ecx, r25d, dword ptr [r19+r16*1+0x482d5dbc] IID378 + __ eaddl(r9, r9, Address(r11, +0x43d5ee01), true); // {NF}add r9d, r9d, dword ptr [r11+0x43d5ee01] IID379 + __ eandl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false); // {EVEX}and ecx, r23d, dword ptr [r21+r15*4+0x2825c2bc] IID380 + __ eandl(r27, r27, Address(r13, r15, (Address::ScaleFactor)3, -0x1268b895), false); // and r27d, dword ptr [r13+r15*8-0x1268b895] IID381 + __ eandl(r9, r23, Address(r22, r30, (Address::ScaleFactor)0, -0x715acbb), true); // {NF}and r9d, r23d, dword ptr [r22+r30*1-0x715acbb] IID382 + __ eandl(rbx, rbx, Address(r28, r16, (Address::ScaleFactor)2, +0xb0223ee), true); // {NF}and ebx, ebx, dword ptr [r28+r16*4+0xb0223ee] IID383 + __ eimull(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false); // {EVEX}imul r15d, r29d, dword ptr [r15+r28*2-0x1f297a69] IID384 + __ eimull(r17, r17, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), false); // imul r17d, dword ptr [r23+rbx*2+0xadc7545] IID385 + __ eimull(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), true); // {NF}imul r27d, r9d, dword ptr [rdx+r22*4-0x43d90f61] IID386 + __ eimull(rbx, rbx, Address(r28, r22, (Address::ScaleFactor)3, -0x519d9a27), true); // {NF}imul ebx, ebx, dword ptr [r28+r22*8-0x519d9a27] IID387 + __ eorl(r17, rcx, Address(r14, +0x10642223), false); // {EVEX}or r17d, ecx, dword ptr [r14+0x10642223] IID388 + __ eorl(r26, r26, Address(r31, -0x7a9a83ba), false); // or r26d, dword ptr [r31-0x7a9a83ba] IID389 + __ eorl(r15, r22, Address(r12, r12, (Address::ScaleFactor)2, +0x743b6997), true); // {NF}or r15d, r22d, dword ptr [r12+r12*4+0x743b6997] IID390 + __ eorl(r8, r8, Address(rdx, r22, (Address::ScaleFactor)3, -0x588414dc), true); // {NF}or r8d, r8d, dword ptr [rdx+r22*8-0x588414dc] IID391 + __ esubl(rcx, r28, Address(r30, r13, (Address::ScaleFactor)2, +0xe9310e5), false); // {EVEX}sub ecx, r28d, dword ptr [r30+r13*4+0xe9310e5] IID392 + __ esubl(rcx, rcx, Address(r30, r10, (Address::ScaleFactor)1, -0x1b076ed1), false); // sub ecx, dword ptr [r30+r10*2-0x1b076ed1] IID393 + __ esubl(r9, r21, Address(r30, +0x2f79ffd3), true); // {NF}sub r9d, r21d, dword ptr [r30+0x2f79ffd3] IID394 + __ esubl(r16, r16, Address(rdx, r14, (Address::ScaleFactor)2, +0x675d71c1), true); // {NF}sub r16d, r16d, dword ptr [rdx+r14*4+0x675d71c1] IID395 + __ exorl(r27, r28, Address(rbx, r26, (Address::ScaleFactor)2, -0x78c20b81), false); // {EVEX}xor r27d, r28d, dword ptr [rbx+r26*4-0x78c20b81] IID396 + __ exorl(r14, r14, Address(r31, r19, (Address::ScaleFactor)1, -0x4ff251cc), false); // xor r14d, dword ptr [r31+r19*2-0x4ff251cc] IID397 + __ exorl(r20, r18, Address(r13, r16, (Address::ScaleFactor)2, -0x19efc6e2), true); // {NF}xor r20d, r18d, dword ptr [r13+r16*4-0x19efc6e2] IID398 + __ exorl(r19, r19, Address(r13, r23, (Address::ScaleFactor)1, -0x2d1bd8aa), true); // {NF}xor r19d, r19d, dword ptr [r13+r23*2-0x2d1bd8aa] IID399 + __ exorb(r29, r17, Address(rdx, r29, (Address::ScaleFactor)2, +0x66573e84), false); // {EVEX}xor r29b, r17b, byte ptr [rdx+r29*4+0x66573e84] IID400 + __ exorb(r22, r22, Address(r24, r25, (Address::ScaleFactor)3, +0x3a94a93f), false); // xor r22b, byte ptr [r24+r25*8+0x3a94a93f] IID401 + __ exorb(r13, r29, Address(r15, r23, (Address::ScaleFactor)1, +0x76d43532), true); // {NF}xor r13b, r29b, byte ptr [r15+r23*2+0x76d43532] IID402 + __ exorb(r15, r15, Address(r13, r9, (Address::ScaleFactor)0, -0x474e6d1a), true); // {NF}xor r15b, r15b, byte ptr [r13+r9*1-0x474e6d1a] IID403 + __ exorw(r17, r16, Address(r23, rdx, (Address::ScaleFactor)0, +0x562a291), false); // {EVEX}xor r17w, r16w, word ptr [r23+rdx*1+0x562a291] IID404 + __ exorw(r29, r29, Address(r18, r28, (Address::ScaleFactor)3, -0x541967f2), false); // xor r29w, word ptr [r18+r28*8-0x541967f2] IID405 + __ exorw(r27, r11, Address(r10, +0xa911c5a), true); // {NF}xor r27w, r11w, word ptr [r10+0xa911c5a] IID406 + __ exorw(r31, r31, Address(r30, r19, (Address::ScaleFactor)2, -0xf6a3da), true); // {NF}xor r31w, r31w, word ptr [r30+r19*4-0xf6a3da] IID407 + __ eaddl(r12, r13, r23, false); // {load}{EVEX}add r12d, r13d, r23d IID408 + __ eaddl(r28, r28, r20, false); // {load}add r28d, r20d IID409 + __ eaddl(r20, r24, r20, false); // {load}add r20d, r24d IID410 + __ eaddl(r11, r10, r15, true); // {load}{NF}add r11d, r10d, r15d IID411 + __ eaddl(r19, r19, r20, true); // {load}{NF}add r19d, r19d, r20d IID412 + __ eaddl(r23, r15, r23, true); // {load}{NF}add r23d, r15d, r23d IID413 + __ eandl(r26, r19, r24, false); // {load}{EVEX}and r26d, r19d, r24d IID414 + __ eandl(r23, r23, r28, false); // {load}and r23d, r28d IID415 + __ eandl(r11, r13, r11, false); // {load}and r11d, r13d IID416 + __ eandl(r13, rdx, r31, true); // {load}{NF}and r13d, edx, r31d IID417 + __ eandl(r23, r23, r23, true); // {load}{NF}and r23d, r23d, r23d IID418 + __ eandl(r9, r27, r9, true); // {load}{NF}and r9d, r27d, r9d IID419 + __ eimull(r21, r20, r24, false); // {load}{EVEX}imul r21d, r20d, r24d IID420 + __ eimull(r21, r21, r29, false); // {load}imul r21d, r29d IID421 + __ eimull(rbx, r11, rbx, false); // {load}imul ebx, r11d IID422 + __ eimull(r21, rbx, rcx, true); // {load}{NF}imul r21d, ebx, ecx IID423 + __ eimull(r31, r31, r21, true); // {load}{NF}imul r31d, r31d, r21d IID424 + __ eimull(r15, r25, r15, true); // {load}{NF}imul r15d, r25d, r15d IID425 + __ eorw(r30, r23, r25, false); // {load}{EVEX}or r30w, r23w, r25w IID426 + __ eorw(r18, r18, rcx, false); // {load}or r18w, cx IID427 + __ eorw(r10, rcx, r10, false); // {load}or r10w, cx IID428 + __ eorw(r31, r21, r26, true); // {load}{NF}or r31w, r21w, r26w IID429 + __ eorw(r21, r21, r19, true); // {load}{NF}or r21w, r21w, r19w IID430 #endif // _LP64 - __ erorl(rax, rdx, 4, false); // {EVEX}ror eax, edx, 4 IID325 + __ eorw(rdx, rbx, rdx, true); // {load}{NF}or dx, bx, dx IID431 #ifdef _LP64 - __ erorl(r8, r8, 16, false); // ror r8d, 16 IID326 - __ erorl(r19, rdx, 16, true); // {NF}ror r19d, edx, 16 IID327 - __ erorl(rax, r31, 16, true); // {NF}ror eax, r31d, 16 IID328 - __ erorl(r22, r22, 8, true); // {NF}ror r22d, r22d, 8 IID329 - __ esall(r23, r25, 16, false); // {EVEX}sal r23d, r25d, 16 IID330 - __ esall(rax, r14, 16, false); // {EVEX}sal eax, r14d, 16 IID331 - __ esall(r31, r31, 8, false); // sal r31d, 8 IID332 - __ esall(r30, r24, 2, true); // {NF}sal r30d, r24d, 2 IID333 - __ esall(rax, r29, 2, true); // {NF}sal eax, r29d, 2 IID334 - __ esall(r8, r8, 2, true); // {NF}sal r8d, r8d, 2 IID335 - __ esarl(r18, r24, 16, false); // {EVEX}sar r18d, r24d, 16 IID336 - __ esarl(rax, r13, 16, false); // {EVEX}sar eax, r13d, 16 IID337 - __ esarl(r24, r24, 1, false); // sar r24d, 1 IID338 - __ esarl(r28, r17, 16, true); // {NF}sar r28d, r17d, 16 IID339 - __ esarl(rax, r24, 16, true); // {NF}sar eax, r24d, 16 IID340 - __ esarl(r17, r17, 4, true); // {NF}sar r17d, r17d, 4 IID341 - __ eshll(r24, rcx, 4, false); // {EVEX}shl r24d, ecx, 4 IID342 - __ eshll(rax, r16, 4, false); // {EVEX}shl eax, r16d, 4 IID343 - __ eshll(r15, r15, 2, false); // shl r15d, 2 IID344 - __ eshll(r14, r27, 4, true); // {NF}shl r14d, r27d, 4 IID345 - __ eshll(rax, r23, 4, true); // {NF}shl eax, r23d, 4 IID346 - __ eshll(r30, r30, 4, true); // {NF}shl r30d, r30d, 4 IID347 - __ eshrl(r27, rdx, 2, false); // {EVEX}shr r27d, edx, 2 IID348 - __ eshrl(rax, r19, 2, false); // {EVEX}shr eax, r19d, 2 IID349 - __ eshrl(r20, r20, 2, false); // shr r20d, 2 IID350 - __ eshrl(r21, r23, 1, true); // {NF}shr r21d, r23d, 1 IID351 - __ eshrl(rax, r30, 1, true); // {NF}shr eax, r30d, 1 IID352 - __ eshrl(r25, r25, 2, true); // {NF}shr r25d, r25d, 2 IID353 - __ esubl(r24, r19, 1048576, false); // {EVEX}sub r24d, r19d, 1048576 IID354 - __ esubl(rax, r14, 1048576, false); // {EVEX}sub eax, r14d, 1048576 IID355 - __ esubl(r22, r22, 268435456, false); // sub r22d, 268435456 IID356 - __ esubl(r24, r24, 65536, true); // {NF}sub r24d, r24d, 65536 IID357 - __ esubl(rax, r14, 65536, true); // {NF}sub eax, r14d, 65536 IID358 - __ esubl(r28, r28, 268435456, true); // {NF}sub r28d, r28d, 268435456 IID359 - __ exorl(rbx, r20, 256, false); // {EVEX}xor ebx, r20d, 256 IID360 - __ exorl(rax, r15, 256, false); // {EVEX}xor eax, r15d, 256 IID361 -#endif // _LP64 - __ exorl(rbx, rbx, 4096, false); // xor ebx, 4096 IID362 -#ifdef _LP64 - __ exorl(r24, r30, 65536, true); // {NF}xor r24d, r30d, 65536 IID363 - __ exorl(rax, r31, 65536, true); // {NF}xor eax, r31d, 65536 IID364 - __ exorl(r31, r31, 4096, true); // {NF}xor r31d, r31d, 4096 IID365 - __ esubl_imm32(r20, r10, 1048576, false); // {EVEX}sub r20d, r10d, 1048576 IID366 - __ esubl_imm32(rax, r13, 1048576, false); // {EVEX}sub eax, r13d, 1048576 IID367 - __ esubl_imm32(r25, r25, 1048576, false); // sub r25d, 1048576 IID368 - __ esubl_imm32(r23, r12, 1073741824, true); // {NF}sub r23d, r12d, 1073741824 IID369 - __ esubl_imm32(rax, r16, 1073741824, true); // {NF}sub eax, r16d, 1073741824 IID370 - __ esubl_imm32(r31, r31, 65536, true); // {NF}sub r31d, r31d, 65536 IID371 - __ eaddl(r17, r13, Address(r9, +0x7fef2f98), false); // {EVEX}add r17d, r13d, dword ptr [r9+0x7fef2f98] IID372 - __ eaddl(r29, r8, Address(r22, -0x4df70aac), true); // {NF}add r29d, r8d, dword ptr [r22-0x4df70aac] IID373 - __ eandl(r13, r17, Address(r12, r15, (Address::ScaleFactor)3, +0x50a8a902), false); // {EVEX}and r13d, r17d, dword ptr [r12+r15*8+0x50a8a902] IID374 - __ eandl(r22, r25, Address(r26, r10, (Address::ScaleFactor)2, +0x70ea2754), true); // {NF}and r22d, r25d, dword ptr [r26+r10*4+0x70ea2754] IID375 - __ eimull(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false); // {EVEX}imul r19d, r12d, dword ptr [r30+r8*1+0x6a1a0a73] IID376 - __ eimull(r30, r18, Address(r18, r19, (Address::ScaleFactor)2, -0x7fcd28c7), true); // {NF}imul r30d, r18d, dword ptr [r18+r19*4-0x7fcd28c7] IID377 - __ eorl(r16, r31, Address(r25, r11, (Address::ScaleFactor)3, +0x482d5dbc), false); // {EVEX}or r16d, r31d, dword ptr [r25+r11*8+0x482d5dbc] IID378 - __ eorl(r9, r27, Address(r11, +0x43d5ee01), true); // {NF}or r9d, r27d, dword ptr [r11+0x43d5ee01] IID379 - __ esubl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false); // {EVEX}sub ecx, r23d, dword ptr [r21+r15*4+0x2825c2bc] IID380 - __ esubl(r27, r22, Address(r13, r15, (Address::ScaleFactor)1, +0x771f0da7), true); // {NF}sub r27d, r22d, dword ptr [r13+r15*2+0x771f0da7] IID381 - __ exorl(r9, r30, Address(r9, r22, (Address::ScaleFactor)3, -0x4ad6c88e), false); // {EVEX}xor r9d, r30d, dword ptr [r9+r22*8-0x4ad6c88e] IID382 - __ exorl(r11, r16, Address(rbx, r28, (Address::ScaleFactor)2, +0xb0223ee), true); // {NF}xor r11d, r16d, dword ptr [rbx+r28*4+0xb0223ee] IID383 - __ exorb(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false); // {EVEX}xor r15b, r29b, byte ptr [r15+r28*2-0x1f297a69] IID384 - __ exorb(r17, r30, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), true); // {NF}xor r17b, r30b, byte ptr [r23+rbx*2+0xadc7545] IID385 - __ exorw(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), false); // {EVEX}xor r27w, r9w, word ptr [rdx+r22*4-0x43d90f61] IID386 - __ exorw(rbx, r22, Address(r28, r22, (Address::ScaleFactor)0, -0x7d30a0b1), true); // {NF}xor bx, r22w, word ptr [r28+r22*1-0x7d30a0b1] IID387 - __ eaddl(r14, r24, rcx, false); // {load}{EVEX}add r14d, r24d, ecx IID388 - __ eaddl(r8, r8, r17, false); // {load}add r8d, r17d IID389 - __ eaddl(r26, r24, r12, true); // {load}{NF}add r26d, r24d, r12d IID390 - __ eaddl(r24, r24, r23, true); // {load}{NF}add r24d, r24d, r23d IID391 - __ eandl(r13, r26, r31, false); // {load}{EVEX}and r13d, r26d, r31d IID392 - __ eandl(r11, r11, r8, false); // {load}and r11d, r8d IID393 - __ eandl(rcx, r19, r15, true); // {load}{NF}and ecx, r19d, r15d IID394 - __ eandl(r12, r12, r12, true); // {load}{NF}and r12d, r12d, r12d IID395 - __ eimull(r22, r20, r19, false); // {load}{EVEX}imul r22d, r20d, r19d IID396 - __ eimull(r8, r8, rdx, false); // {load}imul r8d, edx IID397 - __ eimull(r22, r27, r23, true); // {load}{NF}imul r22d, r27d, r23d IID398 - __ eimull(r9, r9, r18, true); // {load}{NF}imul r9d, r9d, r18d IID399 - __ eorw(rcx, r30, r13, false); // {load}{EVEX}or cx, r30w, r13w IID400 - __ eorw(r28, r28, r19, false); // {load}or r28w, r19w IID401 - __ eorw(r12, r30, r27, true); // {load}{NF}or r12w, r30w, r27w IID402 - __ eorw(r8, r8, r22, true); // {load}{NF}or r8w, r8w, r22w IID403 - __ eorl(r16, rcx, r30, false); // {load}{EVEX}or r16d, ecx, r30d IID404 - __ eorl(r10, r10, r25, false); // {load}or r10d, r25d IID405 - __ eorl(r15, r17, r17, true); // {load}{NF}or r15d, r17d, r17d IID406 - __ eorl(r9, r9, r30, true); // {load}{NF}or r9d, r9d, r30d IID407 - __ eshldl(r20, r21, r8, false); // {load}{EVEX}shld r20d, r21d, r8d, cl IID408 - __ eshldl(r26, r26, r14, false); // {load}shld r26d, r14d IID409 - __ eshldl(r16, rdx, r14, true); // {load}{NF}shld r16d, edx, r14d, cl IID410 - __ eshldl(r19, r19, r8, true); // {load}{NF}shld r19d, r19d, r8d, cl IID411 - __ eshrdl(r27, rbx, r26, false); // {load}{EVEX}shrd r27d, ebx, r26d, cl IID412 - __ eshrdl(r28, r28, r19, false); // {load}shrd r28d, r19d IID413 - __ eshrdl(rcx, r11, r14, true); // {load}{NF}shrd ecx, r11d, r14d, cl IID414 - __ eshrdl(r31, r31, r19, true); // {load}{NF}shrd r31d, r31d, r19d, cl IID415 - __ esubl(r26, r13, r25, false); // {load}{EVEX}sub r26d, r13d, r25d IID416 - __ esubl(r24, r24, r11, false); // {load}sub r24d, r11d IID417 - __ esubl(r18, r20, r13, true); // {load}{NF}sub r18d, r20d, r13d IID418 - __ esubl(r16, r16, r18, true); // {load}{NF}sub r16d, r16d, r18d IID419 - __ exorl(r19, r17, r8, false); // {load}{EVEX}xor r19d, r17d, r8d IID420 - __ exorl(r19, r19, r13, false); // {load}xor r19d, r13d IID421 - __ exorl(r23, r13, r15, true); // {load}{NF}xor r23d, r13d, r15d IID422 - __ exorl(r11, r11, r29, true); // {load}{NF}xor r11d, r11d, r29d IID423 - __ eshldl(r29, r17, r17, 1, false); // {EVEX}shld r29d, r17d, r17d, 1 IID424 - __ eshldl(r22, r22, r24, 4, false); // shld r22d, r24d, 4 IID425 - __ eshldl(r8, r28, r11, 16, true); // {NF}shld r8d, r28d, r11d, 16 IID426 - __ eshldl(r15, r15, r23, 4, true); // {NF}shld r15d, r15d, r23d, 4 IID427 - __ eshrdl(r29, r22, r16, 4, false); // {EVEX}shrd r29d, r22d, r16d, 4 IID428 - __ eshrdl(r13, r13, r9, 4, false); // shrd r13d, r9d, 4 IID429 - __ eshrdl(r15, r21, r12, 2, true); // {NF}shrd r15d, r21d, r12d, 2 IID430 - __ eshrdl(r17, r17, r23, 2, true); // {NF}shrd r17d, r17d, r23d, 2 IID431 - __ ecmovl (Assembler::Condition::overflow, rdx, r16, r29); // cmovo edx, r16d, r29d IID432 - __ ecmovl (Assembler::Condition::overflow, r10, r10, r21); // cmovo r10d, r21d IID433 - __ ecmovl (Assembler::Condition::noOverflow, r17, r29, r18); // cmovno r17d, r29d, r18d IID434 - __ ecmovl (Assembler::Condition::noOverflow, r28, r28, r24); // cmovno r28d, r24d IID435 - __ ecmovl (Assembler::Condition::below, r10, r20, r27); // cmovb r10d, r20d, r27d IID436 - __ ecmovl (Assembler::Condition::below, r10, r10, r14); // cmovb r10d, r14d IID437 - __ ecmovl (Assembler::Condition::aboveEqual, r11, r27, rcx); // cmovae r11d, r27d, ecx IID438 - __ ecmovl (Assembler::Condition::aboveEqual, r22, r22, r15); // cmovae r22d, r15d IID439 - __ ecmovl (Assembler::Condition::zero, r31, r30, r19); // cmovz r31d, r30d, r19d IID440 - __ ecmovl (Assembler::Condition::zero, r19, r19, r26); // cmovz r19d, r26d IID441 - __ ecmovl (Assembler::Condition::notZero, r21, r14, r26); // cmovnz r21d, r14d, r26d IID442 - __ ecmovl (Assembler::Condition::notZero, r20, r20, r15); // cmovnz r20d, r15d IID443 - __ ecmovl (Assembler::Condition::belowEqual, r12, r13, r23); // cmovbe r12d, r13d, r23d IID444 - __ ecmovl (Assembler::Condition::belowEqual, r28, r28, r20); // cmovbe r28d, r20d IID445 - __ ecmovl (Assembler::Condition::above, r20, r24, r11); // cmova r20d, r24d, r11d IID446 - __ ecmovl (Assembler::Condition::above, r10, r10, r15); // cmova r10d, r15d IID447 - __ ecmovl (Assembler::Condition::negative, r19, r20, r23); // cmovs r19d, r20d, r23d IID448 - __ ecmovl (Assembler::Condition::negative, r15, r15, r26); // cmovs r15d, r26d IID449 - __ ecmovl (Assembler::Condition::positive, r19, r24, r23); // cmovns r19d, r24d, r23d IID450 - __ ecmovl (Assembler::Condition::positive, r28, r28, r11); // cmovns r28d, r11d IID451 - __ ecmovl (Assembler::Condition::parity, r13, r13, rdx); // cmovp r13d, edx IID452 - __ ecmovl (Assembler::Condition::parity, r31, r31, r23); // cmovp r31d, r23d IID453 - __ ecmovl (Assembler::Condition::noParity, r23, r9, r27); // cmovnp r23d, r9d, r27d IID454 - __ ecmovl (Assembler::Condition::noParity, r21, r21, r20); // cmovnp r21d, r20d IID455 - __ ecmovl (Assembler::Condition::less, r24, r21, r29); // cmovl r24d, r21d, r29d IID456 - __ ecmovl (Assembler::Condition::less, rbx, rbx, r11); // cmovl ebx, r11d IID457 - __ ecmovl (Assembler::Condition::greaterEqual, r21, rbx, rcx); // cmovge r21d, ebx, ecx IID458 - __ ecmovl (Assembler::Condition::greaterEqual, r31, r31, r21); // cmovge r31d, r21d IID459 - __ ecmovl (Assembler::Condition::lessEqual, r15, r25, r30); // cmovle r15d, r25d, r30d IID460 - __ ecmovl (Assembler::Condition::lessEqual, r23, r23, r25); // cmovle r23d, r25d IID461 - __ ecmovl (Assembler::Condition::greater, r18, rcx, r10); // cmovg r18d, ecx, r10d IID462 - __ ecmovl (Assembler::Condition::greater, rcx, rcx, r31); // cmovg ecx, r31d IID463 - __ ecmovl (Assembler::Condition::overflow, r21, r19, Address(r26, -0x6e290873)); // cmovo r21d, r19d, dword ptr [r26-0x6e290873] IID464 - __ ecmovl (Assembler::Condition::noOverflow, r24, r19, Address(r22, rcx, (Address::ScaleFactor)0, +0x11f85f9a)); // cmovno r24d, r19d, dword ptr [r22+rcx*1+0x11f85f9a] IID465 - __ ecmovl (Assembler::Condition::below, r17, r24, Address(r20, +0x534d775e)); // cmovb r17d, r24d, dword ptr [r20+0x534d775e] IID466 - __ ecmovl (Assembler::Condition::aboveEqual, r20, r18, Address(r20, -0x47c94ecd)); // cmovae r20d, r18d, dword ptr [r20-0x47c94ecd] IID467 - __ ecmovl (Assembler::Condition::zero, r9, r13, Address(r23, -0x4b83c563)); // cmovz r9d, r13d, dword ptr [r23-0x4b83c563] IID468 - __ ecmovl (Assembler::Condition::notZero, r11, r25, Address(r24, r14, (Address::ScaleFactor)1, -0x446507af)); // cmovnz r11d, r25d, dword ptr [r24+r14*2-0x446507af] IID469 - __ ecmovl (Assembler::Condition::belowEqual, r14, r24, Address(r30, r13, (Address::ScaleFactor)2, +0xd0661d)); // cmovbe r14d, r24d, dword ptr [r30+r13*4+0xd0661d] IID470 - __ ecmovl (Assembler::Condition::above, r13, r25, Address(r14, r27, (Address::ScaleFactor)3, +0x47e1403)); // cmova r13d, r25d, dword ptr [r14+r27*8+0x47e1403] IID471 - __ ecmovl (Assembler::Condition::negative, r24, r19, Address(rcx, rdx, (Address::ScaleFactor)3, -0x644a5318)); // cmovs r24d, r19d, dword ptr [rcx+rdx*8-0x644a5318] IID472 - __ ecmovl (Assembler::Condition::positive, r26, r24, Address(r22, r22, (Address::ScaleFactor)0, +0x70352446)); // cmovns r26d, r24d, dword ptr [r22+r22*1+0x70352446] IID473 - __ ecmovl (Assembler::Condition::parity, r19, r26, Address(r8, r30, (Address::ScaleFactor)2, +0x78a12f5c)); // cmovp r19d, r26d, dword ptr [r8+r30*4+0x78a12f5c] IID474 - __ ecmovl (Assembler::Condition::noParity, r29, r11, Address(r25, r20, (Address::ScaleFactor)0, +0x27a8303a)); // cmovnp r29d, r11d, dword ptr [r25+r20*1+0x27a8303a] IID475 - __ ecmovl (Assembler::Condition::less, r22, r24, Address(r27, r16, (Address::ScaleFactor)1, +0x2541a10)); // cmovl r22d, r24d, dword ptr [r27+r16*2+0x2541a10] IID476 - __ ecmovl (Assembler::Condition::greaterEqual, r31, r15, Address(r8, r16, (Address::ScaleFactor)3, +0x558e3251)); // cmovge r31d, r15d, dword ptr [r8+r16*8+0x558e3251] IID477 - __ ecmovl (Assembler::Condition::lessEqual, r27, r18, Address(r8, r10, (Address::ScaleFactor)0, -0x471987b7)); // cmovle r27d, r18d, dword ptr [r8+r10*1-0x471987b7] IID478 - __ ecmovl (Assembler::Condition::greater, r18, r16, Address(r18, r19, (Address::ScaleFactor)2, -0x120ae81e)); // cmovg r18d, r16d, dword ptr [r18+r19*4-0x120ae81e] IID479 + __ eorl(rcx, r24, r22, false); // {load}{EVEX}or ecx, r24d, r22d IID432 + __ eorl(rcx, rcx, r19, false); // {load}or ecx, r19d IID433 + __ eorl(r27, r27, r27, false); // {load}or r27d, r27d IID434 + __ eorl(r31, r9, r13, true); // {load}{NF}or r31d, r9d, r13d IID435 + __ eorl(r31, r31, r23, true); // {load}{NF}or r31d, r31d, r23d IID436 + __ eorl(r19, r17, r19, true); // {load}{NF}or r19d, r17d, r19d IID437 + __ eshldl(r20, r16, r24, false); // {load}{EVEX}shld r20d, r16d, r24d, cl IID438 + __ eshldl(rdx, rdx, r12, false); // {load}shld edx, r12d IID439 + __ eshldl(r29, r9, r31, true); // {load}{NF}shld r29d, r9d, r31d, cl IID440 + __ eshldl(r17, r17, r20, true); // {load}{NF}shld r17d, r17d, r20d, cl IID441 + __ eshrdl(r20, r15, r18, false); // {load}{EVEX}shrd r20d, r15d, r18d, cl IID442 + __ eshrdl(rcx, rcx, r12, false); // {load}shrd ecx, r12d IID443 + __ eshrdl(r14, r9, r23, true); // {load}{NF}shrd r14d, r9d, r23d, cl IID444 + __ eshrdl(r19, r19, r13, true); // {load}{NF}shrd r19d, r19d, r13d, cl IID445 + __ esubl(r30, r27, r27, false); // {load}{EVEX}sub r30d, r27d, r27d IID446 + __ esubl(rdx, rdx, r11, false); // {load}sub edx, r11d IID447 + __ esubl(r15, r11, r24, true); // {load}{NF}sub r15d, r11d, r24d IID448 + __ esubl(r14, r14, r25, true); // {load}{NF}sub r14d, r14d, r25d IID449 + __ exorl(r31, r16, r12, false); // {load}{EVEX}xor r31d, r16d, r12d IID450 + __ exorl(r20, r20, r14, false); // {load}xor r20d, r14d IID451 + __ exorl(r30, r13, r30, false); // {load}xor r30d, r13d IID452 + __ exorl(r24, r17, r17, true); // {load}{NF}xor r24d, r17d, r17d IID453 + __ exorl(r26, r26, r21, true); // {load}{NF}xor r26d, r26d, r21d IID454 + __ exorl(r11, r13, r11, true); // {load}{NF}xor r11d, r13d, r11d IID455 + __ eshldl(r27, r25, r21, 4, false); // {EVEX}shld r27d, r25d, r21d, 4 IID456 + __ eshldl(r22, r22, r10, 4, false); // shld r22d, r10d, 4 IID457 + __ eshldl(r21, r15, r24, 16, true); // {NF}shld r21d, r15d, r24d, 16 IID458 + __ eshldl(rdx, rdx, r19, 1, true); // {NF}shld edx, edx, r19d, 1 IID459 + __ eshrdl(r23, r13, r8, 16, false); // {EVEX}shrd r23d, r13d, r8d, 16 IID460 + __ eshrdl(r26, r26, r22, 1, false); // shrd r26d, r22d, 1 IID461 + __ eshrdl(r24, r9, r30, 16, true); // {NF}shrd r24d, r9d, r30d, 16 IID462 + __ eshrdl(r19, r19, r8, 4, true); // {NF}shrd r19d, r19d, r8d, 4 IID463 + __ ecmovl (Assembler::Condition::overflow, r30, r26, r17); // cmovo r30d, r26d, r17d IID464 + __ ecmovl (Assembler::Condition::overflow, r14, r14, r26); // cmovo r14d, r26d IID465 + __ ecmovl (Assembler::Condition::noOverflow, r24, r19, r29); // cmovno r24d, r19d, r29d IID466 + __ ecmovl (Assembler::Condition::noOverflow, r25, r25, r20); // cmovno r25d, r20d IID467 + __ ecmovl (Assembler::Condition::below, r11, r10, r14); // cmovb r11d, r10d, r14d IID468 + __ ecmovl (Assembler::Condition::below, r30, r30, r25); // cmovb r30d, r25d IID469 + __ ecmovl (Assembler::Condition::aboveEqual, r13, r22, r27); // cmovae r13d, r22d, r27d IID470 + __ ecmovl (Assembler::Condition::aboveEqual, r16, r16, r24); // cmovae r16d, r24d IID471 + __ ecmovl (Assembler::Condition::zero, r28, r13, r30); // cmovz r28d, r13d, r30d IID472 + __ ecmovl (Assembler::Condition::zero, r30, r30, r24); // cmovz r30d, r24d IID473 + __ ecmovl (Assembler::Condition::notZero, r21, r20, r31); // cmovnz r21d, r20d, r31d IID474 + __ ecmovl (Assembler::Condition::notZero, r8, r8, r16); // cmovnz r8d, r16d IID475 + __ ecmovl (Assembler::Condition::belowEqual, r15, r26, r22); // cmovbe r15d, r26d, r22d IID476 + __ ecmovl (Assembler::Condition::belowEqual, r31, r31, rdx); // cmovbe r31d, edx IID477 + __ ecmovl (Assembler::Condition::above, r27, r8, r10); // cmova r27d, r8d, r10d IID478 + __ ecmovl (Assembler::Condition::above, r18, r18, r11); // cmova r18d, r11d IID479 + __ ecmovl (Assembler::Condition::negative, r27, rbx, r21); // cmovs r27d, ebx, r21d IID480 + __ ecmovl (Assembler::Condition::negative, r12, r12, r31); // cmovs r12d, r31d IID481 + __ ecmovl (Assembler::Condition::positive, r12, rdx, r18); // cmovns r12d, edx, r18d IID482 + __ ecmovl (Assembler::Condition::positive, r18, r18, r19); // cmovns r18d, r19d IID483 + __ ecmovl (Assembler::Condition::parity, r16, r20, r23); // cmovp r16d, r20d, r23d IID484 + __ ecmovl (Assembler::Condition::parity, r18, r18, r16); // cmovp r18d, r16d IID485 + __ ecmovl (Assembler::Condition::noParity, rbx, r31, r30); // cmovnp ebx, r31d, r30d IID486 + __ ecmovl (Assembler::Condition::noParity, r31, r31, r29); // cmovnp r31d, r29d IID487 + __ ecmovl (Assembler::Condition::less, r28, r25, r10); // cmovl r28d, r25d, r10d IID488 + __ ecmovl (Assembler::Condition::less, r24, r24, r20); // cmovl r24d, r20d IID489 + __ ecmovl (Assembler::Condition::greaterEqual, r16, rdx, r26); // cmovge r16d, edx, r26d IID490 + __ ecmovl (Assembler::Condition::greaterEqual, r28, r28, r28); // cmovge r28d, r28d IID491 + __ ecmovl (Assembler::Condition::lessEqual, r9, r20, r24); // cmovle r9d, r20d, r24d IID492 + __ ecmovl (Assembler::Condition::lessEqual, r24, r24, r29); // cmovle r24d, r29d IID493 + __ ecmovl (Assembler::Condition::greater, r23, r27, r15); // cmovg r23d, r27d, r15d IID494 + __ ecmovl (Assembler::Condition::greater, r12, r12, r18); // cmovg r12d, r18d IID495 + __ ecmovl (Assembler::Condition::overflow, r19, r9, Address(r31, rcx, (Address::ScaleFactor)1, -0x2be98bd)); // cmovo r19d, r9d, dword ptr [r31+rcx*2-0x2be98bd] IID496 + __ ecmovl (Assembler::Condition::overflow, r8, r8, Address(r21, r24, (Address::ScaleFactor)1, +0x41e6a0cb)); // cmovo r8d, dword ptr [r21+r24*2+0x41e6a0cb] IID497 + __ ecmovl (Assembler::Condition::noOverflow, r23, r15, Address(r19, r30, (Address::ScaleFactor)3, -0x55adfe2d)); // cmovno r23d, r15d, dword ptr [r19+r30*8-0x55adfe2d] IID498 + __ ecmovl (Assembler::Condition::noOverflow, rdx, rdx, Address(r27, rdx, (Address::ScaleFactor)0, -0x1aa12735)); // cmovno edx, dword ptr [r27+rdx*1-0x1aa12735] IID499 + __ ecmovl (Assembler::Condition::below, rbx, r29, Address(r31, r12, (Address::ScaleFactor)0, +0xbd42246)); // cmovb ebx, r29d, dword ptr [r31+r12*1+0xbd42246] IID500 + __ ecmovl (Assembler::Condition::below, r21, r21, Address(r19, r21, (Address::ScaleFactor)1, -0x41518818)); // cmovb r21d, dword ptr [r19+r21*2-0x41518818] IID501 + __ ecmovl (Assembler::Condition::aboveEqual, r23, r29, Address(r22, r9, (Address::ScaleFactor)2, -0x35addbd8)); // cmovae r23d, r29d, dword ptr [r22+r9*4-0x35addbd8] IID502 + __ ecmovl (Assembler::Condition::aboveEqual, r18, r18, Address(r25, +0x632184c3)); // cmovae r18d, dword ptr [r25+0x632184c3] IID503 + __ ecmovl (Assembler::Condition::zero, r29, r13, Address(r18, r13, (Address::ScaleFactor)0, -0x3972eac6)); // cmovz r29d, r13d, dword ptr [r18+r13*1-0x3972eac6] IID504 + __ ecmovl (Assembler::Condition::zero, r29, r29, Address(r12, r9, (Address::ScaleFactor)3, -0x668cdfd2)); // cmovz r29d, dword ptr [r12+r9*8-0x668cdfd2] IID505 + __ ecmovl (Assembler::Condition::notZero, r25, r18, Address(r9, r22, (Address::ScaleFactor)2, +0x7f6ac91f)); // cmovnz r25d, r18d, dword ptr [r9+r22*4+0x7f6ac91f] IID506 + __ ecmovl (Assembler::Condition::notZero, r28, r28, Address(r30, +0x562e6594)); // cmovnz r28d, dword ptr [r30+0x562e6594] IID507 + __ ecmovl (Assembler::Condition::belowEqual, r27, r24, Address(r15, r20, (Address::ScaleFactor)2, -0x466538b7)); // cmovbe r27d, r24d, dword ptr [r15+r20*4-0x466538b7] IID508 + __ ecmovl (Assembler::Condition::belowEqual, r25, r25, Address(r26, r11, (Address::ScaleFactor)3, -0x593812a9)); // cmovbe r25d, dword ptr [r26+r11*8-0x593812a9] IID509 + __ ecmovl (Assembler::Condition::above, rcx, r20, Address(r16, -0x1389a3eb)); // cmova ecx, r20d, dword ptr [r16-0x1389a3eb] IID510 + __ ecmovl (Assembler::Condition::above, rbx, rbx, Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615)); // cmova ebx, dword ptr [r29+r8*1+0x1d022615] IID511 + __ ecmovl (Assembler::Condition::negative, rdx, r14, Address(r12, r28, (Address::ScaleFactor)1, -0x51725a91)); // cmovs edx, r14d, dword ptr [r12+r28*2-0x51725a91] IID512 + __ ecmovl (Assembler::Condition::negative, r24, r24, Address(r17, r18, (Address::ScaleFactor)1, -0x1725c4e4)); // cmovs r24d, dword ptr [r17+r18*2-0x1725c4e4] IID513 + __ ecmovl (Assembler::Condition::positive, rcx, rcx, Address(r15, r23, (Address::ScaleFactor)2, -0x6bd22ccf)); // cmovns ecx, dword ptr [r15+r23*4-0x6bd22ccf] IID514 + __ ecmovl (Assembler::Condition::positive, r24, r24, Address(r15, r10, (Address::ScaleFactor)1, -0x7ffb3d09)); // cmovns r24d, dword ptr [r15+r10*2-0x7ffb3d09] IID515 + __ ecmovl (Assembler::Condition::parity, r23, rcx, Address(r11, r23, (Address::ScaleFactor)0, +0x3738c585)); // cmovp r23d, ecx, dword ptr [r11+r23*1+0x3738c585] IID516 + __ ecmovl (Assembler::Condition::parity, r24, r24, Address(r30, r10, (Address::ScaleFactor)0, +0xfcc15a8)); // cmovp r24d, dword ptr [r30+r10*1+0xfcc15a8] IID517 + __ ecmovl (Assembler::Condition::noParity, r14, r26, Address(r14, r21, (Address::ScaleFactor)1, -0x4430ce9f)); // cmovnp r14d, r26d, dword ptr [r14+r21*2-0x4430ce9f] IID518 + __ ecmovl (Assembler::Condition::noParity, r10, r10, Address(r28, +0x3d7c59f)); // cmovnp r10d, dword ptr [r28+0x3d7c59f] IID519 + __ ecmovl (Assembler::Condition::less, r10, r21, Address(r8, r8, (Address::ScaleFactor)3, +0x4a6584b4)); // cmovl r10d, r21d, dword ptr [r8+r8*8+0x4a6584b4] IID520 + __ ecmovl (Assembler::Condition::less, r26, r26, Address(r19, r20, (Address::ScaleFactor)3, +0x47c660ef)); // cmovl r26d, dword ptr [r19+r20*8+0x47c660ef] IID521 + __ ecmovl (Assembler::Condition::greaterEqual, r26, r10, Address(rcx, +0x61977a97)); // cmovge r26d, r10d, dword ptr [rcx+0x61977a97] IID522 + __ ecmovl (Assembler::Condition::greaterEqual, r30, r30, Address(r15, r19, (Address::ScaleFactor)3, +0x53c601cb)); // cmovge r30d, dword ptr [r15+r19*8+0x53c601cb] IID523 + __ ecmovl (Assembler::Condition::lessEqual, r14, r9, Address(r17, -0x566ceee2)); // cmovle r14d, r9d, dword ptr [r17-0x566ceee2] IID524 + __ ecmovl (Assembler::Condition::lessEqual, r15, r15, Address(r27, r20, (Address::ScaleFactor)0, +0x76164792)); // cmovle r15d, dword ptr [r27+r20*1+0x76164792] IID525 + __ ecmovl (Assembler::Condition::greater, r27, r14, Address(r9, r13, (Address::ScaleFactor)2, +0xf5752d7)); // cmovg r27d, r14d, dword ptr [r9+r13*4+0xf5752d7] IID526 + __ ecmovl (Assembler::Condition::greater, r12, r12, Address(rbx, rcx, (Address::ScaleFactor)3, -0x5501b4c6)); // cmovg r12d, dword ptr [rbx+rcx*8-0x5501b4c6] IID527 #endif // _LP64 #ifdef _LP64 - __ adcq(rbx, r31); // {load}adc rbx, r31 IID480 - __ cmpq(r30, r31); // {load}cmp r30, r31 IID481 - __ imulq(r29, r28); // {load}imul r29, r28 IID482 - __ popcntq(r25, r10); // {load}popcnt r25, r10 IID483 - __ sbbq(r24, r20); // {load}sbb r24, r20 IID484 - __ subq(r16, rdx); // {load}sub r16, rdx IID485 - __ tzcntq(r26, r28); // {load}tzcnt r26, r28 IID486 - __ lzcntq(r28, r9); // {load}lzcnt r28, r9 IID487 - __ addq(r20, r24); // {load}add r20, r24 IID488 - __ andq(r24, r29); // {load}and r24, r29 IID489 - __ orq(r23, r27); // {load}or r23, r27 IID490 - __ xorq(r15, r12); // {load}xor r15, r12 IID491 - __ movq(r18, r19); // {load}mov r18, r19 IID492 - __ bsfq(r31, rcx); // {load}bsf r31, rcx IID493 - __ bsrq(r9, r13); // {load}bsr r9, r13 IID494 - __ btq(r20, rcx); // {load}bt r20, rcx IID495 - __ xchgq(r8, r21); // {load}xchg r8, r21 IID496 - __ testq(r24, r14); // {load}test r24, r14 IID497 - __ addq(Address(rcx, r23, (Address::ScaleFactor)2, +0x4ff06c4d), r29); // add qword ptr [rcx+r23*4+0x4ff06c4d], r29 IID498 - __ andq(Address(r24, r10, (Address::ScaleFactor)1, -0x75d9a189), r26); // and qword ptr [r24+r10*2-0x75d9a189], r26 IID499 - __ cmpq(Address(rbx, rbx, (Address::ScaleFactor)0, +0x4033d59c), r17); // cmp qword ptr [rbx+rbx*1+0x4033d59c], r17 IID500 - __ orq(Address(r22, r12, (Address::ScaleFactor)3, -0x3893347d), r18); // or qword ptr [r22+r12*8-0x3893347d], r18 IID501 - __ xorq(Address(r20, r23, (Address::ScaleFactor)3, +0x4b311560), r12); // xor qword ptr [r20+r23*8+0x4b311560], r12 IID502 - __ subq(Address(r10, r28, (Address::ScaleFactor)2, +0x5c3a2657), r29); // sub qword ptr [r10+r28*4+0x5c3a2657], r29 IID503 - __ movq(Address(r13, r25, (Address::ScaleFactor)3, +0x1a3d6f3f), r22); // mov qword ptr [r13+r25*8+0x1a3d6f3f], r22 IID504 - __ xaddq(Address(r17, r24, (Address::ScaleFactor)3, -0x35addbd8), r25); // xadd qword ptr [r17+r24*8-0x35addbd8], r25 IID505 - __ andq(Address(r25, +0x632184c3), 16777216); // and qword ptr [r25+0x632184c3], 16777216 IID506 - __ addq(Address(r13, r13, (Address::ScaleFactor)0, -0x3972eac6), 16777216); // add qword ptr [r13+r13*1-0x3972eac6], 16777216 IID507 - __ cmpq(Address(r9, -0x13b4c806), 4096); // cmp qword ptr [r9-0x13b4c806], 4096 IID508 - __ sarq(Address(r31, +0x4fa7f551), 1); // sar qword ptr [r31+0x4fa7f551], 1 IID509 - __ salq(Address(r21, r31, (Address::ScaleFactor)2, +0x31aa8232), 1); // sal qword ptr [r21+r31*4+0x31aa8232], 1 IID510 - __ sbbq(Address(r24, r31, (Address::ScaleFactor)2, -0x466538b7), 268435456); // sbb qword ptr [r24+r31*4-0x466538b7], 268435456 IID511 - __ shrq(Address(r28, r22, (Address::ScaleFactor)0, -0x3efe85b1), 2); // shr qword ptr [r28+r22*1-0x3efe85b1], 2 IID512 - __ subq(Address(r16, -0x1389a3eb), 1048576); // sub qword ptr [r16-0x1389a3eb], 1048576 IID513 - __ xorq(Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615), 16); // xor qword ptr [r29+r8*1+0x1d022615], 16 IID514 - __ orq(Address(r12, r28, (Address::ScaleFactor)1, -0x34c898e2), 1); // or qword ptr [r12+r28*2-0x34c898e2], 1 IID515 - __ movq(Address(rcx, r24, (Address::ScaleFactor)2, -0x1644eb08), 256); // mov qword ptr [rcx+r24*4-0x1644eb08], 256 IID516 - __ testq(Address(r29, -0x7d23890b), -65536); // test qword ptr [r29-0x7d23890b], -65536 IID517 - __ addq(r23, Address(rcx, r19, (Address::ScaleFactor)2, +0x70eac654)); // add r23, qword ptr [rcx+r19*4+0x70eac654] IID518 - __ andq(rdx, Address(r24, r15, (Address::ScaleFactor)0, -0x204ddaa9)); // and rdx, qword ptr [r24+r15*1-0x204ddaa9] IID519 - __ cmpq(rdx, Address(r23, r11, (Address::ScaleFactor)3, +0x32c930bd)); // cmp rdx, qword ptr [r23+r11*8+0x32c930bd] IID520 - __ lzcntq(r28, Address(rdx, -0x5433c28f)); // lzcnt r28, qword ptr [rdx-0x5433c28f] IID521 - __ orq(r22, Address(r19, r14, (Address::ScaleFactor)1, -0x2cc67d38)); // or r22, qword ptr [r19+r14*2-0x2cc67d38] IID522 - __ adcq(r10, Address(r10, +0x3d7c59f)); // adc r10, qword ptr [r10+0x3d7c59f] IID523 - __ imulq(r10, Address(r8, r8, (Address::ScaleFactor)3, -0xe61862d)); // imul r10, qword ptr [r8+r8*8-0xe61862d] IID524 - __ popcntq(r23, Address(r29, -0x777ed96d)); // popcnt r23, qword ptr [r29-0x777ed96d] IID525 - __ sbbq(rcx, Address(rbx, r19, (Address::ScaleFactor)1, +0x53c601cb)); // sbb rcx, qword ptr [rbx+r19*2+0x53c601cb] IID526 - __ subq(r14, Address(r17, rbx, (Address::ScaleFactor)0, -0x768bf073)); // sub r14, qword ptr [r17+rbx*1-0x768bf073] IID527 - __ tzcntq(r29, Address(r10, r19, (Address::ScaleFactor)1, +0x30c98d3c)); // tzcnt r29, qword ptr [r10+r19*2+0x30c98d3c] IID528 - __ xorq(r10, Address(r16, r27, (Address::ScaleFactor)0, -0x3d08d602)); // xor r10, qword ptr [r16+r27*1-0x3d08d602] IID529 - __ movq(r18, Address(r28, r28, (Address::ScaleFactor)3, -0x62fbac91)); // mov r18, qword ptr [r28+r28*8-0x62fbac91] IID530 - __ leaq(rbx, Address(rcx, +0x450602a5)); // lea rbx, qword ptr [rcx+0x450602a5] IID531 - __ cvttsd2siq(r12, Address(r30, r31, (Address::ScaleFactor)0, -0x6798a630)); // cvttsd2si r12, qword ptr [r30+r31*1-0x6798a630] IID532 - __ xchgq(r31, Address(r24, r10, (Address::ScaleFactor)1, -0x706712ed)); // xchg r31, qword ptr [r24+r10*2-0x706712ed] IID533 - __ testq(r14, Address(r13, r20, (Address::ScaleFactor)3, +0x171081f2)); // test r14, qword ptr [r13+r20*8+0x171081f2] IID534 - __ addq(r31, 16); // add r31, 16 IID535 - __ andq(r25, 16); // and r25, 16 IID536 - __ adcq(r23, 256); // adc r23, 256 IID537 - __ cmpq(r19, 268435456); // cmp r19, 268435456 IID538 - __ rclq(r31, 1); // rcl r31, 1 IID539 - __ rcrq(r17, 1); // rcr r17, 1 IID540 - __ rolq(r25, 2); // rol r25, 2 IID541 - __ rorq(r17, 4); // ror r17, 4 IID542 - __ sarq(r28, 1); // sar r28, 1 IID543 - __ salq(r15, 4); // sal r15, 4 IID544 - __ sbbq(rbx, 65536); // sbb rbx, 65536 IID545 - __ shlq(r21, 1); // shl r21, 1 IID546 - __ shrq(r10, 1); // shr r10, 1 IID547 - __ subq(r14, 16); // sub r14, 16 IID548 - __ xorq(r18, 268435456); // xor r18, 268435456 IID549 - __ movq(r23, 16); // mov r23, 16 IID550 - __ mov64(r12, 1099511627776); // mov r12, 1099511627776 IID551 - __ btq(r14, 4); // bt r14, 4 IID552 - __ testq(r24, -4096); // test r24, -4096 IID553 - __ orq_imm32(r19, 1048576); // or r19, 1048576 IID554 - __ subq_imm32(rcx, 268435456); // sub rcx, 268435456 IID555 - __ cmovq(Assembler::Condition::overflow, rdx, Address(r19, rbx, (Address::ScaleFactor)3, +0x211c8c4)); // cmovo rdx, qword ptr [r19+rbx*8+0x211c8c4] IID556 - __ cmovq(Assembler::Condition::noOverflow, rbx, Address(r21, +0x49267743)); // cmovno rbx, qword ptr [r21+0x49267743] IID557 - __ cmovq(Assembler::Condition::below, r21, Address(r8, r28, (Address::ScaleFactor)1, -0x4c8c2946)); // cmovb r21, qword ptr [r8+r28*2-0x4c8c2946] IID558 - __ cmovq(Assembler::Condition::aboveEqual, r12, Address(r26, r20, (Address::ScaleFactor)0, -0x264df89c)); // cmovae r12, qword ptr [r26+r20*1-0x264df89c] IID559 - __ cmovq(Assembler::Condition::zero, r17, Address(r28, r9, (Address::ScaleFactor)2, +0x3497196b)); // cmovz r17, qword ptr [r28+r9*4+0x3497196b] IID560 - __ cmovq(Assembler::Condition::notZero, r13, Address(r15, r23, (Address::ScaleFactor)1, -0x27a30999)); // cmovnz r13, qword ptr [r15+r23*2-0x27a30999] IID561 - __ cmovq(Assembler::Condition::belowEqual, r22, Address(r22, +0xf39ab05)); // cmovbe r22, qword ptr [r22+0xf39ab05] IID562 - __ cmovq(Assembler::Condition::above, rcx, Address(r22, r26, (Address::ScaleFactor)3, -0x48c954c)); // cmova rcx, qword ptr [r22+r26*8-0x48c954c] IID563 - __ cmovq(Assembler::Condition::negative, r25, Address(r19, r21, (Address::ScaleFactor)0, +0xe405b0b)); // cmovs r25, qword ptr [r19+r21*1+0xe405b0b] IID564 - __ cmovq(Assembler::Condition::positive, r12, Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b)); // cmovns r12, qword ptr [r19+r29*8-0x7762044b] IID565 - __ cmovq(Assembler::Condition::parity, rbx, Address(r30, r10, (Address::ScaleFactor)1, -0x19798323)); // cmovp rbx, qword ptr [r30+r10*2-0x19798323] IID566 - __ cmovq(Assembler::Condition::noParity, r21, Address(r24, r31, (Address::ScaleFactor)0, -0x5731652b)); // cmovnp r21, qword ptr [r24+r31*1-0x5731652b] IID567 - __ cmovq(Assembler::Condition::less, r18, Address(r8, r10, (Address::ScaleFactor)1, -0x5613be89)); // cmovl r18, qword ptr [r8+r10*2-0x5613be89] IID568 - __ cmovq(Assembler::Condition::greaterEqual, r28, Address(r21, r21, (Address::ScaleFactor)3, +0x65a0fdc4)); // cmovge r28, qword ptr [r21+r21*8+0x65a0fdc4] IID569 - __ cmovq(Assembler::Condition::lessEqual, r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c)); // cmovle r23, qword ptr [r11+r18*1-0x1d1af10c] IID570 - __ cmovq(Assembler::Condition::greater, r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38)); // cmovg r22, qword ptr [r18+r12*2+0x1a5f1c38] IID571 - __ call(r23); // call r23 IID572 - __ divq(r30); // div r30 IID573 - __ idivq(r19); // idiv r19 IID574 - __ imulq(r9); // imul r9 IID575 - __ mulq(r13); // mul r13 IID576 - __ negq(r16); // neg r16 IID577 - __ notq(r29); // not r29 IID578 - __ rolq(rcx); // rol rcx, cl IID579 - __ rorq(r25); // ror r25, cl IID580 - __ sarq(r8); // sar r8, cl IID581 - __ salq(r27); // sal r27, cl IID582 - __ shlq(r30); // shl r30, cl IID583 - __ shrq(r23); // shr r23, cl IID584 - __ incrementq(rbx); // inc rbx IID585 - __ decrementq(r14); // dec r14 IID586 - __ pushp(r21); // pushp r21 IID587 - __ popp(r21); // popp r21 IID588 - __ call(Address(r20, r21, (Address::ScaleFactor)1, +0x56c6af2f)); // call qword ptr [r20+r21*2+0x56c6af2f] IID589 - __ mulq(Address(r31, r19, (Address::ScaleFactor)3, -0x1b4eb23)); // mul qword ptr [r31+r19*8-0x1b4eb23] IID590 - __ negq(Address(r27, r27, (Address::ScaleFactor)0, -0x58dbfc1f)); // neg qword ptr [r27+r27*1-0x58dbfc1f] IID591 - __ sarq(Address(rbx, r22, (Address::ScaleFactor)2, -0x606349d1)); // sar qword ptr [rbx+r22*4-0x606349d1], cl IID592 - __ salq(Address(r26, r23, (Address::ScaleFactor)3, +0xb95a079)); // sal qword ptr [r26+r23*8+0xb95a079], cl IID593 - __ shrq(Address(r14, r26, (Address::ScaleFactor)0, +0x3544e09)); // shr qword ptr [r14+r26*1+0x3544e09], cl IID594 - __ incrementq(Address(r27, rdx, (Address::ScaleFactor)0, +0x120b3250)); // inc qword ptr [r27+rdx*1+0x120b3250] IID595 - __ decrementq(Address(r9, r25, (Address::ScaleFactor)2, -0x34aaeccb)); // dec qword ptr [r9+r25*4-0x34aaeccb] IID596 - __ imulq(r20, Address(r16, r28, (Address::ScaleFactor)1, -0x59de05a5), 1048576); // imul r20, qword ptr [r16+r28*2-0x59de05a5], 1048576 IID597 - __ imulq(r17, r23, 256); // imul r17, r23, 256 IID598 - __ shldq(r19, r11, 8); // shld r19, r11, 8 IID599 - __ shrdq(r28, r10, 8); // shrd r28, r10, 8 IID600 - __ pop2(r29, r26); // {load}pop2 r26, r29 IID601 - __ pop2p(r22, r10); // {load}pop2p r10, r22 IID602 - __ push2(r25, r30); // {load}push2 r30, r25 IID603 - __ push2p(r28, r15); // {load}push2p r15, r28 IID604 - __ movzbq(r11, Address(r29, r19, (Address::ScaleFactor)2, -0x12368d34)); // movzx r11, byte ptr [r29+r19*4-0x12368d34] IID605 - __ movzwq(r14, Address(r8, r30, (Address::ScaleFactor)2, -0x4a9392de)); // movzx r14, word ptr [r8+r30*4-0x4a9392de] IID606 - __ movsbq(r28, Address(r23, r15, (Address::ScaleFactor)0, +0x6189cb54)); // movsx r28, byte ptr [r23+r15*1+0x6189cb54] IID607 - __ movswq(r28, Address(rbx, r23, (Address::ScaleFactor)3, -0x2de86561)); // movsx r28, word ptr [rbx+r23*8-0x2de86561] IID608 - __ movzbq(r11, rcx); // movzx r11, cl IID609 - __ movzwq(r30, r15); // movzx r30, r15w IID610 - __ movsbq(r14, rcx); // movsx r14, cl IID611 - __ movswq(r23, r9); // movsx r23, r9w IID612 - __ cmpxchgq(r12, Address(r13, r10, (Address::ScaleFactor)1, -0x7c62c3a)); // cmpxchg qword ptr [r13+r10*2-0x7c62c3a], r12 IID613 - __ eidivq(rcx, false); // {EVEX}idiv rcx IID614 - __ eidivq(r15, true); // {NF}idiv r15 IID615 - __ edivq(r23, false); // {EVEX}div r23 IID616 - __ edivq(r24, true); // {NF}div r24 IID617 - __ eimulq(r27, false); // {EVEX}imul r27 IID618 - __ eimulq(r30, true); // {NF}imul r30 IID619 - __ emulq(r12, false); // {EVEX}mul r12 IID620 - __ emulq(rcx, true); // {NF}mul rcx IID621 - __ emulq(Address(r13, r9, (Address::ScaleFactor)3, -0x226aab94), false); // {EVEX}mul qword ptr [r13+r9*8-0x226aab94] IID622 - __ emulq(Address(r13, r24, (Address::ScaleFactor)3, -0x286c7605), true); // {NF}mul qword ptr [r13+r24*8-0x286c7605] IID623 - __ eimulq(r21, r30, false); // {EVEX}imul r21, r30 IID624 - __ eimulq(r17, r17, false); // imul r17 IID625 - __ eimulq(r29, r12, true); // {NF}imul r29, r12 IID626 - __ eimulq(r30, r30, true); // {NF}imul r30, r30 IID627 - __ elzcntq(r24, r15, false); // {EVEX}lzcnt r24, r15 IID628 - __ elzcntq(r25, r25, false); // {EVEX}lzcnt r25, r25 IID629 - __ elzcntq(r25, r21, true); // {NF}lzcnt r25, r21 IID630 - __ elzcntq(r22, r22, true); // {NF}lzcnt r22, r22 IID631 - __ enegq(r17, r30, false); // {EVEX}neg r17, r30 IID632 - __ enegq(r17, r17, false); // neg r17 IID633 - __ enegq(r31, r17, true); // {NF}neg r31, r17 IID634 - __ enegq(r29, r29, true); // {NF}neg r29, r29 IID635 - __ enotq(r10, r9); // {EVEX}not r10, r9 IID636 - __ enotq(r24, r24); // not r24 IID637 - __ epopcntq(r28, r15, false); // {EVEX}popcnt r28, r15 IID638 - __ epopcntq(r10, r10, false); // {EVEX}popcnt r10, r10 IID639 - __ epopcntq(r27, r30, true); // {NF}popcnt r27, r30 IID640 - __ epopcntq(r28, r28, true); // {NF}popcnt r28, r28 IID641 - __ erolq(r28, r14, false); // {EVEX}rol r28, r14, cl IID642 - __ erolq(r23, r23, false); // rol r23, cl IID643 - __ erolq(r23, r24, true); // {NF}rol r23, r24, cl IID644 - __ erolq(r21, r21, true); // {NF}rol r21, r21, cl IID645 - __ erorq(r31, r22, false); // {EVEX}ror r31, r22, cl IID646 - __ erorq(r28, r28, false); // ror r28, cl IID647 - __ erorq(r17, r10, true); // {NF}ror r17, r10, cl IID648 - __ erorq(r9, r9, true); // {NF}ror r9, r9, cl IID649 - __ esalq(r29, r30, false); // {EVEX}sal r29, r30, cl IID650 - __ esalq(r11, r11, false); // sal r11, cl IID651 - __ esalq(r26, r11, true); // {NF}sal r26, r11, cl IID652 - __ esalq(r16, r16, true); // {NF}sal r16, r16, cl IID653 - __ esarq(rbx, r15, false); // {EVEX}sar rbx, r15, cl IID654 - __ esarq(r14, r14, false); // sar r14, cl IID655 - __ esarq(r25, r16, true); // {NF}sar r25, r16, cl IID656 - __ esarq(r8, r8, true); // {NF}sar r8, r8, cl IID657 - __ edecq(r11, r13, false); // {EVEX}dec r11, r13 IID658 - __ edecq(rcx, rcx, false); // dec rcx IID659 - __ edecq(r21, r18, true); // {NF}dec r21, r18 IID660 - __ edecq(r28, r28, true); // {NF}dec r28, r28 IID661 - __ eincq(r16, r16, false); // inc r16 IID662 - __ eincq(r29, r29, false); // inc r29 IID663 - __ eincq(r18, r9, true); // {NF}inc r18, r9 IID664 - __ eincq(r19, r19, true); // {NF}inc r19, r19 IID665 - __ eshlq(r19, r18, false); // {EVEX}shl r19, r18, cl IID666 - __ eshlq(r8, r8, false); // shl r8, cl IID667 - __ eshlq(r12, r15, true); // {NF}shl r12, r15, cl IID668 - __ eshlq(r29, r29, true); // {NF}shl r29, r29, cl IID669 - __ eshrq(r28, r24, false); // {EVEX}shr r28, r24, cl IID670 - __ eshrq(r19, r19, false); // shr r19, cl IID671 - __ eshrq(r8, r28, true); // {NF}shr r8, r28, cl IID672 - __ eshrq(r17, r17, true); // {NF}shr r17, r17, cl IID673 - __ etzcntq(r28, r16, false); // {EVEX}tzcnt r28, r16 IID674 - __ etzcntq(r14, r14, false); // {EVEX}tzcnt r14, r14 IID675 - __ etzcntq(r12, r31, true); // {NF}tzcnt r12, r31 IID676 - __ etzcntq(r14, r14, true); // {NF}tzcnt r14, r14 IID677 - __ eimulq(r31, Address(r13, -0x69c4b352), false); // {EVEX}imul r31, qword ptr [r13-0x69c4b352] IID678 - __ eimulq(r17, Address(r18, -0x60ab1105), true); // {NF}imul r17, qword ptr [r18-0x60ab1105] IID679 - __ elzcntq(r27, Address(r14, r25, (Address::ScaleFactor)2, +0x2798bf83), false); // {EVEX}lzcnt r27, qword ptr [r14+r25*4+0x2798bf83] IID680 - __ elzcntq(r23, Address(r10, r11, (Address::ScaleFactor)0, -0x378e635d), true); // {NF}lzcnt r23, qword ptr [r10+r11*1-0x378e635d] IID681 - __ enegq(rcx, Address(r19, r9, (Address::ScaleFactor)3, -0x6847d440), false); // {EVEX}neg rcx, qword ptr [r19+r9*8-0x6847d440] IID682 - __ enegq(rcx, Address(rbx, rcx, (Address::ScaleFactor)0, +0x6f92d38d), true); // {NF}neg rcx, qword ptr [rbx+rcx*1+0x6f92d38d] IID683 - __ epopcntq(r20, Address(r12, -0x2a8b27d6), false); // {EVEX}popcnt r20, qword ptr [r12-0x2a8b27d6] IID684 - __ epopcntq(r31, Address(r30, +0x4603f6d0), true); // {NF}popcnt r31, qword ptr [r30+0x4603f6d0] IID685 - __ esalq(rbx, Address(r24, +0x567d06f9), false); // {EVEX}sal rbx, qword ptr [r24+0x567d06f9], cl IID686 - __ esalq(r12, Address(r24, r28, (Address::ScaleFactor)0, -0x1c4c584e), true); // {NF}sal r12, qword ptr [r24+r28*1-0x1c4c584e], cl IID687 - __ esarq(r12, Address(r23, r24, (Address::ScaleFactor)2, -0x3157bcba), false); // {EVEX}sar r12, qword ptr [r23+r24*4-0x3157bcba], cl IID688 - __ esarq(r8, Address(r14, r24, (Address::ScaleFactor)2, -0x714290a5), true); // {NF}sar r8, qword ptr [r14+r24*4-0x714290a5], cl IID689 - __ edecq(r23, Address(r8, r15, (Address::ScaleFactor)1, -0x5ae272dd), false); // {EVEX}dec r23, qword ptr [r8+r15*2-0x5ae272dd] IID690 - __ edecq(r13, Address(r29, r9, (Address::ScaleFactor)3, -0x5b5174a9), true); // {NF}dec r13, qword ptr [r29+r9*8-0x5b5174a9] IID691 - __ eincq(r11, Address(r21, r31, (Address::ScaleFactor)3, -0x2176b4dc), false); // {EVEX}inc r11, qword ptr [r21+r31*8-0x2176b4dc] IID692 - __ eincq(r13, Address(rcx, r16, (Address::ScaleFactor)0, -0x36b448c9), true); // {NF}inc r13, qword ptr [rcx+r16*1-0x36b448c9] IID693 - __ eshrq(r26, Address(r25, rcx, (Address::ScaleFactor)2, -0x5f894993), false); // {EVEX}shr r26, qword ptr [r25+rcx*4-0x5f894993], cl IID694 - __ eshrq(r25, Address(r9, +0x51798d21), true); // {NF}shr r25, qword ptr [r9+0x51798d21], cl IID695 - __ etzcntq(r28, Address(r13, r26, (Address::ScaleFactor)2, +0x207196f6), false); // {EVEX}tzcnt r28, qword ptr [r13+r26*4+0x207196f6] IID696 - __ etzcntq(rbx, Address(r19, r13, (Address::ScaleFactor)0, -0x24d937d5), true); // {NF}tzcnt rbx, qword ptr [r19+r13*1-0x24d937d5] IID697 - __ eaddq(r17, Address(r30, +0x3935ccff), r31, false); // {EVEX}add r17, qword ptr [r30+0x3935ccff], r31 IID698 - __ eaddq(r14, Address(r27, r10, (Address::ScaleFactor)2, -0x34ad9bab), r14, false); // {EVEX}add r14, qword ptr [r27+r10*4-0x34ad9bab], r14 IID699 - __ eaddq(r18, Address(r20, r23, (Address::ScaleFactor)0, +0x5ad3ed4b), r30, true); // {NF}add r18, qword ptr [r20+r23*1+0x5ad3ed4b], r30 IID700 - __ eaddq(r20, Address(rdx, -0x322a99e5), r20, true); // {NF}add r20, qword ptr [rdx-0x322a99e5], r20 IID701 - __ eandq(r31, Address(rbx, r27, (Address::ScaleFactor)3, +0x4ce247d2), r17, false); // {EVEX}and r31, qword ptr [rbx+r27*8+0x4ce247d2], r17 IID702 - __ eandq(r30, Address(r18, r19, (Address::ScaleFactor)1, -0x4ee3d14), r30, false); // {EVEX}and r30, qword ptr [r18+r19*2-0x4ee3d14], r30 IID703 - __ eandq(r28, Address(r11, rbx, (Address::ScaleFactor)3, -0x28994bbf), r24, true); // {NF}and r28, qword ptr [r11+rbx*8-0x28994bbf], r24 IID704 - __ eandq(r30, Address(r22, +0x7d21c24), r30, true); // {NF}and r30, qword ptr [r22+0x7d21c24], r30 IID705 - __ eorq(r26, Address(r15, r19, (Address::ScaleFactor)3, +0x58c21792), r20, false); // {EVEX}or r26, qword ptr [r15+r19*8+0x58c21792], r20 IID706 - __ eorq(r13, Address(r10, r27, (Address::ScaleFactor)2, -0x2c70d333), r13, false); // {EVEX}or r13, qword ptr [r10+r27*4-0x2c70d333], r13 IID707 - __ eorq(rbx, Address(r12, rbx, (Address::ScaleFactor)0, -0x1fb0f1bc), r26, true); // {NF}or rbx, qword ptr [r12+rbx*1-0x1fb0f1bc], r26 IID708 - __ eorq(r31, Address(r27, r31, (Address::ScaleFactor)1, +0x28d1756), r31, true); // {NF}or r31, qword ptr [r27+r31*2+0x28d1756], r31 IID709 - __ esubq(r24, Address(r28, r23, (Address::ScaleFactor)1, +0x6980f610), r27, false); // {EVEX}sub r24, qword ptr [r28+r23*2+0x6980f610], r27 IID710 - __ esubq(r15, Address(r11, r30, (Address::ScaleFactor)3, -0x49777e7), r15, false); // {EVEX}sub r15, qword ptr [r11+r30*8-0x49777e7], r15 IID711 - __ esubq(r17, Address(r25, r13, (Address::ScaleFactor)2, +0x31619e46), r31, true); // {NF}sub r17, qword ptr [r25+r13*4+0x31619e46], r31 IID712 - __ esubq(r18, Address(r11, r10, (Address::ScaleFactor)2, +0x1922861a), r18, true); // {NF}sub r18, qword ptr [r11+r10*4+0x1922861a], r18 IID713 - __ exorq(rbx, Address(r11, -0x4716d420), r21, false); // {EVEX}xor rbx, qword ptr [r11-0x4716d420], r21 IID714 - __ exorq(r8, Address(rdx, r9, (Address::ScaleFactor)2, -0x4cfe39c), r8, false); // {EVEX}xor r8, qword ptr [rdx+r9*4-0x4cfe39c], r8 IID715 - __ exorq(r16, Address(r14, r27, (Address::ScaleFactor)0, +0x7c6654d9), r25, true); // {NF}xor r16, qword ptr [r14+r27*1+0x7c6654d9], r25 IID716 - __ exorq(r29, Address(r15, -0x5efab479), r29, true); // {NF}xor r29, qword ptr [r15-0x5efab479], r29 IID717 - __ eaddq(r19, Address(r13, r22, (Address::ScaleFactor)2, +0x68b64559), 16777216, false); // {EVEX}add r19, qword ptr [r13+r22*4+0x68b64559], 16777216 IID718 - __ eaddq(r16, Address(r13, r31, (Address::ScaleFactor)3, -0x65143af5), 1, true); // {NF}add r16, qword ptr [r13+r31*8-0x65143af5], 1 IID719 - __ eandq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), 1, false); // {EVEX}and r31, qword ptr [r24+r13*2-0x25b16a0e], 1 IID720 - __ eandq(r11, Address(r28, -0xf6d4b26), 65536, true); // {NF}and r11, qword ptr [r28-0xf6d4b26], 65536 IID721 - __ eimulq(rcx, Address(r18, r10, (Address::ScaleFactor)0, +0x46ec6da1), 16777216, false); // {EVEX}imul rcx, qword ptr [r18+r10*1+0x46ec6da1], 16777216 IID722 - __ eimulq(r15, Address(r9, r10, (Address::ScaleFactor)3, -0x7fc36af3), 16, true); // {NF}imul r15, qword ptr [r9+r10*8-0x7fc36af3], 16 IID723 - __ eorq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), 1, false); // {EVEX}or r17, qword ptr [r27+r30*1+0x1b4cda2c], 1 IID724 - __ eorq(rdx, Address(r25, r14, (Address::ScaleFactor)2, -0x59aa6b85), 4096, true); // {NF}or rdx, qword ptr [r25+r14*4-0x59aa6b85], 4096 IID725 - __ esalq(r17, Address(r26, r21, (Address::ScaleFactor)1, -0x6ab1f15f), 8, false); // {EVEX}sal r17, qword ptr [r26+r21*2-0x6ab1f15f], 8 IID726 - __ esalq(r12, Address(r22, r17, (Address::ScaleFactor)0, -0x43ac14ab), 2, true); // {NF}sal r12, qword ptr [r22+r17*1-0x43ac14ab], 2 IID727 - __ esarq(r29, Address(r18, r16, (Address::ScaleFactor)0, -0x59dc0c61), 4, false); // {EVEX}sar r29, qword ptr [r18+r16*1-0x59dc0c61], 4 IID728 - __ esarq(r16, Address(r11, -0x7bdd314), 4, true); // {NF}sar r16, qword ptr [r11-0x7bdd314], 4 IID729 - __ eshrq(r26, Address(r23, r27, (Address::ScaleFactor)3, -0x55b92314), 16, false); // {EVEX}shr r26, qword ptr [r23+r27*8-0x55b92314], 16 IID730 - __ eshrq(r23, Address(r16, r29, (Address::ScaleFactor)1, +0x71311a1d), 2, true); // {NF}shr r23, qword ptr [r16+r29*2+0x71311a1d], 2 IID731 - __ esubq(r25, Address(r9, -0x9532bac), 1048576, false); // {EVEX}sub r25, qword ptr [r9-0x9532bac], 1048576 IID732 - __ esubq(r17, Address(r8, r23, (Address::ScaleFactor)0, +0x55d06ca2), 1048576, true); // {NF}sub r17, qword ptr [r8+r23*1+0x55d06ca2], 1048576 IID733 - __ exorq(r29, Address(r9, r24, (Address::ScaleFactor)0, -0x2c141c1), 1048576, false); // {EVEX}xor r29, qword ptr [r9+r24*1-0x2c141c1], 1048576 IID734 - __ exorq(r28, Address(r22, r19, (Address::ScaleFactor)1, -0x2d9d9abd), 16, true); // {NF}xor r28, qword ptr [r22+r19*2-0x2d9d9abd], 16 IID735 - __ eaddq(r22, r14, 16, false); // {EVEX}add r22, r14, 16 IID736 - __ eaddq(rax, r12, 16, false); // {EVEX}add rax, r12, 16 IID737 - __ eaddq(r24, r24, 65536, false); // add r24, 65536 IID738 - __ eaddq(r21, rbx, 65536, true); // {NF}add r21, rbx, 65536 IID739 - __ eaddq(rax, rbx, 65536, true); // {NF}add rax, rbx, 65536 IID740 - __ eaddq(r24, r24, 65536, true); // {NF}add r24, r24, 65536 IID741 - __ eandq(r21, r27, 16777216, false); // {EVEX}and r21, r27, 16777216 IID742 - __ eandq(rax, r27, 16777216, false); // {EVEX}and rax, r27, 16777216 IID743 - __ eandq(r24, r24, 65536, false); // and r24, 65536 IID744 - __ eandq(r13, r31, 1048576, true); // {NF}and r13, r31, 1048576 IID745 - __ eandq(rax, r21, 1048576, true); // {NF}and rax, r21, 1048576 IID746 - __ eandq(r30, r30, 1048576, true); // {NF}and r30, r30, 1048576 IID747 - __ eimulq(r8, r13, 268435456, false); // {EVEX}imul r8, r13, 268435456 IID748 - __ eimulq(rax, r31, 268435456, false); // {EVEX}imul rax, r31, 268435456 IID749 - __ eimulq(r13, r13, 65536, false); // {EVEX}imul r13, r13, 65536 IID750 - __ eimulq(r14, r29, 1048576, true); // {NF}imul r14, r29, 1048576 IID751 - __ eimulq(rax, r22, 1048576, true); // {NF}imul rax, r22, 1048576 IID752 - __ eimulq(r8, r8, 268435456, true); // {NF}imul r8, r8, 268435456 IID753 - __ eorq(r30, r15, 4096, false); // {EVEX}or r30, r15, 4096 IID754 - __ eorq(rax, r28, 4096, false); // {EVEX}or rax, r28, 4096 IID755 - __ eorq(r26, r26, 1048576, false); // or r26, 1048576 IID756 - __ eorq(r16, r12, 268435456, true); // {NF}or r16, r12, 268435456 IID757 - __ eorq(rax, r9, 268435456, true); // {NF}or rax, r9, 268435456 IID758 - __ eorq(r23, r23, 256, true); // {NF}or r23, r23, 256 IID759 - __ erclq(r15, r9, 16); // {EVEX}rcl r15, r9, 16 IID760 - __ erclq(rax, r8, 16); // {EVEX}rcl rax, r8, 16 IID761 - __ erclq(r25, r25, 1); // rcl r25, 1 IID762 - __ erolq(r9, r17, 16, false); // {EVEX}rol r9, r17, 16 IID763 - __ erolq(rax, r20, 16, false); // {EVEX}rol rax, r20, 16 IID764 - __ erolq(r27, r27, 1, false); // rol r27, 1 IID765 - __ erolq(r20, r31, 1, true); // {NF}rol r20, r31, 1 IID766 - __ erolq(rax, r18, 1, true); // {NF}rol rax, r18, 1 IID767 - __ erolq(r28, r28, 16, true); // {NF}rol r28, r28, 16 IID768 - __ erorq(r26, r18, 16, false); // {EVEX}ror r26, r18, 16 IID769 - __ erorq(rax, r24, 16, false); // {EVEX}ror rax, r24, 16 IID770 - __ erorq(r22, r22, 16, false); // ror r22, 16 IID771 - __ erorq(r27, r29, 1, true); // {NF}ror r27, r29, 1 IID772 - __ erorq(rax, r18, 1, true); // {NF}ror rax, r18, 1 IID773 - __ erorq(r21, r21, 1, true); // {NF}ror r21, r21, 1 IID774 - __ esalq(r12, rcx, 2, false); // {EVEX}sal r12, rcx, 2 IID775 - __ esalq(rax, r24, 2, false); // {EVEX}sal rax, r24, 2 IID776 - __ esalq(r22, r22, 8, false); // sal r22, 8 IID777 - __ esalq(r17, r23, 8, true); // {NF}sal r17, r23, 8 IID778 - __ esalq(rax, r27, 8, true); // {NF}sal rax, r27, 8 IID779 - __ esalq(r23, r23, 1, true); // {NF}sal r23, r23, 1 IID780 - __ esarq(r8, r25, 16, false); // {EVEX}sar r8, r25, 16 IID781 - __ esarq(rax, r23, 16, false); // {EVEX}sar rax, r23, 16 IID782 - __ esarq(r9, r9, 4, false); // sar r9, 4 IID783 - __ esarq(r22, r13, 1, true); // {NF}sar r22, r13, 1 IID784 - __ esarq(rax, r11, 1, true); // {NF}sar rax, r11, 1 IID785 - __ esarq(r12, r12, 2, true); // {NF}sar r12, r12, 2 IID786 - __ eshlq(rcx, r30, 8, false); // {EVEX}shl rcx, r30, 8 IID787 - __ eshlq(rax, r19, 8, false); // {EVEX}shl rax, r19, 8 IID788 - __ eshlq(r13, r13, 2, false); // shl r13, 2 IID789 - __ eshlq(r18, r11, 8, true); // {NF}shl r18, r11, 8 IID790 - __ eshlq(rax, r9, 8, true); // {NF}shl rax, r9, 8 IID791 - __ eshlq(rcx, rcx, 16, true); // {NF}shl rcx, rcx, 16 IID792 - __ eshrq(r10, r22, 4, false); // {EVEX}shr r10, r22, 4 IID793 - __ eshrq(rax, r9, 4, false); // {EVEX}shr rax, r9, 4 IID794 - __ eshrq(r12, r12, 2, false); // shr r12, 2 IID795 - __ eshrq(r26, r31, 8, true); // {NF}shr r26, r31, 8 IID796 - __ eshrq(rax, r12, 8, true); // {NF}shr rax, r12, 8 IID797 - __ eshrq(r28, r28, 1, true); // {NF}shr r28, r28, 1 IID798 - __ esubq(r15, r30, 65536, false); // {EVEX}sub r15, r30, 65536 IID799 - __ esubq(rax, rcx, 65536, false); // {EVEX}sub rax, rcx, 65536 IID800 - __ esubq(r26, r26, 16, false); // sub r26, 16 IID801 - __ esubq(r12, r14, 1, true); // {NF}sub r12, r14, 1 IID802 - __ esubq(rax, r21, 1, true); // {NF}sub rax, r21, 1 IID803 - __ esubq(r20, r20, 1048576, true); // {NF}sub r20, r20, 1048576 IID804 - __ exorq(r11, rbx, 16777216, false); // {EVEX}xor r11, rbx, 16777216 IID805 - __ exorq(rax, r23, 16777216, false); // {EVEX}xor rax, r23, 16777216 IID806 - __ exorq(r31, r31, 268435456, false); // xor r31, 268435456 IID807 - __ exorq(r29, r28, 4096, true); // {NF}xor r29, r28, 4096 IID808 - __ exorq(rax, r19, 4096, true); // {NF}xor rax, r19, 4096 IID809 - __ exorq(rdx, rdx, 268435456, true); // {NF}xor rdx, rdx, 268435456 IID810 - __ eorq_imm32(rdx, rdx, 1048576, false); // or rdx, 1048576 IID811 - __ eorq_imm32(rax, r22, 1048576, false); // {EVEX}or rax, r22, 1048576 IID812 - __ eorq_imm32(r29, r29, 1048576, false); // or r29, 1048576 IID813 - __ eorq_imm32(r17, rcx, 4194304, false); // {EVEX}or r17, rcx, 4194304 IID814 - __ eorq_imm32(rax, r25, 4194304, false); // {EVEX}or rax, r25, 4194304 IID815 - __ eorq_imm32(r27, r27, 1073741824, false); // or r27, 1073741824 IID816 - __ esubq_imm32(r16, r19, 4194304, false); // {EVEX}sub r16, r19, 4194304 IID817 - __ esubq_imm32(rax, r31, 4194304, false); // {EVEX}sub rax, r31, 4194304 IID818 - __ esubq_imm32(r26, r26, 262144, false); // sub r26, 262144 IID819 - __ esubq_imm32(r17, r22, 1073741824, true); // {NF}sub r17, r22, 1073741824 IID820 - __ esubq_imm32(rax, r18, 1073741824, true); // {NF}sub rax, r18, 1073741824 IID821 - __ esubq_imm32(r23, r23, 268435456, true); // {NF}sub r23, r23, 268435456 IID822 - __ eaddq(r13, r30, Address(r24, r19, (Address::ScaleFactor)1, +0x56ea3a3b), false); // {EVEX}add r13, r30, qword ptr [r24+r19*2+0x56ea3a3b] IID823 - __ eaddq(r29, r15, Address(r26, r27, (Address::ScaleFactor)3, -0x4b113958), true); // {NF}add r29, r15, qword ptr [r26+r27*8-0x4b113958] IID824 - __ eandq(r12, r30, Address(r31, -0x46103c74), false); // {EVEX}and r12, r30, qword ptr [r31-0x46103c74] IID825 - __ eandq(r27, r10, Address(r22, r25, (Address::ScaleFactor)1, +0x6a1ebee5), true); // {NF}and r27, r10, qword ptr [r22+r25*2+0x6a1ebee5] IID826 - __ eorq(r30, r26, Address(r11, r18, (Address::ScaleFactor)2, -0x2b9fff29), false); // {EVEX}or r30, r26, qword ptr [r11+r18*4-0x2b9fff29] IID827 - __ eorq(r9, r12, Address(r18, r17, (Address::ScaleFactor)0, +0xb4859f6), true); // {NF}or r9, r12, qword ptr [r18+r17*1+0xb4859f6] IID828 - __ eimulq(rdx, r17, Address(r24, rdx, (Address::ScaleFactor)2, +0x3d284cd8), false); // {EVEX}imul rdx, r17, qword ptr [r24+rdx*4+0x3d284cd8] IID829 - __ eimulq(r29, r26, Address(r30, r12, (Address::ScaleFactor)1, +0x6e813124), true); // {NF}imul r29, r26, qword ptr [r30+r12*2+0x6e813124] IID830 - __ esubq(rbx, r13, Address(r22, -0x702a289e), false); // {EVEX}sub rbx, r13, qword ptr [r22-0x702a289e] IID831 - __ esubq(r23, r29, Address(r25, rdx, (Address::ScaleFactor)0, -0x6252a7ed), true); // {NF}sub r23, r29, qword ptr [r25+rdx*1-0x6252a7ed] IID832 - __ exorq(r8, r18, Address(r19, r14, (Address::ScaleFactor)2, -0xebfa697), false); // {EVEX}xor r8, r18, qword ptr [r19+r14*4-0xebfa697] IID833 - __ exorq(r10, r28, Address(r26, +0x168381ca), true); // {NF}xor r10, r28, qword ptr [r26+0x168381ca] IID834 - __ eaddq(rcx, r18, r8, false); // {load}{EVEX}add rcx, r18, r8 IID835 - __ eaddq(rcx, rcx, r14, false); // {load}add rcx, r14 IID836 - __ eaddq(r23, r10, r16, true); // {load}{NF}add r23, r10, r16 IID837 - __ eaddq(r11, r11, r24, true); // {load}{NF}add r11, r11, r24 IID838 - __ eadcxq(r9, r18, rdx); // {load}{EVEX}adcx r9, r18, rdx IID839 - __ eadcxq(r8, r8, r15); // {load}adcx r8, r15 IID840 - __ eadoxq(r15, r22, r26); // {load}{EVEX}adox r15, r22, r26 IID841 - __ eadoxq(r11, r11, rdx); // {load}adox r11, rdx IID842 - __ eandq(r19, rdx, r22, false); // {load}{EVEX}and r19, rdx, r22 IID843 - __ eandq(r29, r29, r17, false); // {load}and r29, r17 IID844 - __ eandq(r23, r27, r15, true); // {load}{NF}and r23, r27, r15 IID845 - __ eandq(r9, r9, r13, true); // {load}{NF}and r9, r9, r13 IID846 - __ eimulq(r18, r15, r16, false); // {load}{EVEX}imul r18, r15, r16 IID847 - __ eimulq(rcx, rcx, r17, false); // {load}imul rcx, r17 IID848 - __ eimulq(r23, r12, r20, true); // {load}{NF}imul r23, r12, r20 IID849 - __ eimulq(r10, r10, r9, true); // {load}{NF}imul r10, r10, r9 IID850 - __ eorq(rdx, r19, r14, false); // {load}{EVEX}or rdx, r19, r14 IID851 - __ eorq(rcx, rcx, r13, false); // {load}or rcx, r13 IID852 - __ eorq(r9, r25, r29, true); // {load}{NF}or r9, r25, r29 IID853 - __ eorq(rdx, rdx, r25, true); // {load}{NF}or rdx, rdx, r25 IID854 - __ esubq(r23, r8, r16, false); // {load}{EVEX}sub r23, r8, r16 IID855 - __ esubq(r13, r13, r13, false); // {load}sub r13, r13 IID856 - __ esubq(r19, r12, r15, true); // {load}{NF}sub r19, r12, r15 IID857 - __ esubq(r9, r9, rdx, true); // {load}{NF}sub r9, r9, rdx IID858 - __ exorq(r13, r16, r31, false); // {load}{EVEX}xor r13, r16, r31 IID859 - __ exorq(r17, r17, r30, false); // {load}xor r17, r30 IID860 - __ exorq(r19, r30, r20, true); // {load}{NF}xor r19, r30, r20 IID861 - __ exorq(r31, r31, r13, true); // {load}{NF}xor r31, r31, r13 IID862 - __ eshldq(r22, r10, r13, 4, false); // {EVEX}shld r22, r10, r13, 4 IID863 - __ eshldq(r24, r24, r21, 16, false); // shld r24, r21, 16 IID864 - __ eshldq(r20, r13, r27, 16, true); // {NF}shld r20, r13, r27, 16 IID865 - __ eshldq(r31, r31, r19, 2, true); // {NF}shld r31, r31, r19, 2 IID866 - __ eshrdq(r30, r20, r11, 8, false); // {EVEX}shrd r30, r20, r11, 8 IID867 - __ eshrdq(rdx, rdx, r15, 1, false); // shrd rdx, r15, 1 IID868 - __ eshrdq(r28, r30, r14, 2, true); // {NF}shrd r28, r30, r14, 2 IID869 - __ eshrdq(r20, r20, r16, 1, true); // {NF}shrd r20, r20, r16, 1 IID870 - __ ecmovq (Assembler::Condition::overflow, r21, r17, r28); // cmovo r21, r17, r28 IID871 - __ ecmovq (Assembler::Condition::overflow, r15, r15, r30); // cmovo r15, r30 IID872 - __ ecmovq (Assembler::Condition::noOverflow, rcx, r15, r15); // cmovno rcx, r15, r15 IID873 - __ ecmovq (Assembler::Condition::noOverflow, rcx, rcx, r13); // cmovno rcx, r13 IID874 - __ ecmovq (Assembler::Condition::below, rdx, r26, r26); // cmovb rdx, r26, r26 IID875 - __ ecmovq (Assembler::Condition::below, r28, r28, r15); // cmovb r28, r15 IID876 - __ ecmovq (Assembler::Condition::aboveEqual, r8, rdx, rcx); // cmovae r8, rdx, rcx IID877 - __ ecmovq (Assembler::Condition::aboveEqual, rcx, rcx, rcx); // cmovae rcx, rcx IID878 - __ ecmovq (Assembler::Condition::zero, r10, r13, r9); // cmovz r10, r13, r9 IID879 - __ ecmovq (Assembler::Condition::zero, r14, r14, r27); // cmovz r14, r27 IID880 - __ ecmovq (Assembler::Condition::notZero, r11, r23, r9); // cmovnz r11, r23, r9 IID881 - __ ecmovq (Assembler::Condition::notZero, r11, r11, rdx); // cmovnz r11, rdx IID882 - __ ecmovq (Assembler::Condition::belowEqual, r31, r14, r25); // cmovbe r31, r14, r25 IID883 - __ ecmovq (Assembler::Condition::belowEqual, r20, r20, r12); // cmovbe r20, r12 IID884 - __ ecmovq (Assembler::Condition::above, rdx, r10, r28); // cmova rdx, r10, r28 IID885 - __ ecmovq (Assembler::Condition::above, r8, r8, r17); // cmova r8, r17 IID886 - __ ecmovq (Assembler::Condition::negative, rcx, r30, r23); // cmovs rcx, r30, r23 IID887 - __ ecmovq (Assembler::Condition::negative, r26, r26, r18); // cmovs r26, r18 IID888 - __ ecmovq (Assembler::Condition::positive, rdx, rbx, r18); // cmovns rdx, rbx, r18 IID889 - __ ecmovq (Assembler::Condition::positive, r21, r21, r13); // cmovns r21, r13 IID890 - __ ecmovq (Assembler::Condition::parity, r27, r28, r27); // cmovp r27, r28, r27 IID891 - __ ecmovq (Assembler::Condition::parity, r11, r11, r30); // cmovp r11, r30 IID892 - __ ecmovq (Assembler::Condition::noParity, rcx, r21, r18); // cmovnp rcx, r21, r18 IID893 - __ ecmovq (Assembler::Condition::noParity, rcx, rcx, r29); // cmovnp rcx, r29 IID894 - __ ecmovq (Assembler::Condition::less, rdx, r21, r12); // cmovl rdx, r21, r12 IID895 - __ ecmovq (Assembler::Condition::less, rdx, rdx, r26); // cmovl rdx, r26 IID896 - __ ecmovq (Assembler::Condition::greaterEqual, r17, rbx, r22); // cmovge r17, rbx, r22 IID897 - __ ecmovq (Assembler::Condition::greaterEqual, rdx, rdx, r11); // cmovge rdx, r11 IID898 - __ ecmovq (Assembler::Condition::lessEqual, rdx, r14, r8); // cmovle rdx, r14, r8 IID899 - __ ecmovq (Assembler::Condition::lessEqual, r14, r14, r8); // cmovle r14, r8 IID900 - __ ecmovq (Assembler::Condition::greater, r25, r29, r21); // cmovg r25, r29, r21 IID901 - __ ecmovq (Assembler::Condition::greater, r26, r26, r30); // cmovg r26, r30 IID902 - __ ecmovq (Assembler::Condition::overflow, r24, r21, Address(r13, r11, (Address::ScaleFactor)1, +0x439c521e)); // cmovo r24, r21, qword ptr [r13+r11*2+0x439c521e] IID903 - __ ecmovq (Assembler::Condition::noOverflow, r11, r18, Address(r29, r16, (Address::ScaleFactor)0, +0x632127f)); // cmovno r11, r18, qword ptr [r29+r16*1+0x632127f] IID904 - __ ecmovq (Assembler::Condition::below, r16, r8, Address(r8, r26, (Address::ScaleFactor)1, +0x10633def)); // cmovb r16, r8, qword ptr [r8+r26*2+0x10633def] IID905 - __ ecmovq (Assembler::Condition::aboveEqual, r13, r14, Address(r18, -0x54f69e38)); // cmovae r13, r14, qword ptr [r18-0x54f69e38] IID906 - __ ecmovq (Assembler::Condition::zero, r12, r8, Address(r31, r26, (Address::ScaleFactor)1, -0x7a1e447a)); // cmovz r12, r8, qword ptr [r31+r26*2-0x7a1e447a] IID907 - __ ecmovq (Assembler::Condition::notZero, r29, r29, Address(r19, r11, (Address::ScaleFactor)2, -0x35d82dd2)); // cmovnz r29, qword ptr [r19+r11*4-0x35d82dd2] IID908 - __ ecmovq (Assembler::Condition::belowEqual, rcx, r18, Address(r25, r28, (Address::ScaleFactor)0, +0x30be64a0)); // cmovbe rcx, r18, qword ptr [r25+r28*1+0x30be64a0] IID909 - __ ecmovq (Assembler::Condition::above, r28, r12, Address(r10, r16, (Address::ScaleFactor)1, -0x22b8fefa)); // cmova r28, r12, qword ptr [r10+r16*2-0x22b8fefa] IID910 - __ ecmovq (Assembler::Condition::negative, r11, r8, Address(rbx, r11, (Address::ScaleFactor)3, +0x25cc9e96)); // cmovs r11, r8, qword ptr [rbx+r11*8+0x25cc9e96] IID911 - __ ecmovq (Assembler::Condition::positive, r12, r27, Address(r11, -0xc2d70fe)); // cmovns r12, r27, qword ptr [r11-0xc2d70fe] IID912 - __ ecmovq (Assembler::Condition::parity, r8, r26, Address(r19, rbx, (Address::ScaleFactor)1, -0x486db7ea)); // cmovp r8, r26, qword ptr [r19+rbx*2-0x486db7ea] IID913 - __ ecmovq (Assembler::Condition::noParity, r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884)); // cmovnp r30, r10, qword ptr [r14+r18*8+0x14884884] IID914 - __ ecmovq (Assembler::Condition::less, r27, r8, Address(r29, r14, (Address::ScaleFactor)2, +0x92b7a8)); // cmovl r27, r8, qword ptr [r29+r14*4+0x92b7a8] IID915 - __ ecmovq (Assembler::Condition::greaterEqual, r14, r28, Address(r19, rdx, (Address::ScaleFactor)0, +0x9c2d45)); // cmovge r14, r28, qword ptr [r19+rdx*1+0x9c2d45] IID916 - __ ecmovq (Assembler::Condition::lessEqual, r25, r8, Address(rcx, r18, (Address::ScaleFactor)2, +0x6655c86b)); // cmovle r25, r8, qword ptr [rcx+r18*4+0x6655c86b] IID917 - __ ecmovq (Assembler::Condition::greater, r19, r21, Address(r10, r25, (Address::ScaleFactor)0, -0x1005430b)); // cmovg r19, r21, qword ptr [r10+r25*1-0x1005430b] IID918 + __ adcq(r30, r31); // {load}adc r30, r31 IID528 + __ cmpq(r12, rdx); // {load}cmp r12, rdx IID529 + __ imulq(r21, r24); // {load}imul r21, r24 IID530 + __ popcntq(r9, r25); // {load}popcnt r9, r25 IID531 + __ sbbq(r8, r12); // {load}sbb r8, r12 IID532 + __ subq(r31, r24); // {load}sub r31, r24 IID533 + __ tzcntq(r10, r16); // {load}tzcnt r10, r16 IID534 + __ lzcntq(r20, r21); // {load}lzcnt r20, r21 IID535 + __ addq(rdx, r17); // {load}add rdx, r17 IID536 + __ andq(r14, r13); // {load}and r14, r13 IID537 + __ orq(r20, r24); // {load}or r20, r24 IID538 + __ xorq(r21, r22); // {load}xor r21, r22 IID539 + __ movq(r12, r27); // {load}mov r12, r27 IID540 + __ bsfq(r23, rdx); // {load}bsf r23, rdx IID541 + __ bsrq(r31, r28); // {load}bsr r31, r28 IID542 + __ btq(r8, r25); // {load}bt r8, r25 IID543 + __ xchgq(r21, rbx); // {load}xchg r21, rbx IID544 + __ testq(r23, r23); // {load}test r23, r23 IID545 + __ addq(Address(r19, -0x180d3ea1), r10); // add qword ptr [r19-0x180d3ea1], r10 IID546 + __ andq(Address(r11, r17, (Address::ScaleFactor)1, -0x78976be8), r25); // and qword ptr [r11+r17*2-0x78976be8], r25 IID547 + __ cmpq(Address(rbx, r28, (Address::ScaleFactor)3, +0x35f72102), r13); // cmp qword ptr [rbx+r28*8+0x35f72102], r13 IID548 + __ orq(Address(r8, -0x34465011), r21); // or qword ptr [r8-0x34465011], r21 IID549 + __ xorq(Address(r19, -0x404b22dd), r18); // xor qword ptr [r19-0x404b22dd], r18 IID550 + __ subq(Address(r23, r27, (Address::ScaleFactor)3, -0x428d2646), r14); // sub qword ptr [r23+r27*8-0x428d2646], r14 IID551 + __ movq(Address(r9, rcx, (Address::ScaleFactor)2, -0x72611661), r28); // mov qword ptr [r9+rcx*4-0x72611661], r28 IID552 + __ xaddq(Address(r24, r21, (Address::ScaleFactor)2, +0x3a6be990), rbx); // xadd qword ptr [r24+r21*4+0x3a6be990], rbx IID553 + __ andq(Address(r22, r10, (Address::ScaleFactor)0, +0x7ef8bdd), 1048576); // and qword ptr [r22+r10*1+0x7ef8bdd], 1048576 IID554 + __ addq(Address(r13, r28, (Address::ScaleFactor)0, -0x754789b1), 65536); // add qword ptr [r13+r28*1-0x754789b1], 65536 IID555 + __ cmpq(Address(r10, -0xbd2a8da), 268435456); // cmp qword ptr [r10-0xbd2a8da], 268435456 IID556 + __ sarq(Address(r23, r14, (Address::ScaleFactor)1, +0x6a16d9f5), 4); // sar qword ptr [r23+r14*2+0x6a16d9f5], 4 IID557 + __ salq(Address(rcx, r21, (Address::ScaleFactor)1, +0x5f66ac1e), 8); // sal qword ptr [rcx+r21*2+0x5f66ac1e], 8 IID558 + __ sbbq(Address(rcx, r22, (Address::ScaleFactor)3, -0x48c954c), 268435456); // sbb qword ptr [rcx+r22*8-0x48c954c], 268435456 IID559 + __ shrq(Address(r21, r30, (Address::ScaleFactor)0, +0xe405b0b), 8); // shr qword ptr [r21+r30*1+0xe405b0b], 8 IID560 + __ subq(Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b), 4096); // sub qword ptr [r19+r29*8-0x7762044b], 4096 IID561 + __ xorq(Address(r30, r10, (Address::ScaleFactor)1, -0x19798323), 16); // xor qword ptr [r30+r10*2-0x19798323], 16 IID562 + __ orq(Address(rdx, r24, (Address::ScaleFactor)3, +0x18d9b316), 4096); // or qword ptr [rdx+r24*8+0x18d9b316], 4096 IID563 + __ movq(Address(rbx, -0x3058074d), 256); // mov qword ptr [rbx-0x3058074d], 256 IID564 + __ testq(Address(r28, r21, (Address::ScaleFactor)3, +0x65a0fdc4), -268435456); // test qword ptr [r28+r21*8+0x65a0fdc4], -268435456 IID565 + __ addq(r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c)); // add r23, qword ptr [r11+r18*1-0x1d1af10c] IID566 + __ andq(r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38)); // and r22, qword ptr [r18+r12*2+0x1a5f1c38] IID567 + __ cmpq(r23, Address(r30, r19, (Address::ScaleFactor)0, -0x3e912f7f)); // cmp r23, qword ptr [r30+r19*1-0x3e912f7f] IID568 + __ lzcntq(r29, Address(rcx, +0x12e3fbe4)); // lzcnt r29, qword ptr [rcx+0x12e3fbe4] IID569 + __ orq(r14, Address(r21, r21, (Address::ScaleFactor)2, +0xd73042)); // or r14, qword ptr [r21+r21*4+0xd73042] IID570 + __ adcq(r31, Address(r17, r31, (Address::ScaleFactor)2, +0xabde912)); // adc r31, qword ptr [r17+r31*4+0xabde912] IID571 + __ imulq(r20, Address(r13, r27, (Address::ScaleFactor)0, -0x58dbfc1f)); // imul r20, qword ptr [r13+r27*1-0x58dbfc1f] IID572 + __ popcntq(rbx, Address(r22, -0x72c66c23)); // popcnt rbx, qword ptr [r22-0x72c66c23] IID573 + __ sbbq(r26, Address(r9, +0x334aba09)); // sbb r26, qword ptr [r9+0x334aba09] IID574 + __ subq(r9, Address(r9, r30, (Address::ScaleFactor)3, -0x219a6102)); // sub r9, qword ptr [r9+r30*8-0x219a6102] IID575 + __ tzcntq(r25, Address(r20, -0x2131bab1)); // tzcnt r25, qword ptr [r20-0x2131bab1] IID576 + __ xorq(r16, Address(r28, r16, (Address::ScaleFactor)1, +0x48c483b9)); // xor r16, qword ptr [r28+r16*2+0x48c483b9] IID577 + __ movq(r30, Address(r9, r16, (Address::ScaleFactor)0, -0x88ce84f)); // mov r30, qword ptr [r9+r16*1-0x88ce84f] IID578 + __ leaq(r11, Address(r30, r29, (Address::ScaleFactor)2, +0x3eeb8fd0)); // lea r11, qword ptr [r30+r29*4+0x3eeb8fd0] IID579 + __ cvttsd2siq(r26, Address(r29, r10, (Address::ScaleFactor)3, +0x3ef4822e)); // cvttsd2si r26, qword ptr [r29+r10*8+0x3ef4822e] IID580 + __ xchgq(r29, Address(r19, r20, (Address::ScaleFactor)2, -0x3f0f3db9)); // xchg r29, qword ptr [r19+r20*4-0x3f0f3db9] IID581 + __ testq(r8, Address(r30, r20, (Address::ScaleFactor)0, +0x15b56a17)); // test r8, qword ptr [r30+r20*1+0x15b56a17] IID582 + __ addq(r26, 4096); // add r26, 4096 IID583 + __ andq(r20, 16); // and r20, 16 IID584 + __ adcq(r23, 1048576); // adc r23, 1048576 IID585 + __ cmpq(r12, 4096); // cmp r12, 4096 IID586 + __ rclq(rcx, 4); // rcl rcx, 4 IID587 + __ rcrq(r14, 1); // rcr r14, 1 IID588 + __ rolq(r23, 2); // rol r23, 2 IID589 + __ rorq(r12, 4); // ror r12, 4 IID590 + __ sarq(r10, 4); // sar r10, 4 IID591 + __ salq(r20, 4); // sal r20, 4 IID592 + __ sbbq(rcx, 1048576); // sbb rcx, 1048576 IID593 + __ shlq(r23, 16); // shl r23, 16 IID594 + __ shrq(r27, 2); // shr r27, 2 IID595 + __ subq(rcx, 65536); // sub rcx, 65536 IID596 + __ xorq(r9, 1048576); // xor r9, 1048576 IID597 + __ movq(r16, 65536); // mov r16, 65536 IID598 + __ mov64(r24, 4503599627370496); // mov r24, 4503599627370496 IID599 + __ btq(r18, 64); // bt r18, 64 IID600 + __ testq(r29, -4096); // test r29, -4096 IID601 + __ orq_imm32(r30, 67108864); // or r30, 67108864 IID602 + __ subq_imm32(r25, 268435456); // sub r25, 268435456 IID603 + __ cmovq(Assembler::Condition::overflow, r30, Address(r17, r31, (Address::ScaleFactor)2, +0x47ff92f0)); // cmovo r30, qword ptr [r17+r31*4+0x47ff92f0] IID604 + __ cmovq(Assembler::Condition::noOverflow, r9, Address(r24, r28, (Address::ScaleFactor)1, +0x384904c0)); // cmovno r9, qword ptr [r24+r28*2+0x384904c0] IID605 + __ cmovq(Assembler::Condition::below, r23, Address(r23, r24, (Address::ScaleFactor)3, -0x197f1266)); // cmovb r23, qword ptr [r23+r24*8-0x197f1266] IID606 + __ cmovq(Assembler::Condition::aboveEqual, r9, Address(r29, r30, (Address::ScaleFactor)0, +0x2b5d49c8)); // cmovae r9, qword ptr [r29+r30*1+0x2b5d49c8] IID607 + __ cmovq(Assembler::Condition::zero, r16, Address(rbx, r15, (Address::ScaleFactor)1, +0x22379381)); // cmovz r16, qword ptr [rbx+r15*2+0x22379381] IID608 + __ cmovq(Assembler::Condition::notZero, r8, Address(r11, +0x49d67a0)); // cmovnz r8, qword ptr [r11+0x49d67a0] IID609 + __ cmovq(Assembler::Condition::belowEqual, r28, Address(r16, r16, (Address::ScaleFactor)2, -0x5e941da9)); // cmovbe r28, qword ptr [r16+r16*4-0x5e941da9] IID610 + __ cmovq(Assembler::Condition::above, r19, Address(r18, r8, (Address::ScaleFactor)0, -0xa5e55ec)); // cmova r19, qword ptr [r18+r8*1-0xa5e55ec] IID611 + __ cmovq(Assembler::Condition::negative, r28, Address(r17, r28, (Address::ScaleFactor)1, -0x3264220c)); // cmovs r28, qword ptr [r17+r28*2-0x3264220c] IID612 + __ cmovq(Assembler::Condition::positive, r31, Address(r14, r31, (Address::ScaleFactor)1, +0x5001bc5a)); // cmovns r31, qword ptr [r14+r31*2+0x5001bc5a] IID613 + __ cmovq(Assembler::Condition::parity, rbx, Address(r18, r17, (Address::ScaleFactor)2, -0x286f2379)); // cmovp rbx, qword ptr [r18+r17*4-0x286f2379] IID614 + __ cmovq(Assembler::Condition::noParity, r17, Address(r20, -0x5549f838)); // cmovnp r17, qword ptr [r20-0x5549f838] IID615 + __ cmovq(Assembler::Condition::less, r30, Address(r9, r28, (Address::ScaleFactor)1, -0x25b00cf3)); // cmovl r30, qword ptr [r9+r28*2-0x25b00cf3] IID616 + __ cmovq(Assembler::Condition::greaterEqual, r19, Address(r9, -0x2aabf22c)); // cmovge r19, qword ptr [r9-0x2aabf22c] IID617 + __ cmovq(Assembler::Condition::lessEqual, rbx, Address(rcx, r12, (Address::ScaleFactor)1, -0x432d68cc)); // cmovle rbx, qword ptr [rcx+r12*2-0x432d68cc] IID618 + __ cmovq(Assembler::Condition::greater, rbx, Address(r15, r17, (Address::ScaleFactor)3, -0x2b97565e)); // cmovg rbx, qword ptr [r15+r17*8-0x2b97565e] IID619 + __ call(r24); // call r24 IID620 + __ divq(r9); // div r9 IID621 + __ idivq(r28); // idiv r28 IID622 + __ imulq(rdx); // imul rdx IID623 + __ mulq(r31); // mul r31 IID624 + __ negq(r12); // neg r12 IID625 + __ notq(r12); // not r12 IID626 + __ rolq(r24); // rol r24, cl IID627 + __ rorq(r28); // ror r28, cl IID628 + __ sarq(r11); // sar r11, cl IID629 + __ salq(r27); // sal r27, cl IID630 + __ shlq(r23); // shl r23, cl IID631 + __ shrq(r17); // shr r17, cl IID632 + __ incrementq(r16); // inc r16 IID633 + __ decrementq(r12); // dec r12 IID634 + __ pushp(r23); // pushp r23 IID635 + __ popp(r24); // popp r24 IID636 + __ call(Address(r18, r14, (Address::ScaleFactor)0, -0x66639d32)); // call qword ptr [r18+r14*1-0x66639d32] IID637 + __ mulq(Address(r24, -0x660a2421)); // mul qword ptr [r24-0x660a2421] IID638 + __ negq(Address(r14, r18, (Address::ScaleFactor)0, +0x40f3936e)); // neg qword ptr [r14+r18*1+0x40f3936e] IID639 + __ sarq(Address(r10, r13, (Address::ScaleFactor)0, +0x7d04cb72)); // sar qword ptr [r10+r13*1+0x7d04cb72], cl IID640 + __ salq(Address(r18, r11, (Address::ScaleFactor)3, -0x2176b4dc)); // sal qword ptr [r18+r11*8-0x2176b4dc], cl IID641 + __ shrq(Address(r13, rcx, (Address::ScaleFactor)1, +0x7996aa80)); // shr qword ptr [r13+rcx*2+0x7996aa80], cl IID642 + __ incrementq(Address(r14, +0x67c2d02a)); // inc qword ptr [r14+0x67c2d02a] IID643 + __ decrementq(Address(r22, r26, (Address::ScaleFactor)0, +0x224f62c0)); // dec qword ptr [r22+r26*1+0x224f62c0] IID644 + __ imulq(rdx, Address(r31, rbx, (Address::ScaleFactor)1, +0x2b00bb10), 16777216); // imul rdx, qword ptr [r31+rbx*2+0x2b00bb10], 16777216 IID645 + __ imulq(r21, r31, 4096); // imul r21, r31, 4096 IID646 + __ shldq(rbx, r19, 1); // shld rbx, r19, 1 IID647 + __ shrdq(r11, r23, 4); // shrd r11, r23, 4 IID648 + __ pop2(r16, r30); // {load}pop2 r30, r16 IID649 + __ pop2p(r17, rbx); // {load}pop2p rbx, r17 IID650 + __ push2(r20, r30); // {load}push2 r30, r20 IID651 + __ push2p(r8, r31); // {load}push2p r31, r8 IID652 + __ movzbq(r28, Address(r8, r14, (Address::ScaleFactor)0, +0x469ae67a)); // movzx r28, byte ptr [r8+r14*1+0x469ae67a] IID653 + __ movzwq(r14, Address(r8, r18, (Address::ScaleFactor)2, -0x48699e02)); // movzx r14, word ptr [r8+r18*4-0x48699e02] IID654 + __ movsbq(r21, Address(rbx, -0x64dae06b)); // movsx r21, byte ptr [rbx-0x64dae06b] IID655 + __ movswq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x60318819)); // movsx r19, word ptr [r31+rbx*4+0x60318819] IID656 + __ movzbq(r30, r13); // movzx r30, r13b IID657 + __ movzwq(r30, r18); // movzx r30, r18w IID658 + __ movsbq(r19, r15); // movsx r19, r15b IID659 + __ movswq(r20, r16); // movsx r20, r16w IID660 + __ cmpxchgq(r28, Address(r11, rbx, (Address::ScaleFactor)3, +0xfc3479d)); // cmpxchg qword ptr [r11+rbx*8+0xfc3479d], r28 IID661 + __ eidivq(r20, false); // {EVEX}idiv r20 IID662 + __ eidivq(r30, true); // {NF}idiv r30 IID663 + __ edivq(r22, false); // {EVEX}div r22 IID664 + __ edivq(r11, true); // {NF}div r11 IID665 + __ eimulq(rcx, false); // {EVEX}imul rcx IID666 + __ eimulq(r28, true); // {NF}imul r28 IID667 + __ emulq(r21, false); // {EVEX}mul r21 IID668 + __ emulq(r13, true); // {NF}mul r13 IID669 + __ emulq(Address(r26, r15, (Address::ScaleFactor)2, +0x70a1ce6e), false); // {EVEX}mul qword ptr [r26+r15*4+0x70a1ce6e] IID670 + __ emulq(Address(r24, r19, (Address::ScaleFactor)1, -0x1670855c), true); // {NF}mul qword ptr [r24+r19*2-0x1670855c] IID671 + __ eimulq(r10, r27, false); // {EVEX}imul r10, r27 IID672 + __ eimulq(r17, r17, false); // imul r17 IID673 + __ eimulq(rdx, r22, true); // {NF}imul rdx, r22 IID674 + __ eimulq(rbx, rbx, true); // {NF}imul rbx, rbx IID675 + __ elzcntq(r28, r15, false); // {EVEX}lzcnt r28, r15 IID676 + __ elzcntq(r15, r15, false); // {EVEX}lzcnt r15, r15 IID677 + __ elzcntq(rbx, r12, true); // {NF}lzcnt rbx, r12 IID678 + __ elzcntq(rbx, rbx, true); // {NF}lzcnt rbx, rbx IID679 + __ enegq(r26, r11, false); // {EVEX}neg r26, r11 IID680 + __ enegq(r17, r17, false); // neg r17 IID681 + __ enegq(rdx, r31, true); // {NF}neg rdx, r31 IID682 + __ enegq(r27, r27, true); // {NF}neg r27, r27 IID683 + __ enotq(r31, r15); // {EVEX}not r31, r15 IID684 + __ enotq(r21, r21); // not r21 IID685 + __ epopcntq(rbx, r24, false); // {EVEX}popcnt rbx, r24 IID686 + __ epopcntq(r28, r28, false); // {EVEX}popcnt r28, r28 IID687 + __ epopcntq(r23, r27, true); // {NF}popcnt r23, r27 IID688 + __ epopcntq(r13, r13, true); // {NF}popcnt r13, r13 IID689 + __ erolq(r25, r28, false); // {EVEX}rol r25, r28, cl IID690 + __ erolq(r31, r31, false); // rol r31, cl IID691 + __ erolq(r25, r23, true); // {NF}rol r25, r23, cl IID692 + __ erolq(rcx, rcx, true); // {NF}rol rcx, rcx, cl IID693 + __ erorq(r22, r14, false); // {EVEX}ror r22, r14, cl IID694 + __ erorq(r15, r15, false); // ror r15, cl IID695 + __ erorq(r11, r30, true); // {NF}ror r11, r30, cl IID696 + __ erorq(r24, r24, true); // {NF}ror r24, r24, cl IID697 + __ esalq(r10, r20, false); // {EVEX}sal r10, r20, cl IID698 + __ esalq(r19, r19, false); // sal r19, cl IID699 + __ esalq(r17, r25, true); // {NF}sal r17, r25, cl IID700 + __ esalq(r13, r13, true); // {NF}sal r13, r13, cl IID701 + __ esarq(r31, r30, false); // {EVEX}sar r31, r30, cl IID702 + __ esarq(r18, r18, false); // sar r18, cl IID703 + __ esarq(r25, r25, true); // {NF}sar r25, r25, cl IID704 + __ esarq(r28, r28, true); // {NF}sar r28, r28, cl IID705 + __ edecq(r22, r27, false); // {EVEX}dec r22, r27 IID706 + __ edecq(r12, r12, false); // dec r12 IID707 + __ edecq(r18, r11, true); // {NF}dec r18, r11 IID708 + __ edecq(r10, r10, true); // {NF}dec r10, r10 IID709 + __ eincq(r20, r24, false); // {EVEX}inc r20, r24 IID710 + __ eincq(r18, r18, false); // inc r18 IID711 + __ eincq(rbx, r11, true); // {NF}inc rbx, r11 IID712 + __ eincq(r26, r26, true); // {NF}inc r26, r26 IID713 + __ eshlq(r21, r8, false); // {EVEX}shl r21, r8, cl IID714 + __ eshlq(rbx, rbx, false); // shl rbx, cl IID715 + __ eshlq(r22, r21, true); // {NF}shl r22, r21, cl IID716 + __ eshlq(r27, r27, true); // {NF}shl r27, r27, cl IID717 + __ eshrq(r12, r16, false); // {EVEX}shr r12, r16, cl IID718 + __ eshrq(r8, r8, false); // shr r8, cl IID719 + __ eshrq(rdx, r9, true); // {NF}shr rdx, r9, cl IID720 + __ eshrq(r20, r20, true); // {NF}shr r20, r20, cl IID721 + __ etzcntq(r31, r21, false); // {EVEX}tzcnt r31, r21 IID722 + __ etzcntq(r20, r20, false); // {EVEX}tzcnt r20, r20 IID723 + __ etzcntq(rcx, r16, true); // {NF}tzcnt rcx, r16 IID724 + __ etzcntq(r14, r14, true); // {NF}tzcnt r14, r14 IID725 + __ eimulq(r27, Address(r25, r9, (Address::ScaleFactor)1, +0x445a2393), false); // {EVEX}imul r27, qword ptr [r25+r9*2+0x445a2393] IID726 + __ eimulq(r23, Address(rcx, r9, (Address::ScaleFactor)1, -0x1480ef0c), true); // {NF}imul r23, qword ptr [rcx+r9*2-0x1480ef0c] IID727 + __ elzcntq(r13, Address(r22, r17, (Address::ScaleFactor)1, -0x750c1996), false); // {EVEX}lzcnt r13, qword ptr [r22+r17*2-0x750c1996] IID728 + __ elzcntq(r13, Address(r31, -0x342b6259), true); // {NF}lzcnt r13, qword ptr [r31-0x342b6259] IID729 + __ enegq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), false); // {EVEX}neg r31, qword ptr [r24+r13*2-0x25b16a0e] IID730 + __ enegq(r13, Address(r11, r28, (Address::ScaleFactor)3, +0x5c0013ab), true); // {NF}neg r13, qword ptr [r11+r28*8+0x5c0013ab] IID731 + __ epopcntq(rdx, Address(r18, rcx, (Address::ScaleFactor)2, -0x6113eaaf), false); // {EVEX}popcnt rdx, qword ptr [r18+rcx*4-0x6113eaaf] IID732 + __ epopcntq(r9, Address(r10, -0x5ca7d588), true); // {NF}popcnt r9, qword ptr [r10-0x5ca7d588] IID733 + __ esalq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), false); // {EVEX}sal r17, qword ptr [r27+r30*1+0x1b4cda2c], cl IID734 + __ esalq(r25, Address(r12, rdx, (Address::ScaleFactor)1, +0x62823bce), true); // {NF}sal r25, qword ptr [r12+rdx*2+0x62823bce], cl IID735 + __ esarq(r9, Address(r10, r18, (Address::ScaleFactor)2, -0x264a7a48), false); // {EVEX}sar r9, qword ptr [r10+r18*4-0x264a7a48], cl IID736 + __ esarq(rbx, Address(r14, r27, (Address::ScaleFactor)0, +0x20291e00), true); // {NF}sar rbx, qword ptr [r14+r27*1+0x20291e00], cl IID737 + __ edecq(r12, Address(r15, r14, (Address::ScaleFactor)2, -0x20f7dabb), false); // {EVEX}dec r12, qword ptr [r15+r14*4-0x20f7dabb] IID738 + __ edecq(r9, Address(r10, r25, (Address::ScaleFactor)1, +0x21411d84), true); // {NF}dec r9, qword ptr [r10+r25*2+0x21411d84] IID739 + __ eincq(r20, Address(rbx, r25, (Address::ScaleFactor)3, +0x2f0329e), false); // {EVEX}inc r20, qword ptr [rbx+r25*8+0x2f0329e] IID740 + __ eincq(r10, Address(r12, r31, (Address::ScaleFactor)0, -0x37505c8c), true); // {NF}inc r10, qword ptr [r12+r31*1-0x37505c8c] IID741 + __ eshrq(r24, Address(r23, r14, (Address::ScaleFactor)3, -0x71e75ab0), false); // {EVEX}shr r24, qword ptr [r23+r14*8-0x71e75ab0], cl IID742 + __ eshrq(r25, Address(r19, r10, (Address::ScaleFactor)1, +0x507b0a88), true); // {NF}shr r25, qword ptr [r19+r10*2+0x507b0a88], cl IID743 + __ etzcntq(r31, Address(rbx, r16, (Address::ScaleFactor)0, +0x19d5192a), false); // {EVEX}tzcnt r31, qword ptr [rbx+r16*1+0x19d5192a] IID744 + __ etzcntq(r9, Address(r22, r28, (Address::ScaleFactor)2, +0x211007cd), true); // {NF}tzcnt r9, qword ptr [r22+r28*4+0x211007cd] IID745 + __ eaddq(r16, Address(r21, rbx, (Address::ScaleFactor)3, -0x823fa1e), r28, false); // {EVEX}add r16, qword ptr [r21+rbx*8-0x823fa1e], r28 IID746 + __ eaddq(r15, Address(rdx, r8, (Address::ScaleFactor)3, -0x34b9a058), r15, false); // add r15, qword ptr [rdx+r8*8-0x34b9a058] IID747 + __ eaddq(r24, Address(r14, r24, (Address::ScaleFactor)3, +0x6cdc59d2), r13, true); // {NF}add r24, qword ptr [r14+r24*8+0x6cdc59d2], r13 IID748 + __ eaddq(rbx, Address(r27, r14, (Address::ScaleFactor)3, +0x36c5e8de), rbx, true); // {NF}add rbx, qword ptr [r27+r14*8+0x36c5e8de], rbx IID749 + __ eandq(r21, Address(r27, r27, (Address::ScaleFactor)1, -0x2c023b13), r27, false); // {EVEX}and r21, qword ptr [r27+r27*2-0x2c023b13], r27 IID750 + __ eandq(r31, Address(r21, r15, (Address::ScaleFactor)2, +0x6ef2c74a), r31, false); // and r31, qword ptr [r21+r15*4+0x6ef2c74a] IID751 + __ eandq(r13, Address(r31, r25, (Address::ScaleFactor)1, +0x734fe9ab), r27, true); // {NF}and r13, qword ptr [r31+r25*2+0x734fe9ab], r27 IID752 + __ eandq(r15, Address(r14, r29, (Address::ScaleFactor)3, -0x6e68556), r15, true); // {NF}and r15, qword ptr [r14+r29*8-0x6e68556], r15 IID753 + __ eorq(r12, Address(r30, r15, (Address::ScaleFactor)3, +0x3ba33f9e), r28, false); // {EVEX}or r12, qword ptr [r30+r15*8+0x3ba33f9e], r28 IID754 + __ eorq(r16, Address(r12, r9, (Address::ScaleFactor)0, -0x28e03b33), r16, false); // or r16, qword ptr [r12+r9*1-0x28e03b33] IID755 + __ eorq(r8, Address(r8, r25, (Address::ScaleFactor)3, -0x1e42bd95), r27, true); // {NF}or r8, qword ptr [r8+r25*8-0x1e42bd95], r27 IID756 + __ eorq(rcx, Address(r27, rbx, (Address::ScaleFactor)2, +0x7be4bcad), rcx, true); // {NF}or rcx, qword ptr [r27+rbx*4+0x7be4bcad], rcx IID757 + __ esubq(r24, Address(r23, r22, (Address::ScaleFactor)2, +0x6f8827d7), rdx, false); // {EVEX}sub r24, qword ptr [r23+r22*4+0x6f8827d7], rdx IID758 + __ esubq(r21, Address(r10, -0x635b8c8), r21, false); // {EVEX}sub r21, qword ptr [r10-0x635b8c8], r21 IID759 + __ esubq(r23, Address(r27, r26, (Address::ScaleFactor)3, +0x922bcc0), rbx, true); // {NF}sub r23, qword ptr [r27+r26*8+0x922bcc0], rbx IID760 + __ esubq(r25, Address(r23, r15, (Address::ScaleFactor)0, -0x38f494ac), r25, true); // {NF}sub r25, qword ptr [r23+r15*1-0x38f494ac], r25 IID761 + __ exorq(r11, Address(r12, r19, (Address::ScaleFactor)2, -0x5b71ec17), rcx, false); // {EVEX}xor r11, qword ptr [r12+r19*4-0x5b71ec17], rcx IID762 + __ exorq(r28, Address(r19, r18, (Address::ScaleFactor)0, +0x716b9b7e), r28, false); // xor r28, qword ptr [r19+r18*1+0x716b9b7e] IID763 + __ exorq(r21, Address(rcx, r29, (Address::ScaleFactor)0, -0x5af0441e), r16, true); // {NF}xor r21, qword ptr [rcx+r29*1-0x5af0441e], r16 IID764 + __ exorq(r12, Address(r20, r26, (Address::ScaleFactor)0, +0xe0b7fb1), r12, true); // {NF}xor r12, qword ptr [r20+r26*1+0xe0b7fb1], r12 IID765 + __ eaddq(r30, Address(rcx, +0x2d3b7b4f), 1048576, false); // {EVEX}add r30, qword ptr [rcx+0x2d3b7b4f], 1048576 IID766 + __ eaddq(r14, Address(r21, r15, (Address::ScaleFactor)2, -0x1222aee8), 4096, true); // {NF}add r14, qword ptr [r21+r15*4-0x1222aee8], 4096 IID767 + __ eandq(r23, Address(r20, r31, (Address::ScaleFactor)0, -0x96e4d6a), 16, false); // {EVEX}and r23, qword ptr [r20+r31*1-0x96e4d6a], 16 IID768 + __ eandq(r10, Address(rdx, rdx, (Address::ScaleFactor)3, +0x3875f17c), 1, true); // {NF}and r10, qword ptr [rdx+rdx*8+0x3875f17c], 1 IID769 + __ eimulq(r17, Address(rcx, r25, (Address::ScaleFactor)2, +0x32c71076), 4096, false); // {EVEX}imul r17, qword ptr [rcx+r25*4+0x32c71076], 4096 IID770 + __ eimulq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x7bada60d), 1048576, true); // {NF}imul r19, qword ptr [r31+rbx*4+0x7bada60d], 1048576 IID771 + __ eorq(r25, Address(r18, r23, (Address::ScaleFactor)1, +0x48147444), 16777216, false); // {EVEX}or r25, qword ptr [r18+r23*2+0x48147444], 16777216 IID772 + __ eorq(r29, Address(r26, r27, (Address::ScaleFactor)1, -0x4b113958), 1048576, true); // {NF}or r29, qword ptr [r26+r27*2-0x4b113958], 1048576 IID773 + __ esalq(r31, Address(r18, -0x46103c74), 2, false); // {EVEX}sal r31, qword ptr [r18-0x46103c74], 2 IID774 + __ esalq(r25, Address(r10, r15, (Address::ScaleFactor)0, +0x48925da4), 16, true); // {NF}sal r25, qword ptr [r10+r15*1+0x48925da4], 16 IID775 + __ esarq(r26, Address(r18, -0x5ea1c542), 8, false); // {EVEX}sar r26, qword ptr [r18-0x5ea1c542], 8 IID776 + __ esarq(r12, Address(r10, r22, (Address::ScaleFactor)2, +0x5d958264), 8, true); // {NF}sar r12, qword ptr [r10+r22*4+0x5d958264], 8 IID777 + __ eshrq(rdx, Address(r17, r20, (Address::ScaleFactor)2, +0x295add23), 16, false); // {EVEX}shr rdx, qword ptr [r17+r20*4+0x295add23], 16 IID778 + __ eshrq(rbx, Address(r22, r28, (Address::ScaleFactor)1, +0x782929cb), 2, true); // {NF}shr rbx, qword ptr [r22+r28*2+0x782929cb], 2 IID779 + __ esubq(r19, Address(r23, -0x49811d72), 1, false); // {EVEX}sub r19, qword ptr [r23-0x49811d72], 1 IID780 + __ esubq(r8, Address(r19, r14, (Address::ScaleFactor)2, -0x1b2bae9a), 1048576, true); // {NF}sub r8, qword ptr [r19+r14*4-0x1b2bae9a], 1048576 IID781 + __ exorq(r19, Address(rcx, r10, (Address::ScaleFactor)0, +0x45a66ee9), 1048576, false); // {EVEX}xor r19, qword ptr [rcx+r10*1+0x45a66ee9], 1048576 IID782 + __ exorq(r28, Address(r9, r29, (Address::ScaleFactor)0, -0x28a19314), 16, true); // {NF}xor r28, qword ptr [r9+r29*1-0x28a19314], 16 IID783 + __ eaddq(r8, rcx, 16777216, false); // {EVEX}add r8, rcx, 16777216 IID784 + __ eaddq(rax, r14, 16777216, false); // {EVEX}add rax, r14, 16777216 IID785 + __ eaddq(r16, r16, 256, false); // add r16, 256 IID786 + __ eaddq(r24, r9, 4096, true); // {NF}add r24, r9, 4096 IID787 + __ eaddq(rax, r18, 4096, true); // {NF}add rax, r18, 4096 IID788 + __ eaddq(r8, r8, 1, true); // {NF}add r8, r8, 1 IID789 + __ eandq(r15, r22, 1048576, false); // {EVEX}and r15, r22, 1048576 IID790 + __ eandq(rax, r26, 1048576, false); // {EVEX}and rax, r26, 1048576 IID791 + __ eandq(rdx, rdx, 4096, false); // and rdx, 4096 IID792 + __ eandq(rdx, r22, 268435456, true); // {NF}and rdx, r22, 268435456 IID793 + __ eandq(rax, r29, 268435456, true); // {NF}and rax, r29, 268435456 IID794 + __ eandq(r23, r23, 16777216, true); // {NF}and r23, r23, 16777216 IID795 + __ eimulq(r9, r13, 1048576, false); // {EVEX}imul r9, r13, 1048576 IID796 + __ eimulq(rax, r18, 1048576, false); // {EVEX}imul rax, r18, 1048576 IID797 + __ eimulq(r16, r16, 1048576, false); // {EVEX}imul r16, r16, 1048576 IID798 + __ eimulq(r17, r23, 1, true); // {NF}imul r17, r23, 1 IID799 + __ eimulq(rax, r12, 1, true); // {NF}imul rax, r12, 1 IID800 + __ eimulq(r10, r10, 268435456, true); // {NF}imul r10, r10, 268435456 IID801 + __ eorq(rdx, r19, 256, false); // {EVEX}or rdx, r19, 256 IID802 + __ eorq(rax, r14, 256, false); // {EVEX}or rax, r14, 256 IID803 + __ eorq(r13, r13, 1, false); // or r13, 1 IID804 + __ eorq(r25, r29, 256, true); // {NF}or r25, r29, 256 IID805 + __ eorq(rax, rdx, 256, true); // {NF}or rax, rdx, 256 IID806 + __ eorq(r16, r16, 16, true); // {NF}or r16, r16, 16 IID807 + __ erclq(r13, r19, 4); // {EVEX}rcl r13, r19, 4 IID808 + __ erclq(rax, r12, 4); // {EVEX}rcl rax, r12, 4 IID809 + __ erclq(r9, r9, 4); // rcl r9, 4 IID810 + __ erolq(r13, r16, 1, false); // {EVEX}rol r13, r16, 1 IID811 + __ erolq(rax, r31, 1, false); // {EVEX}rol rax, r31, 1 IID812 + __ erolq(r30, r30, 8, false); // rol r30, 8 IID813 + __ erolq(r30, r20, 8, true); // {NF}rol r30, r20, 8 IID814 + __ erolq(rax, r31, 8, true); // {NF}rol rax, r31, 8 IID815 + __ erolq(r31, r31, 4, true); // {NF}rol r31, r31, 4 IID816 + __ erorq(r22, r10, 4, false); // {EVEX}ror r22, r10, 4 IID817 + __ erorq(rax, r13, 4, false); // {EVEX}ror rax, r13, 4 IID818 + __ erorq(r24, r24, 16, false); // ror r24, 16 IID819 + __ erorq(r29, r22, 16, true); // {NF}ror r29, r22, 16 IID820 + __ erorq(rax, r20, 16, true); // {NF}ror rax, r20, 16 IID821 + __ erorq(r27, r27, 4, true); // {NF}ror r27, r27, 4 IID822 + __ esalq(r31, r19, 2, false); // {EVEX}sal r31, r19, 2 IID823 + __ esalq(rax, r20, 2, false); // {EVEX}sal rax, r20, 2 IID824 + __ esalq(r11, r11, 8, false); // sal r11, 8 IID825 + __ esalq(rdx, r15, 1, true); // {NF}sal rdx, r15, 1 IID826 + __ esalq(rax, r10, 1, true); // {NF}sal rax, r10, 1 IID827 + __ esalq(r29, r29, 4, true); // {NF}sal r29, r29, 4 IID828 + __ esarq(r20, r16, 1, false); // {EVEX}sar r20, r16, 1 IID829 + __ esarq(rax, r21, 1, false); // {EVEX}sar rax, r21, 1 IID830 + __ esarq(r28, r28, 8, false); // sar r28, 8 IID831 + __ esarq(r30, rcx, 4, true); // {NF}sar r30, rcx, 4 IID832 + __ esarq(rax, r15, 4, true); // {NF}sar rax, r15, 4 IID833 + __ esarq(rcx, rcx, 4, true); // {NF}sar rcx, rcx, 4 IID834 + __ eshlq(rdx, r26, 4, false); // {EVEX}shl rdx, r26, 4 IID835 + __ eshlq(rax, r26, 4, false); // {EVEX}shl rax, r26, 4 IID836 + __ eshlq(r8, r8, 4, false); // shl r8, 4 IID837 + __ eshlq(rcx, rcx, 1, true); // {NF}shl rcx, rcx, 1 IID838 + __ eshlq(rax, rcx, 1, true); // {NF}shl rax, rcx, 1 IID839 + __ eshlq(r13, r13, 2, true); // {NF}shl r13, r13, 2 IID840 + __ eshrq(r14, r27, 2, false); // {EVEX}shr r14, r27, 2 IID841 + __ eshrq(rax, r11, 2, false); // {EVEX}shr rax, r11, 2 IID842 + __ eshrq(r9, r9, 16, false); // shr r9, 16 IID843 + __ eshrq(rdx, r31, 2, true); // {NF}shr rdx, r31, 2 IID844 + __ eshrq(rax, r14, 2, true); // {NF}shr rax, r14, 2 IID845 + __ eshrq(r12, r12, 8, true); // {NF}shr r12, r12, 8 IID846 + __ esubq(r10, r28, 1, false); // {EVEX}sub r10, r28, 1 IID847 + __ esubq(rax, r8, 1, false); // {EVEX}sub rax, r8, 1 IID848 + __ esubq(rcx, rcx, 16777216, false); // sub rcx, 16777216 IID849 + __ esubq(rdx, rbx, 16777216, true); // {NF}sub rdx, rbx, 16777216 IID850 + __ esubq(rax, r18, 16777216, true); // {NF}sub rax, r18, 16777216 IID851 + __ esubq(r27, r27, 65536, true); // {NF}sub r27, r27, 65536 IID852 + __ exorq(r30, rcx, 4096, false); // {EVEX}xor r30, rcx, 4096 IID853 + __ exorq(rax, r21, 4096, false); // {EVEX}xor rax, r21, 4096 IID854 + __ exorq(rcx, rcx, 16777216, false); // xor rcx, 16777216 IID855 + __ exorq(r21, r12, 1, true); // {NF}xor r21, r12, 1 IID856 + __ exorq(rax, rdx, 1, true); // {NF}xor rax, rdx, 1 IID857 + __ exorq(rbx, rbx, 16777216, true); // {NF}xor rbx, rbx, 16777216 IID858 + __ eorq_imm32(r11, rdx, 65536, false); // {EVEX}or r11, rdx, 65536 IID859 + __ eorq_imm32(rax, r14, 65536, false); // {EVEX}or rax, r14, 65536 IID860 + __ eorq_imm32(r14, r14, 262144, false); // or r14, 262144 IID861 + __ eorq_imm32(r25, r29, 262144, false); // {EVEX}or r25, r29, 262144 IID862 + __ eorq_imm32(rax, r21, 262144, false); // {EVEX}or rax, r21, 262144 IID863 + __ eorq_imm32(r11, r11, 16777216, false); // or r11, 16777216 IID864 + __ esubq_imm32(r29, r19, 67108864, false); // {EVEX}sub r29, r19, 67108864 IID865 + __ esubq_imm32(rax, r11, 67108864, false); // {EVEX}sub rax, r11, 67108864 IID866 + __ esubq_imm32(r18, r18, 67108864, false); // sub r18, 67108864 IID867 + __ esubq_imm32(r28, r23, 4194304, true); // {NF}sub r28, r23, 4194304 IID868 + __ esubq_imm32(rax, r21, 4194304, true); // {NF}sub rax, r21, 4194304 IID869 + __ esubq_imm32(r16, r16, 16777216, true); // {NF}sub r16, r16, 16777216 IID870 + __ eaddq(r8, r25, Address(r26, r8, (Address::ScaleFactor)1, +0x10633def), false); // {EVEX}add r8, r25, qword ptr [r26+r8*2+0x10633def] IID871 + __ eaddq(r13, r13, Address(r18, r16, (Address::ScaleFactor)1, -0x74204508), false); // add r13, qword ptr [r18+r16*2-0x74204508] IID872 + __ eaddq(r17, r26, Address(r12, +0x23a80abf), true); // {NF}add r17, r26, qword ptr [r12+0x23a80abf] IID873 + __ eaddq(r9, r9, Address(r29, r19, (Address::ScaleFactor)0, -0x29e9e52), true); // {NF}add r9, r9, qword ptr [r29+r19*1-0x29e9e52] IID874 + __ eandq(r9, r28, Address(rcx, r25, (Address::ScaleFactor)2, +0x4261ffaa), false); // {EVEX}and r9, r28, qword ptr [rcx+r25*4+0x4261ffaa] IID875 + __ eandq(r27, r27, Address(rdx, r28, (Address::ScaleFactor)0, -0x26bdc9c1), false); // and r27, qword ptr [rdx+r28*1-0x26bdc9c1] IID876 + __ eandq(r14, r11, Address(r16, +0x63ba0ddf), true); // {NF}and r14, r11, qword ptr [r16+0x63ba0ddf] IID877 + __ eandq(r8, r8, Address(r22, r25, (Address::ScaleFactor)1, -0x43b6ab44), true); // {NF}and r8, r8, qword ptr [r22+r25*2-0x43b6ab44] IID878 + __ eorq(r19, rcx, Address(r27, rcx, (Address::ScaleFactor)2, -0x7f687fc6), false); // {EVEX}or r19, rcx, qword ptr [r27+rcx*4-0x7f687fc6] IID879 + __ eorq(r19, r19, Address(rbx, r26, (Address::ScaleFactor)1, -0x486db7ea), false); // or r19, qword ptr [rbx+r26*2-0x486db7ea] IID880 + __ eorq(r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884), true); // {NF}or r30, r10, qword ptr [r14+r18*8+0x14884884] IID881 + __ eorq(r27, r27, Address(r29, +0x20337180), true); // {NF}or r27, r27, qword ptr [r29+0x20337180] IID882 + __ eimulq(rcx, r21, Address(r21, rbx, (Address::ScaleFactor)0, -0x3303888e), false); // {EVEX}imul rcx, r21, qword ptr [r21+rbx*1-0x3303888e] IID883 + __ eimulq(rdx, rdx, Address(r28, r9, (Address::ScaleFactor)3, -0x7ad8f741), false); // imul rdx, qword ptr [r28+r9*8-0x7ad8f741] IID884 + __ eimulq(r8, r29, Address(r17, r12, (Address::ScaleFactor)0, +0x6e85396a), true); // {NF}imul r8, r29, qword ptr [r17+r12*1+0x6e85396a] IID885 + __ eimulq(r16, r16, Address(r19, r10, (Address::ScaleFactor)3, -0x49599300), true); // {NF}imul r16, r16, qword ptr [r19+r10*8-0x49599300] IID886 + __ esubq(r20, r17, Address(r13, r22, (Address::ScaleFactor)0, +0x1d219a4f), false); // {EVEX}sub r20, r17, qword ptr [r13+r22*1+0x1d219a4f] IID887 + __ esubq(r25, r25, Address(r21, r21, (Address::ScaleFactor)3, -0x6868a8c7), false); // sub r25, qword ptr [r21+r21*8-0x6868a8c7] IID888 + __ esubq(r20, r24, Address(rbx, r20, (Address::ScaleFactor)2, +0x32c59da6), true); // {NF}sub r20, r24, qword ptr [rbx+r20*4+0x32c59da6] IID889 + __ esubq(r8, r8, Address(r12, r17, (Address::ScaleFactor)0, -0x26be2dcf), true); // {NF}sub r8, r8, qword ptr [r12+r17*1-0x26be2dcf] IID890 + __ exorq(rdx, r19, Address(r9, +0x7d903b91), false); // {EVEX}xor rdx, r19, qword ptr [r9+0x7d903b91] IID891 + __ exorq(r28, r28, Address(r29, r27, (Address::ScaleFactor)2, +0x53091f6f), false); // xor r28, qword ptr [r29+r27*4+0x53091f6f] IID892 + __ exorq(r17, r16, Address(r27, +0x7c6e9207), true); // {NF}xor r17, r16, qword ptr [r27+0x7c6e9207] IID893 + __ exorq(r15, r15, Address(r13, r24, (Address::ScaleFactor)3, -0x75c87960), true); // {NF}xor r15, r15, qword ptr [r13+r24*8-0x75c87960] IID894 + __ eaddq(r16, rbx, r18, false); // {load}{EVEX}add r16, rbx, r18 IID895 + __ eaddq(r24, r24, r18, false); // {load}add r24, r18 IID896 + __ eaddq(r9, r15, r9, false); // {load}add r9, r15 IID897 + __ eaddq(r19, r26, r13, true); // {load}{NF}add r19, r26, r13 IID898 + __ eaddq(r28, r28, r22, true); // {load}{NF}add r28, r28, r22 IID899 + __ eaddq(r22, r11, r22, true); // {load}{NF}add r22, r11, r22 IID900 + __ eadcxq(rcx, r12, r13); // {load}{EVEX}adcx rcx, r12, r13 IID901 + __ eadcxq(r30, r30, r12); // {load}adcx r30, r12 IID902 + __ eadoxq(r28, r14, r18); // {load}{EVEX}adox r28, r14, r18 IID903 + __ eadoxq(r30, r30, r19); // {load}adox r30, r19 IID904 + __ eandq(r20, r14, r14, false); // {load}{EVEX}and r20, r14, r14 IID905 + __ eandq(r17, r17, r23, false); // {load}and r17, r23 IID906 + __ eandq(r17, r14, r17, false); // {load}and r17, r14 IID907 + __ eandq(r19, r20, r15, true); // {load}{NF}and r19, r20, r15 IID908 + __ eandq(rbx, rbx, r13, true); // {load}{NF}and rbx, rbx, r13 IID909 + __ eandq(r22, r30, r22, true); // {load}{NF}and r22, r30, r22 IID910 + __ eimulq(r17, r24, rcx, false); // {load}{EVEX}imul r17, r24, rcx IID911 + __ eimulq(r21, r21, r8, false); // {load}imul r21, r8 IID912 + __ eimulq(r29, r21, r29, false); // {load}imul r29, r21 IID913 + __ eimulq(r27, r13, r23, true); // {load}{NF}imul r27, r13, r23 IID914 + __ eimulq(r26, r26, r8, true); // {load}{NF}imul r26, r26, r8 IID915 + __ eimulq(r22, r13, r22, true); // {load}{NF}imul r22, r13, r22 IID916 + __ eorq(r11, rdx, r29, false); // {load}{EVEX}or r11, rdx, r29 IID917 + __ eorq(rdx, rdx, r31, false); // {load}or rdx, r31 IID918 + __ eorq(r10, r29, r10, false); // {load}or r10, r29 IID919 + __ eorq(r27, r28, rcx, true); // {load}{NF}or r27, r28, rcx IID920 + __ eorq(r25, r25, r9, true); // {load}{NF}or r25, r25, r9 IID921 + __ eorq(rcx, r8, rcx, true); // {load}{NF}or rcx, r8, rcx IID922 + __ esubq(rcx, r10, r16, false); // {load}{EVEX}sub rcx, r10, r16 IID923 + __ esubq(r17, r17, rcx, false); // {load}sub r17, rcx IID924 + __ esubq(r13, r21, r24, true); // {load}{NF}sub r13, r21, r24 IID925 + __ esubq(r31, r31, r28, true); // {load}{NF}sub r31, r31, r28 IID926 + __ exorq(r23, r28, r23, false); // {load}xor r23, r28 IID927 + __ exorq(r10, r10, r11, false); // {load}xor r10, r11 IID928 + __ exorq(r19, r18, r19, false); // {load}xor r19, r18 IID929 + __ exorq(r31, r9, rdx, true); // {load}{NF}xor r31, r9, rdx IID930 + __ exorq(r13, r13, r9, true); // {load}{NF}xor r13, r13, r9 IID931 + __ exorq(rcx, r10, rcx, true); // {load}{NF}xor rcx, r10, rcx IID932 + __ eshldq(r12, r24, r22, 8, false); // {EVEX}shld r12, r24, r22, 8 IID933 + __ eshldq(r25, r25, r25, 8, false); // shld r25, r25, 8 IID934 + __ eshldq(r21, r20, r15, 8, true); // {NF}shld r21, r20, r15, 8 IID935 + __ eshldq(r21, r21, r10, 8, true); // {NF}shld r21, r21, r10, 8 IID936 + __ eshrdq(r18, r18, r8, 2, false); // shrd r18, r8, 2 IID937 + __ eshrdq(r26, r26, r29, 8, false); // shrd r26, r29, 8 IID938 + __ eshrdq(r29, r26, r19, 2, true); // {NF}shrd r29, r26, r19, 2 IID939 + __ eshrdq(r12, r12, rcx, 4, true); // {NF}shrd r12, r12, rcx, 4 IID940 + __ ecmovq (Assembler::Condition::overflow, r21, r22, r23); // cmovo r21, r22, r23 IID941 + __ ecmovq (Assembler::Condition::overflow, r9, r9, r13); // cmovo r9, r13 IID942 + __ ecmovq (Assembler::Condition::noOverflow, rcx, r23, r24); // cmovno rcx, r23, r24 IID943 + __ ecmovq (Assembler::Condition::noOverflow, r28, r28, rdx); // cmovno r28, rdx IID944 + __ ecmovq (Assembler::Condition::below, r14, r31, r23); // cmovb r14, r31, r23 IID945 + __ ecmovq (Assembler::Condition::below, r30, r30, r23); // cmovb r30, r23 IID946 + __ ecmovq (Assembler::Condition::aboveEqual, r10, r29, r22); // cmovae r10, r29, r22 IID947 + __ ecmovq (Assembler::Condition::aboveEqual, rbx, rbx, r26); // cmovae rbx, r26 IID948 + __ ecmovq (Assembler::Condition::zero, r23, r21, r13); // cmovz r23, r21, r13 IID949 + __ ecmovq (Assembler::Condition::zero, r10, r10, r20); // cmovz r10, r20 IID950 + __ ecmovq (Assembler::Condition::notZero, rbx, r9, r29); // cmovnz rbx, r9, r29 IID951 + __ ecmovq (Assembler::Condition::notZero, r16, r16, r30); // cmovnz r16, r30 IID952 + __ ecmovq (Assembler::Condition::belowEqual, r13, rcx, r29); // cmovbe r13, rcx, r29 IID953 + __ ecmovq (Assembler::Condition::belowEqual, r31, r31, r13); // cmovbe r31, r13 IID954 + __ ecmovq (Assembler::Condition::above, r27, r9, r30); // cmova r27, r9, r30 IID955 + __ ecmovq (Assembler::Condition::above, r26, r26, r20); // cmova r26, r20 IID956 + __ ecmovq (Assembler::Condition::negative, r8, r12, r22); // cmovs r8, r12, r22 IID957 + __ ecmovq (Assembler::Condition::negative, r31, r31, r17); // cmovs r31, r17 IID958 + __ ecmovq (Assembler::Condition::positive, r29, rcx, r25); // cmovns r29, rcx, r25 IID959 + __ ecmovq (Assembler::Condition::positive, r22, r22, r14); // cmovns r22, r14 IID960 + __ ecmovq (Assembler::Condition::parity, rcx, r27, r9); // cmovp rcx, r27, r9 IID961 + __ ecmovq (Assembler::Condition::parity, r22, r22, r11); // cmovp r22, r11 IID962 + __ ecmovq (Assembler::Condition::noParity, r14, r19, r24); // cmovnp r14, r19, r24 IID963 + __ ecmovq (Assembler::Condition::noParity, r24, r24, r17); // cmovnp r24, r17 IID964 + __ ecmovq (Assembler::Condition::less, r17, r19, r30); // cmovl r17, r19, r30 IID965 + __ ecmovq (Assembler::Condition::less, r19, r19, r14); // cmovl r19, r14 IID966 + __ ecmovq (Assembler::Condition::greaterEqual, r25, r11, r29); // cmovge r25, r11, r29 IID967 + __ ecmovq (Assembler::Condition::greaterEqual, r12, r12, r26); // cmovge r12, r26 IID968 + __ ecmovq (Assembler::Condition::lessEqual, r11, rbx, r10); // cmovle r11, rbx, r10 IID969 + __ ecmovq (Assembler::Condition::lessEqual, rdx, rdx, r22); // cmovle rdx, r22 IID970 + __ ecmovq (Assembler::Condition::greater, r14, r15, r23); // cmovg r14, r15, r23 IID971 + __ ecmovq (Assembler::Condition::greater, r8, r8, r24); // cmovg r8, r24 IID972 + __ ecmovq (Assembler::Condition::overflow, rbx, r31, Address(r10, r8, (Address::ScaleFactor)3, -0x313f60e0)); // cmovo rbx, r31, qword ptr [r10+r8*8-0x313f60e0] IID973 + __ ecmovq (Assembler::Condition::overflow, r23, r23, Address(rcx, r24, (Address::ScaleFactor)2, +0x17f41d9c)); // cmovo r23, qword ptr [rcx+r24*4+0x17f41d9c] IID974 + __ ecmovq (Assembler::Condition::noOverflow, r31, r11, Address(r16, +0x2c018942)); // cmovno r31, r11, qword ptr [r16+0x2c018942] IID975 + __ ecmovq (Assembler::Condition::noOverflow, r11, r11, Address(r16, r20, (Address::ScaleFactor)3, +0x674b6a55)); // cmovno r11, qword ptr [r16+r20*8+0x674b6a55] IID976 + __ ecmovq (Assembler::Condition::below, r9, r13, Address(r9, rcx, (Address::ScaleFactor)0, +0x394a11df)); // cmovb r9, r13, qword ptr [r9+rcx*1+0x394a11df] IID977 + __ ecmovq (Assembler::Condition::below, r30, r30, Address(rdx, r22, (Address::ScaleFactor)1, -0x6c362b88)); // cmovb r30, qword ptr [rdx+r22*2-0x6c362b88] IID978 + __ ecmovq (Assembler::Condition::aboveEqual, r13, rcx, Address(r24, rcx, (Address::ScaleFactor)3, +0x46500b66)); // cmovae r13, rcx, qword ptr [r24+rcx*8+0x46500b66] IID979 + __ ecmovq (Assembler::Condition::aboveEqual, r24, r24, Address(r18, r25, (Address::ScaleFactor)1, +0x53283b7c)); // cmovae r24, qword ptr [r18+r25*2+0x53283b7c] IID980 + __ ecmovq (Assembler::Condition::zero, r23, r25, Address(r15, r9, (Address::ScaleFactor)0, -0x5f03031e)); // cmovz r23, r25, qword ptr [r15+r9*1-0x5f03031e] IID981 + __ ecmovq (Assembler::Condition::zero, r25, r25, Address(r28, r16, (Address::ScaleFactor)1, -0x53cef514)); // cmovz r25, qword ptr [r28+r16*2-0x53cef514] IID982 + __ ecmovq (Assembler::Condition::notZero, rbx, r25, Address(r24, r25, (Address::ScaleFactor)2, -0x66caac87)); // cmovnz rbx, r25, qword ptr [r24+r25*4-0x66caac87] IID983 + __ ecmovq (Assembler::Condition::notZero, r16, r16, Address(r27, r30, (Address::ScaleFactor)3, +0x797f455d)); // cmovnz r16, qword ptr [r27+r30*8+0x797f455d] IID984 + __ ecmovq (Assembler::Condition::belowEqual, r25, r30, Address(r18, r18, (Address::ScaleFactor)1, +0x1c9daacd)); // cmovbe r25, r30, qword ptr [r18+r18*2+0x1c9daacd] IID985 + __ ecmovq (Assembler::Condition::belowEqual, r22, r22, Address(rcx, r25, (Address::ScaleFactor)1, -0x3dcbfaa9)); // cmovbe r22, qword ptr [rcx+r25*2-0x3dcbfaa9] IID986 + __ ecmovq (Assembler::Condition::above, r24, r26, Address(r25, +0x747060b5)); // cmova r24, r26, qword ptr [r25+0x747060b5] IID987 + __ ecmovq (Assembler::Condition::above, r8, r8, Address(r24, r20, (Address::ScaleFactor)3, +0x47d285f6)); // cmova r8, qword ptr [r24+r20*8+0x47d285f6] IID988 + __ ecmovq (Assembler::Condition::negative, r12, r16, Address(r13, r10, (Address::ScaleFactor)2, +0x34e5b214)); // cmovs r12, r16, qword ptr [r13+r10*4+0x34e5b214] IID989 + __ ecmovq (Assembler::Condition::negative, rdx, rdx, Address(r15, r19, (Address::ScaleFactor)0, -0x405138b1)); // cmovs rdx, qword ptr [r15+r19*1-0x405138b1] IID990 + __ ecmovq (Assembler::Condition::positive, r18, r21, Address(rbx, r13, (Address::ScaleFactor)2, +0x51b19197)); // cmovns r18, r21, qword ptr [rbx+r13*4+0x51b19197] IID991 + __ ecmovq (Assembler::Condition::positive, r24, r24, Address(r11, r31, (Address::ScaleFactor)3, +0x3e01520a)); // cmovns r24, qword ptr [r11+r31*8+0x3e01520a] IID992 + __ ecmovq (Assembler::Condition::parity, r29, r26, Address(r10, r25, (Address::ScaleFactor)3, -0x5f7c3872)); // cmovp r29, r26, qword ptr [r10+r25*8-0x5f7c3872] IID993 + __ ecmovq (Assembler::Condition::parity, r11, r11, Address(r22, r10, (Address::ScaleFactor)3, -0x68731453)); // cmovp r11, qword ptr [r22+r10*8-0x68731453] IID994 + __ ecmovq (Assembler::Condition::noParity, r20, r15, Address(r9, r25, (Address::ScaleFactor)0, +0x4a37edaa)); // cmovnp r20, r15, qword ptr [r9+r25*1+0x4a37edaa] IID995 + __ ecmovq (Assembler::Condition::noParity, r31, r31, Address(r9, r20, (Address::ScaleFactor)0, +0x4f999f86)); // cmovnp r31, qword ptr [r9+r20*1+0x4f999f86] IID996 + __ ecmovq (Assembler::Condition::less, r18, r23, Address(r9, r27, (Address::ScaleFactor)0, -0x3410441d)); // cmovl r18, r23, qword ptr [r9+r27*1-0x3410441d] IID997 + __ ecmovq (Assembler::Condition::less, r16, r16, Address(r24, r10, (Address::ScaleFactor)3, +0x52ed66ee)); // cmovl r16, qword ptr [r24+r10*8+0x52ed66ee] IID998 + __ ecmovq (Assembler::Condition::greaterEqual, r11, r18, Address(rcx, +0x1de09163)); // cmovge r11, r18, qword ptr [rcx+0x1de09163] IID999 + __ ecmovq (Assembler::Condition::greaterEqual, r14, r14, Address(r24, r23, (Address::ScaleFactor)1, +0x5df3b4da)); // cmovge r14, qword ptr [r24+r23*2+0x5df3b4da] IID1000 + __ ecmovq (Assembler::Condition::lessEqual, r15, r14, Address(r30, r20, (Address::ScaleFactor)1, +0x5c9ab976)); // cmovle r15, r14, qword ptr [r30+r20*2+0x5c9ab976] IID1001 + __ ecmovq (Assembler::Condition::lessEqual, r26, r26, Address(r18, r27, (Address::ScaleFactor)2, -0xd8c329)); // cmovle r26, qword ptr [r18+r27*4-0xd8c329] IID1002 + __ ecmovq (Assembler::Condition::greater, r29, r9, Address(r30, r20, (Address::ScaleFactor)3, -0x37a9cf8d)); // cmovg r29, r9, qword ptr [r30+r20*8-0x37a9cf8d] IID1003 + __ ecmovq (Assembler::Condition::greater, r20, r20, Address(r8, rbx, (Address::ScaleFactor)1, +0x1bdc7def)); // cmovg r20, qword ptr [r8+rbx*2+0x1bdc7def] IID1004 #endif // _LP64 static const uint8_t insns[] = @@ -1243,665 +1327,749 @@ 0x62, 0xd4, 0x68, 0x18, 0x81, 0xb4, 0x82, 0xb9, 0xe2, 0xe1, 0xe9, 0x00, 0x00, 0x00, 0x01, // IID265 0x62, 0xdc, 0x68, 0x1c, 0x81, 0xb4, 0x7d, 0xf8, 0xe2, 0x34, 0x1b, 0x00, 0x00, 0x00, 0x01, // IID266 0x62, 0x0c, 0x60, 0x10, 0x01, 0xac, 0x3b, 0xd8, 0xe7, 0x3c, 0x1f, // IID267 - 0x62, 0x4c, 0x1c, 0x10, 0x01, 0xa4, 0xc8, 0x3e, 0x12, 0xac, 0x9f, // IID268 + 0xd5, 0x55, 0x03, 0xa4, 0xc8, 0x3e, 0x12, 0xac, 0x9f, // IID268 0x62, 0x2c, 0x70, 0x14, 0x01, 0xac, 0xc2, 0x88, 0xe0, 0x08, 0xe4, // IID269 0x62, 0x94, 0x70, 0x1c, 0x01, 0x8c, 0x67, 0x16, 0x82, 0x5b, 0x01, // IID270 - 0x62, 0x64, 0x0c, 0x10, 0x09, 0xa4, 0xd3, 0x4c, 0xbf, 0xca, 0xb9, // IID271 - 0x62, 0x8c, 0x6c, 0x10, 0x09, 0x94, 0xd4, 0x3b, 0xa7, 0x23, 0x35, // IID272 - 0x62, 0x84, 0x34, 0x1c, 0x09, 0xac, 0x7f, 0xaa, 0x22, 0xf4, 0xd5, // IID273 - 0x62, 0xec, 0x7c, 0x14, 0x09, 0x87, 0x01, 0x9b, 0xaf, 0xe9, // IID274 - 0x62, 0x8c, 0x1c, 0x10, 0x08, 0xa4, 0x1e, 0x3a, 0x1e, 0x28, 0x17, // IID275 - 0x62, 0xb4, 0x68, 0x18, 0x08, 0x94, 0xbb, 0xbb, 0xb5, 0x77, 0x24, // IID276 - 0x62, 0x44, 0x7c, 0x14, 0x08, 0x84, 0x4b, 0x51, 0x2e, 0x8a, 0xce, // IID277 - 0x62, 0xd4, 0x60, 0x1c, 0x08, 0x9c, 0xe3, 0x2d, 0x84, 0x29, 0xdd, // IID278 - 0x62, 0x9c, 0x28, 0x10, 0x29, 0x94, 0x73, 0xd2, 0x31, 0x64, 0xc2, // IID279 - 0x62, 0x2c, 0x00, 0x10, 0x29, 0xbc, 0x6e, 0x19, 0x85, 0x21, 0x14, // IID280 - 0x62, 0x54, 0x54, 0x14, 0x29, 0xa9, 0x86, 0xed, 0xaf, 0xef, // IID281 - 0x62, 0x04, 0x04, 0x14, 0x29, 0xbc, 0x01, 0x9f, 0x76, 0x1e, 0xf5, // IID282 - 0x62, 0x7c, 0x04, 0x18, 0x31, 0xa2, 0xe5, 0xbc, 0x2b, 0x5c, // IID283 - 0x62, 0x4c, 0x20, 0x10, 0x31, 0x9c, 0x39, 0xb3, 0x78, 0x60, 0x5c, // IID284 - 0x62, 0x54, 0x6c, 0x14, 0x31, 0xb4, 0xd0, 0x7f, 0xc7, 0x12, 0xf6, // IID285 - 0x62, 0x54, 0x34, 0x1c, 0x31, 0x8f, 0xad, 0xcd, 0x5a, 0x77, // IID286 - 0x62, 0xac, 0x50, 0x10, 0x30, 0xbc, 0x52, 0xd5, 0x1f, 0xe3, 0x2f, // IID287 - 0x62, 0x5c, 0x2c, 0x18, 0x30, 0x93, 0xde, 0x50, 0x31, 0x0a, // IID288 - 0x62, 0x2c, 0x68, 0x14, 0x30, 0x84, 0xf6, 0x97, 0xe8, 0xd4, 0x1a, // IID289 - 0x62, 0x7c, 0x38, 0x1c, 0x30, 0x84, 0x20, 0x82, 0xae, 0x6e, 0x62, // IID290 - 0x62, 0xd4, 0x54, 0x10, 0x81, 0xc7, 0x00, 0x00, 0x10, 0x00, // IID291 - 0x62, 0xfc, 0x7c, 0x18, 0x81, 0xc2, 0x00, 0x00, 0x10, 0x00, // IID292 - 0xd5, 0x10, 0x81, 0xc2, 0x00, 0x01, 0x00, 0x00, // IID293 - 0x62, 0xfc, 0x14, 0x1c, 0x83, 0xc3, 0x10, // IID294 - 0x62, 0xfc, 0x7c, 0x1c, 0x83, 0xc7, 0x10, // IID295 - 0x62, 0xdc, 0x34, 0x14, 0x81, 0xc1, 0x00, 0x00, 0x00, 0x01, // IID296 - 0x62, 0xfc, 0x14, 0x10, 0x81, 0xe2, 0x00, 0x00, 0x10, 0x00, // IID297 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xe6, 0x00, 0x00, 0x10, 0x00, // IID298 - 0xd5, 0x10, 0x81, 0xe3, 0x00, 0x00, 0x01, 0x00, // IID299 - 0x62, 0xdc, 0x24, 0x14, 0x81, 0xe1, 0x00, 0x00, 0x10, 0x00, // IID300 - 0x62, 0xfc, 0x7c, 0x1c, 0x81, 0xe4, 0x00, 0x00, 0x10, 0x00, // IID301 - 0x62, 0xdc, 0x1c, 0x14, 0x83, 0xe4, 0x10, // IID302 - 0x62, 0x6c, 0x7c, 0x08, 0x69, 0xfe, 0x00, 0x10, 0x00, 0x00, // IID303 + 0x62, 0x64, 0x0c, 0x10, 0x21, 0xa4, 0xd3, 0x4c, 0xbf, 0xca, 0xb9, // IID271 + 0xd5, 0x53, 0x23, 0x94, 0xd4, 0x3b, 0xa7, 0x23, 0x35, // IID272 + 0x62, 0x84, 0x34, 0x1c, 0x21, 0xac, 0x7f, 0xaa, 0x22, 0xf4, 0xd5, // IID273 + 0x62, 0xec, 0x7c, 0x14, 0x21, 0x87, 0x01, 0x9b, 0xaf, 0xe9, // IID274 + 0x62, 0x8c, 0x1c, 0x10, 0x09, 0xa4, 0x1e, 0x3a, 0x1e, 0x28, 0x17, // IID275 + 0xd5, 0x22, 0x0b, 0x94, 0xbb, 0xbb, 0xb5, 0x77, 0x24, // IID276 + 0x62, 0x44, 0x7c, 0x14, 0x09, 0x84, 0x4b, 0x51, 0x2e, 0x8a, 0xce, // IID277 + 0x62, 0xd4, 0x60, 0x1c, 0x09, 0x9c, 0xe3, 0x2d, 0x84, 0x29, 0xdd, // IID278 + 0x62, 0x9c, 0x28, 0x10, 0x08, 0x94, 0x73, 0xd2, 0x31, 0x64, 0xc2, // IID279 + 0xd5, 0x76, 0x0a, 0xbc, 0x6e, 0x19, 0x85, 0x21, 0x14, // IID280 + 0x62, 0x54, 0x54, 0x14, 0x08, 0xa9, 0x86, 0xed, 0xaf, 0xef, // IID281 + 0x62, 0x04, 0x04, 0x14, 0x08, 0xbc, 0x01, 0x9f, 0x76, 0x1e, 0xf5, // IID282 + 0x62, 0x7c, 0x04, 0x18, 0x29, 0xa2, 0xe5, 0xbc, 0x2b, 0x5c, // IID283 + 0x62, 0x4c, 0x20, 0x10, 0x29, 0x9c, 0x39, 0xb3, 0x78, 0x60, 0x5c, // IID284 + 0x62, 0x54, 0x6c, 0x14, 0x29, 0xb4, 0xd0, 0x7f, 0xc7, 0x12, 0xf6, // IID285 + 0x62, 0x54, 0x34, 0x1c, 0x29, 0x8f, 0xad, 0xcd, 0x5a, 0x77, // IID286 + 0x62, 0xac, 0x50, 0x10, 0x31, 0xbc, 0x52, 0xd5, 0x1f, 0xe3, 0x2f, // IID287 + 0xd5, 0x15, 0x33, 0x93, 0xde, 0x50, 0x31, 0x0a, // IID288 + 0x62, 0x2c, 0x68, 0x14, 0x31, 0x84, 0xf6, 0x97, 0xe8, 0xd4, 0x1a, // IID289 + 0x62, 0x7c, 0x38, 0x1c, 0x31, 0x84, 0x20, 0x82, 0xae, 0x6e, 0x62, // IID290 + 0x62, 0xac, 0x7c, 0x10, 0x30, 0x94, 0x3d, 0xf3, 0x49, 0xfc, 0xeb, // IID291 + 0xd5, 0x34, 0x32, 0xac, 0xbb, 0xe1, 0xf1, 0x7e, 0x23, // IID292 + 0x62, 0x3c, 0x14, 0x14, 0x30, 0xb4, 0xb2, 0x5b, 0x09, 0xc0, 0x5c, // IID293 + 0x62, 0x4c, 0x20, 0x14, 0x30, 0x9c, 0xe1, 0x58, 0xb9, 0xf7, 0x1c, // IID294 + 0x62, 0xdc, 0x7c, 0x10, 0x83, 0xc0, 0x10, // IID295 + 0x62, 0xdc, 0x7c, 0x18, 0x83, 0xc0, 0x10, // IID296 + 0xd5, 0x10, 0x81, 0xc5, 0x00, 0x00, 0x01, 0x00, // IID297 + 0x62, 0xd4, 0x3c, 0x14, 0x81, 0xc0, 0x00, 0x00, 0x10, 0x00, // IID298 + 0x62, 0xd4, 0x7c, 0x1c, 0x81, 0xc5, 0x00, 0x00, 0x10, 0x00, // IID299 + 0x62, 0xdc, 0x14, 0x14, 0x81, 0xc5, 0x00, 0x00, 0x00, 0x01, // IID300 + 0x41, 0x83, 0xe4, 0x10, // IID301 + 0x62, 0xdc, 0x7c, 0x18, 0x83, 0xe6, 0x10, // IID302 + 0xd5, 0x11, 0x83, 0xe0, 0x10, // IID303 + 0x62, 0xd4, 0x3c, 0x1c, 0x83, 0xe4, 0x01, // IID304 + 0x62, 0xd4, 0x7c, 0x1c, 0x83, 0xe5, 0x01, // IID305 + 0x62, 0xdc, 0x34, 0x14, 0x83, 0xe1, 0x10, // IID306 + 0x62, 0xec, 0x7c, 0x08, 0x69, 0xd7, 0x00, 0x00, 0x01, 0x00, // IID307 + 0x62, 0xd4, 0x7c, 0x08, 0x69, 0xc1, 0x00, 0x00, 0x01, 0x00, // IID308 + 0x62, 0x4c, 0x7c, 0x08, 0x69, 0xd2, 0x00, 0x00, 0x00, 0x10, // IID309 + 0x62, 0x6c, 0x7c, 0x0c, 0x6b, 0xcd, 0x01, // IID310 + 0x62, 0xdc, 0x7c, 0x0c, 0x6b, 0xc0, 0x01, // IID311 + 0x62, 0x4c, 0x7c, 0x0c, 0x69, 0xc0, 0x00, 0x00, 0x00, 0x01, // IID312 + 0x62, 0xdc, 0x0c, 0x10, 0x83, 0xca, 0x01, // IID313 + 0x62, 0xfc, 0x7c, 0x18, 0x83, 0xce, 0x01, // IID314 + 0xd5, 0x10, 0x81, 0xc9, 0x00, 0x00, 0x10, 0x00, // IID315 + 0x62, 0xd4, 0x3c, 0x14, 0x83, 0xc8, 0x01, // IID316 + 0x62, 0xdc, 0x7c, 0x1c, 0x83, 0xcb, 0x01, // IID317 #endif // _LP64 - 0x62, 0xf4, 0x7c, 0x08, 0x69, 0xc3, 0x00, 0x10, 0x00, 0x00, // IID304 + 0x62, 0xf4, 0x6c, 0x1c, 0x81, 0xca, 0x00, 0x00, 0x00, 0x10, // IID318 #ifdef _LP64 - 0x62, 0x4c, 0x7c, 0x08, 0x69, 0xc0, 0x00, 0x00, 0x10, 0x00, // IID305 - 0x62, 0xec, 0x7c, 0x0c, 0x69, 0xe8, 0x00, 0x00, 0x01, 0x00, // IID306 - 0x62, 0xdc, 0x7c, 0x0c, 0x69, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID307 - 0x62, 0x54, 0x7c, 0x0c, 0x6b, 0xed, 0x10, // IID308 - 0x62, 0xd4, 0x14, 0x10, 0x81, 0xc8, 0x00, 0x00, 0x00, 0x01, // IID309 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xcc, 0x00, 0x00, 0x00, 0x01, // IID310 - 0xd5, 0x11, 0x81, 0xce, 0x00, 0x10, 0x00, 0x00, // IID311 - 0x62, 0xf4, 0x3c, 0x14, 0x83, 0xca, 0x10, // IID312 - 0x62, 0xd4, 0x7c, 0x1c, 0x83, 0xc8, 0x10, // IID313 - 0x62, 0xd4, 0x14, 0x1c, 0x81, 0xcd, 0x00, 0x10, 0x00, 0x00, // IID314 - 0x62, 0xd4, 0x34, 0x10, 0xd1, 0xd5, // IID315 - 0x62, 0xfc, 0x7c, 0x18, 0xd1, 0xd2, // IID316 - 0x41, 0xc1, 0xd1, 0x10, // IID317 - 0x62, 0xdc, 0x2c, 0x10, 0xc1, 0xc1, 0x08, // IID318 + 0xd5, 0x10, 0xc1, 0xd6, 0x08, // IID319 + 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xd7, 0x08, // IID320 + 0xd5, 0x10, 0xc1, 0xd3, 0x04, // IID321 + 0x62, 0xdc, 0x0c, 0x10, 0xc1, 0xc0, 0x02, // IID322 + 0x62, 0xdc, 0x7c, 0x18, 0xc1, 0xc5, 0x02, // IID323 + 0x41, 0xc1, 0xc0, 0x02, // IID324 + 0x62, 0xdc, 0x6c, 0x14, 0xc1, 0xc0, 0x10, // IID325 + 0x62, 0xd4, 0x7c, 0x1c, 0xc1, 0xc5, 0x10, // IID326 + 0x62, 0xdc, 0x3c, 0x14, 0xd1, 0xc0, // IID327 + 0x62, 0xfc, 0x1c, 0x10, 0xc1, 0xc9, 0x10, // IID328 + 0x62, 0xdc, 0x7c, 0x18, 0xc1, 0xc8, 0x10, // IID329 + 0xd5, 0x10, 0xc1, 0xc9, 0x04, // IID330 + 0x62, 0xf4, 0x3c, 0x14, 0xc1, 0xc9, 0x04, // IID331 + 0x62, 0xfc, 0x7c, 0x1c, 0xc1, 0xc8, 0x04, // IID332 + 0x62, 0xd4, 0x04, 0x1c, 0xc1, 0xcf, 0x02, // IID333 + 0x62, 0xdc, 0x0c, 0x18, 0xc1, 0xe3, 0x04, // IID334 + 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xe7, 0x04, // IID335 + 0xd5, 0x11, 0xc1, 0xe6, 0x04, // IID336 + 0x62, 0xf4, 0x24, 0x14, 0xc1, 0xe2, 0x02, // IID337 + 0x62, 0xfc, 0x7c, 0x1c, 0xc1, 0xe3, 0x02, // IID338 + 0x62, 0xfc, 0x5c, 0x14, 0xc1, 0xe4, 0x02, // IID339 + 0x62, 0xfc, 0x54, 0x10, 0xd1, 0xff, // IID340 + 0x62, 0xdc, 0x7c, 0x18, 0xd1, 0xfe, // IID341 + 0xd5, 0x11, 0xc1, 0xf9, 0x02, // IID342 + 0x62, 0xfc, 0x3c, 0x14, 0xc1, 0xfb, 0x04, // IID343 + 0x62, 0xd4, 0x7c, 0x1c, 0xc1, 0xfe, 0x04, // IID344 + 0x62, 0xdc, 0x2c, 0x14, 0xc1, 0xfa, 0x10, // IID345 + 0x62, 0xd4, 0x4c, 0x10, 0xc1, 0xe5, 0x08, // IID346 + 0x62, 0xdc, 0x7c, 0x18, 0xc1, 0xe0, 0x08, // IID347 + 0x41, 0xc1, 0xe6, 0x10, // IID348 + 0x62, 0xdc, 0x1c, 0x14, 0xc1, 0xe1, 0x08, // IID349 + 0x62, 0xd4, 0x7c, 0x1c, 0xc1, 0xe2, 0x08, // IID350 + 0x62, 0xfc, 0x5c, 0x14, 0xd1, 0xe4, // IID351 + 0x62, 0xf4, 0x1c, 0x18, 0xc1, 0xeb, 0x04, // IID352 + 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xef, 0x04, // IID353 + 0xd5, 0x11, 0xc1, 0xec, 0x10, // IID354 + 0x62, 0xdc, 0x3c, 0x14, 0xc1, 0xee, 0x04, // IID355 + 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xef, 0x04, // IID356 + 0x62, 0xdc, 0x04, 0x14, 0xc1, 0xef, 0x02, // IID357 + 0x62, 0xd4, 0x5c, 0x10, 0x81, 0xea, 0x00, 0x01, 0x00, 0x00, // IID358 + 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xed, 0x00, 0x01, 0x00, 0x00, // IID359 + 0xd5, 0x11, 0x81, 0xe9, 0x00, 0x01, 0x00, 0x00, // IID360 + 0x62, 0xd4, 0x44, 0x14, 0x81, 0xec, 0x00, 0x00, 0x00, 0x10, // IID361 + 0x62, 0xfc, 0x7c, 0x1c, 0x81, 0xe8, 0x00, 0x00, 0x00, 0x10, // IID362 + 0x62, 0xdc, 0x04, 0x14, 0x83, 0xef, 0x01, // IID363 + 0x62, 0xd4, 0x34, 0x18, 0x81, 0xf7, 0x00, 0x00, 0x00, 0x01, // IID364 + 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xf5, 0x00, 0x00, 0x00, 0x01, // IID365 + 0xd5, 0x11, 0x83, 0xf4, 0x10, // IID366 + 0x62, 0xfc, 0x14, 0x14, 0x83, 0xf6, 0x10, // IID367 #endif // _LP64 - 0x62, 0xf4, 0x7c, 0x18, 0xc1, 0xc2, 0x08, // IID319 + 0x62, 0xf4, 0x7c, 0x1c, 0x83, 0xf3, 0x10, // IID368 #ifdef _LP64 - 0xd5, 0x11, 0xc1, 0xc0, 0x10, // IID320 - 0x62, 0xf4, 0x3c, 0x14, 0xc1, 0xc1, 0x08, // IID321 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xc6, 0x08, // IID322 - 0x62, 0xdc, 0x1c, 0x14, 0xc1, 0xc4, 0x10, // IID323 - 0x62, 0xdc, 0x74, 0x10, 0xc1, 0xcc, 0x04, // IID324 + 0x62, 0xd4, 0x3c, 0x1c, 0x83, 0xf0, 0x10, // IID369 + 0x62, 0xd4, 0x7c, 0x10, 0x81, 0xed, 0x00, 0x00, 0x40, 0x00, // IID370 + 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xec, 0x00, 0x00, 0x40, 0x00, // IID371 + 0xd5, 0x10, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x04, // IID372 + 0x62, 0xdc, 0x4c, 0x14, 0x81, 0xea, 0x00, 0x00, 0x00, 0x40, // IID373 + 0x62, 0xd4, 0x7c, 0x1c, 0x81, 0xea, 0x00, 0x00, 0x00, 0x40, // IID374 + 0x62, 0xd4, 0x24, 0x1c, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x40, // IID375 + 0x62, 0x1c, 0x64, 0x10, 0x03, 0xa4, 0x06, 0x73, 0x0a, 0x1a, 0x6a, // IID376 + 0xd5, 0x74, 0x03, 0xb4, 0x9a, 0xcf, 0x90, 0xf9, 0x25, // IID377 + 0x62, 0x6c, 0x70, 0x1c, 0x03, 0x8c, 0x03, 0xbc, 0x5d, 0x2d, 0x48, // IID378 + 0x62, 0x54, 0x34, 0x1c, 0x03, 0x8b, 0x01, 0xee, 0xd5, 0x43, // IID379 + 0x62, 0xac, 0x74, 0x18, 0x23, 0xbc, 0xbd, 0xbc, 0xc2, 0x25, 0x28, // IID380 + 0xd5, 0x47, 0x23, 0x9c, 0xfd, 0x6b, 0x47, 0x97, 0xed, // IID381 + 0x62, 0xac, 0x30, 0x1c, 0x23, 0xbc, 0x36, 0x45, 0x53, 0xea, 0xf8, // IID382 + 0x62, 0xdc, 0x60, 0x1c, 0x23, 0x9c, 0x84, 0xee, 0x23, 0x02, 0x0b, // IID383 + 0x62, 0x04, 0x00, 0x18, 0xaf, 0xac, 0x67, 0x97, 0x85, 0xd6, 0xe0, // IID384 + 0xd5, 0xd0, 0xaf, 0x8c, 0x5f, 0x45, 0x75, 0xdc, 0x0a, // IID385 + 0x62, 0x74, 0x20, 0x14, 0xaf, 0x8c, 0xb2, 0x9f, 0xf0, 0x26, 0xbc, // IID386 + 0x62, 0xdc, 0x60, 0x1c, 0xaf, 0x9c, 0xf4, 0xd9, 0x65, 0x62, 0xae, // IID387 + 0x62, 0xd4, 0x74, 0x10, 0x0b, 0x8e, 0x23, 0x22, 0x64, 0x10, // IID388 + 0xd5, 0x55, 0x0b, 0x97, 0x46, 0x7c, 0x65, 0x85, // IID389 + 0x62, 0x84, 0x04, 0x1c, 0x0b, 0xb4, 0xa4, 0x97, 0x69, 0x3b, 0x74, // IID390 + 0x62, 0x74, 0x38, 0x1c, 0x0b, 0x84, 0xf2, 0x24, 0xeb, 0x7b, 0xa7, // IID391 + 0x62, 0x0c, 0x74, 0x18, 0x2b, 0xa4, 0xae, 0xe5, 0x10, 0x93, 0x0e, // IID392 + 0xd5, 0x13, 0x2b, 0x8c, 0x56, 0x2f, 0x91, 0xf8, 0xe4, // IID393 + 0x62, 0xcc, 0x34, 0x1c, 0x2b, 0xae, 0xd3, 0xff, 0x79, 0x2f, // IID394 + 0x62, 0xa4, 0x7c, 0x14, 0x2b, 0x84, 0xb2, 0xc1, 0x71, 0x5d, 0x67, // IID395 + 0x62, 0x24, 0x20, 0x10, 0x33, 0xa4, 0x93, 0x7f, 0xf4, 0x3d, 0x87, // IID396 + 0xd5, 0x35, 0x33, 0xb4, 0x5f, 0x34, 0xae, 0x0d, 0xb0, // IID397 + 0x62, 0xc4, 0x58, 0x14, 0x33, 0x94, 0x85, 0x1e, 0x39, 0x10, 0xe6, // IID398 + 0x62, 0xc4, 0x60, 0x14, 0x33, 0x9c, 0x7d, 0x56, 0x27, 0xe4, 0xd2, // IID399 + 0x62, 0xa4, 0x10, 0x10, 0x32, 0x8c, 0xaa, 0x84, 0x3e, 0x57, 0x66, // IID400 + 0xd5, 0x73, 0x32, 0xb4, 0xc8, 0x3f, 0xa9, 0x94, 0x3a, // IID401 + 0x62, 0x44, 0x10, 0x1c, 0x32, 0xac, 0x7f, 0x32, 0x35, 0xd4, 0x76, // IID402 + 0x62, 0x14, 0x04, 0x1c, 0x32, 0xbc, 0x0d, 0xe6, 0x92, 0xb1, 0xb8, // IID403 + 0x62, 0xec, 0x75, 0x10, 0x33, 0x84, 0x17, 0x91, 0xa2, 0x62, 0x05, // IID404 + 0x66, 0xd5, 0x76, 0x33, 0xac, 0xe2, 0x0e, 0x98, 0xe6, 0xab, // IID405 + 0x62, 0x54, 0x25, 0x14, 0x33, 0x9a, 0x5a, 0x1c, 0x91, 0x0a, // IID406 + 0x62, 0x4c, 0x01, 0x14, 0x33, 0xbc, 0x9e, 0x26, 0x5c, 0x09, 0xff, // IID407 + 0x62, 0x7c, 0x1c, 0x18, 0x03, 0xef, // IID408 + 0xd5, 0x54, 0x03, 0xe4, // IID409 + 0xd5, 0x51, 0x03, 0xe0, // IID410 + 0x62, 0x54, 0x24, 0x1c, 0x03, 0xd7, // IID411 + 0x62, 0xec, 0x64, 0x14, 0x03, 0xdc, // IID412 + 0x62, 0x7c, 0x44, 0x14, 0x03, 0xff, // IID413 + 0x62, 0xcc, 0x2c, 0x10, 0x23, 0xd8, // IID414 + 0xd5, 0x51, 0x23, 0xfc, // IID415 + 0x45, 0x23, 0xdd, // IID416 + 0x62, 0xdc, 0x14, 0x1c, 0x23, 0xd7, // IID417 + 0x62, 0xec, 0x44, 0x14, 0x23, 0xff, // IID418 + 0x62, 0x44, 0x34, 0x1c, 0x23, 0xd9, // IID419 + 0x62, 0xcc, 0x54, 0x10, 0xaf, 0xe0, // IID420 + 0xd5, 0xd1, 0xaf, 0xed, // IID421 + 0x41, 0x0f, 0xaf, 0xdb, // IID422 + 0x62, 0xf4, 0x54, 0x14, 0xaf, 0xd9, // IID423 + 0x62, 0x6c, 0x04, 0x14, 0xaf, 0xfd, // IID424 + 0x62, 0x44, 0x04, 0x1c, 0xaf, 0xcf, // IID425 + 0x62, 0xcc, 0x0d, 0x10, 0x0b, 0xf9, // IID426 + 0x66, 0xd5, 0x40, 0x0b, 0xd1, // IID427 + 0x66, 0x44, 0x0b, 0xd1, // IID428 + 0x62, 0xcc, 0x05, 0x14, 0x0b, 0xea, // IID429 + 0x62, 0xec, 0x55, 0x14, 0x0b, 0xeb, // IID430 #endif // _LP64 - 0x62, 0xf4, 0x7c, 0x18, 0xc1, 0xca, 0x04, // IID325 + 0x62, 0xf4, 0x6d, 0x1c, 0x0b, 0xda, // IID431 #ifdef _LP64 - 0x41, 0xc1, 0xc8, 0x10, // IID326 - 0x62, 0xf4, 0x64, 0x14, 0xc1, 0xca, 0x10, // IID327 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xcf, 0x10, // IID328 - 0x62, 0xfc, 0x4c, 0x14, 0xc1, 0xce, 0x08, // IID329 - 0x62, 0xdc, 0x44, 0x10, 0xc1, 0xe1, 0x10, // IID330 - 0x62, 0xd4, 0x7c, 0x18, 0xc1, 0xe6, 0x10, // IID331 - 0xd5, 0x11, 0xc1, 0xe7, 0x08, // IID332 - 0x62, 0xdc, 0x0c, 0x14, 0xc1, 0xe0, 0x02, // IID333 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xe5, 0x02, // IID334 - 0x62, 0xd4, 0x3c, 0x1c, 0xc1, 0xe0, 0x02, // IID335 - 0x62, 0xdc, 0x6c, 0x10, 0xc1, 0xf8, 0x10, // IID336 - 0x62, 0xd4, 0x7c, 0x18, 0xc1, 0xfd, 0x10, // IID337 - 0xd5, 0x11, 0xd1, 0xf8, // IID338 - 0x62, 0xfc, 0x1c, 0x14, 0xc1, 0xf9, 0x10, // IID339 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xf8, 0x10, // IID340 - 0x62, 0xfc, 0x74, 0x14, 0xc1, 0xf9, 0x04, // IID341 - 0x62, 0xf4, 0x3c, 0x10, 0xc1, 0xe1, 0x04, // IID342 - 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xe0, 0x04, // IID343 - 0x41, 0xc1, 0xe7, 0x02, // IID344 - 0x62, 0xdc, 0x0c, 0x1c, 0xc1, 0xe3, 0x04, // IID345 - 0x62, 0xfc, 0x7c, 0x1c, 0xc1, 0xe7, 0x04, // IID346 - 0x62, 0xdc, 0x0c, 0x14, 0xc1, 0xe6, 0x04, // IID347 - 0x62, 0xf4, 0x24, 0x10, 0xc1, 0xea, 0x02, // IID348 - 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xeb, 0x02, // IID349 - 0xd5, 0x10, 0xc1, 0xec, 0x02, // IID350 - 0x62, 0xfc, 0x54, 0x14, 0xd1, 0xef, // IID351 - 0x62, 0xdc, 0x7c, 0x1c, 0xd1, 0xee, // IID352 - 0x62, 0xdc, 0x34, 0x14, 0xc1, 0xe9, 0x02, // IID353 - 0x62, 0xfc, 0x3c, 0x10, 0x81, 0xeb, 0x00, 0x00, 0x10, 0x00, // IID354 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xee, 0x00, 0x00, 0x10, 0x00, // IID355 - 0xd5, 0x10, 0x81, 0xee, 0x00, 0x00, 0x00, 0x10, // IID356 - 0x62, 0xdc, 0x3c, 0x14, 0x81, 0xe8, 0x00, 0x00, 0x01, 0x00, // IID357 - 0x62, 0xd4, 0x7c, 0x1c, 0x81, 0xee, 0x00, 0x00, 0x01, 0x00, // IID358 - 0x62, 0xdc, 0x1c, 0x14, 0x81, 0xec, 0x00, 0x00, 0x00, 0x10, // IID359 - 0x62, 0xfc, 0x64, 0x18, 0x81, 0xf4, 0x00, 0x01, 0x00, 0x00, // IID360 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xf7, 0x00, 0x01, 0x00, 0x00, // IID361 -#endif // _LP64 - 0x81, 0xf3, 0x00, 0x10, 0x00, 0x00, // IID362 -#ifdef _LP64 - 0x62, 0xdc, 0x3c, 0x14, 0x81, 0xf6, 0x00, 0x00, 0x01, 0x00, // IID363 - 0x62, 0xdc, 0x7c, 0x1c, 0x81, 0xf7, 0x00, 0x00, 0x01, 0x00, // IID364 - 0x62, 0xdc, 0x04, 0x14, 0x81, 0xf7, 0x00, 0x10, 0x00, 0x00, // IID365 - 0x62, 0xd4, 0x5c, 0x10, 0x81, 0xea, 0x00, 0x00, 0x10, 0x00, // IID366 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xed, 0x00, 0x00, 0x10, 0x00, // IID367 - 0xd5, 0x11, 0x81, 0xe9, 0x00, 0x00, 0x10, 0x00, // IID368 - 0x62, 0xd4, 0x44, 0x14, 0x81, 0xec, 0x00, 0x00, 0x00, 0x40, // IID369 - 0x62, 0xfc, 0x7c, 0x1c, 0x81, 0xe8, 0x00, 0x00, 0x00, 0x40, // IID370 - 0x62, 0xdc, 0x04, 0x14, 0x81, 0xef, 0x00, 0x00, 0x01, 0x00, // IID371 - 0x62, 0x54, 0x74, 0x10, 0x03, 0xa9, 0x98, 0x2f, 0xef, 0x7f, // IID372 - 0x62, 0x7c, 0x14, 0x14, 0x03, 0x86, 0x54, 0xf5, 0x08, 0xb2, // IID373 - 0x62, 0x84, 0x14, 0x18, 0x23, 0x8c, 0xfc, 0x02, 0xa9, 0xa8, 0x50, // IID374 - 0x62, 0x0c, 0x4c, 0x14, 0x23, 0x8c, 0x92, 0x54, 0x27, 0xea, 0x70, // IID375 - 0x62, 0x1c, 0x64, 0x10, 0xaf, 0xa4, 0x06, 0x73, 0x0a, 0x1a, 0x6a, // IID376 - 0x62, 0xec, 0x08, 0x14, 0xaf, 0x94, 0x9a, 0x39, 0xd7, 0x32, 0x80, // IID377 - 0x62, 0x0c, 0x7c, 0x10, 0x0b, 0xbc, 0xd9, 0xbc, 0x5d, 0x2d, 0x48, // IID378 - 0x62, 0x44, 0x34, 0x1c, 0x0b, 0x9b, 0x01, 0xee, 0xd5, 0x43, // IID379 - 0x62, 0xac, 0x74, 0x18, 0x2b, 0xbc, 0xbd, 0xbc, 0xc2, 0x25, 0x28, // IID380 - 0x62, 0x84, 0x24, 0x14, 0x2b, 0xb4, 0x7d, 0xa7, 0x0d, 0x1f, 0x77, // IID381 - 0x62, 0x44, 0x30, 0x18, 0x33, 0xb4, 0xf1, 0x72, 0x37, 0x29, 0xb5, // IID382 - 0x62, 0xa4, 0x20, 0x1c, 0x33, 0x84, 0xa3, 0xee, 0x23, 0x02, 0x0b, // IID383 - 0x62, 0x04, 0x00, 0x18, 0x32, 0xac, 0x67, 0x97, 0x85, 0xd6, 0xe0, // IID384 - 0x62, 0x6c, 0x74, 0x14, 0x32, 0xb4, 0x5f, 0x45, 0x75, 0xdc, 0x0a, // IID385 - 0x62, 0x74, 0x21, 0x10, 0x33, 0x8c, 0xb2, 0x9f, 0xf0, 0x26, 0xbc, // IID386 - 0x62, 0xcc, 0x61, 0x1c, 0x33, 0xb4, 0x34, 0x4f, 0x5f, 0xcf, 0x82, // IID387 - 0x62, 0x64, 0x0c, 0x18, 0x03, 0xc1, // IID388 - 0xd5, 0x14, 0x03, 0xc1, // IID389 - 0x62, 0x44, 0x2c, 0x14, 0x03, 0xc4, // IID390 - 0x62, 0x6c, 0x3c, 0x14, 0x03, 0xc7, // IID391 - 0x62, 0x4c, 0x14, 0x18, 0x23, 0xd7, // IID392 - 0x45, 0x23, 0xd8, // IID393 - 0x62, 0xc4, 0x74, 0x1c, 0x23, 0xdf, // IID394 - 0x62, 0x54, 0x1c, 0x1c, 0x23, 0xe4, // IID395 - 0x62, 0xec, 0x4c, 0x10, 0xaf, 0xe3, // IID396 - 0x44, 0x0f, 0xaf, 0xc2, // IID397 - 0x62, 0x6c, 0x4c, 0x14, 0xaf, 0xdf, // IID398 - 0x62, 0x7c, 0x34, 0x1c, 0xaf, 0xca, // IID399 - 0x62, 0x44, 0x75, 0x18, 0x0b, 0xf5, // IID400 - 0x66, 0xd5, 0x54, 0x0b, 0xe3, // IID401 - 0x62, 0x4c, 0x1d, 0x1c, 0x0b, 0xf3, // IID402 - 0x62, 0x7c, 0x3d, 0x1c, 0x0b, 0xc6, // IID403 - 0x62, 0xdc, 0x7c, 0x10, 0x0b, 0xce, // IID404 - 0xd5, 0x15, 0x0b, 0xd1, // IID405 - 0x62, 0xec, 0x04, 0x1c, 0x0b, 0xc9, // IID406 - 0x62, 0x5c, 0x34, 0x1c, 0x0b, 0xce, // IID407 - 0x62, 0x7c, 0x5c, 0x10, 0xa5, 0xc5, // IID408 - 0xd5, 0x95, 0xa5, 0xf2, // IID409 - 0x62, 0x74, 0x7c, 0x14, 0xa5, 0xf2, // IID410 - 0x62, 0x7c, 0x64, 0x14, 0xa5, 0xc3, // IID411 - 0x62, 0x64, 0x24, 0x10, 0xad, 0xd3, // IID412 - 0xd5, 0xd1, 0xad, 0xdc, // IID413 - 0x62, 0x54, 0x74, 0x1c, 0xad, 0xf3, // IID414 - 0x62, 0xcc, 0x04, 0x14, 0xad, 0xdf, // IID415 - 0x62, 0x5c, 0x2c, 0x10, 0x2b, 0xe9, // IID416 - 0xd5, 0x45, 0x2b, 0xc3, // IID417 - 0x62, 0xc4, 0x6c, 0x14, 0x2b, 0xe5, // IID418 - 0x62, 0xec, 0x7c, 0x14, 0x2b, 0xc2, // IID419 - 0x62, 0xc4, 0x64, 0x10, 0x33, 0xc8, // IID420 - 0xd5, 0x41, 0x33, 0xdd, // IID421 - 0x62, 0x54, 0x44, 0x14, 0x33, 0xef, // IID422 - 0x62, 0x5c, 0x24, 0x1c, 0x33, 0xdd, // IID423 - 0x62, 0xec, 0x14, 0x10, 0x24, 0xc9, 0x01, // IID424 - 0xd5, 0xd4, 0xa4, 0xc6, 0x04, // IID425 - 0x62, 0x5c, 0x3c, 0x1c, 0x24, 0xdc, 0x10, // IID426 - 0x62, 0xc4, 0x04, 0x1c, 0x24, 0xff, 0x04, // IID427 - 0x62, 0xec, 0x14, 0x10, 0x2c, 0xc6, 0x04, // IID428 - 0x45, 0x0f, 0xac, 0xcd, 0x04, // IID429 - 0x62, 0x7c, 0x04, 0x1c, 0x2c, 0xe5, 0x02, // IID430 - 0x62, 0xec, 0x74, 0x14, 0x2c, 0xf9, 0x02, // IID431 - 0x62, 0xcc, 0x6c, 0x18, 0x40, 0xc5, // IID432 - 0xd5, 0x94, 0x40, 0xd5, // IID433 - 0x62, 0x6c, 0x74, 0x10, 0x41, 0xea, // IID434 - 0xd5, 0xd5, 0x41, 0xe0, // IID435 - 0x62, 0xcc, 0x2c, 0x18, 0x42, 0xe3, // IID436 - 0x45, 0x0f, 0x42, 0xd6, // IID437 - 0x62, 0x64, 0x24, 0x18, 0x43, 0xd9, // IID438 - 0xd5, 0xc1, 0x43, 0xf7, // IID439 - 0x62, 0x6c, 0x04, 0x10, 0x44, 0xf3, // IID440 - 0xd5, 0xd1, 0x44, 0xda, // IID441 - 0x62, 0x5c, 0x54, 0x10, 0x45, 0xf2, // IID442 - 0xd5, 0xc1, 0x45, 0xe7, // IID443 - 0x62, 0x7c, 0x1c, 0x18, 0x46, 0xef, // IID444 - 0xd5, 0xd4, 0x46, 0xe4, // IID445 - 0x62, 0x44, 0x5c, 0x10, 0x47, 0xc3, // IID446 - 0x45, 0x0f, 0x47, 0xd7, // IID447 - 0x62, 0xec, 0x64, 0x10, 0x48, 0xe7, // IID448 - 0xd5, 0x95, 0x48, 0xfa, // IID449 - 0x62, 0x6c, 0x64, 0x10, 0x49, 0xc7, // IID450 - 0xd5, 0xc5, 0x49, 0xe3, // IID451 - 0x44, 0x0f, 0x4a, 0xea, // IID452 - 0xd5, 0xd4, 0x4a, 0xff, // IID453 - 0x62, 0x5c, 0x44, 0x10, 0x4b, 0xcb, // IID454 - 0xd5, 0xd0, 0x4b, 0xec, // IID455 - 0x62, 0xcc, 0x3c, 0x10, 0x4c, 0xed, // IID456 - 0x41, 0x0f, 0x4c, 0xdb, // IID457 - 0x62, 0xf4, 0x54, 0x10, 0x4d, 0xd9, // IID458 - 0xd5, 0xd4, 0x4d, 0xfd, // IID459 - 0x62, 0x4c, 0x04, 0x18, 0x4e, 0xce, // IID460 - 0xd5, 0xd1, 0x4e, 0xf9, // IID461 - 0x62, 0xd4, 0x6c, 0x10, 0x4f, 0xca, // IID462 - 0xd5, 0x91, 0x4f, 0xcf, // IID463 - 0x62, 0xcc, 0x54, 0x10, 0x40, 0x9a, 0x8d, 0xf7, 0xd6, 0x91, // IID464 - 0x62, 0xec, 0x3c, 0x10, 0x41, 0x9c, 0x0e, 0x9a, 0x5f, 0xf8, 0x11, // IID465 - 0x62, 0x6c, 0x74, 0x10, 0x42, 0x84, 0x24, 0x5e, 0x77, 0x4d, 0x53, // IID466 - 0x62, 0xec, 0x5c, 0x10, 0x43, 0x94, 0x24, 0x33, 0xb1, 0x36, 0xb8, // IID467 - 0x62, 0x7c, 0x34, 0x18, 0x44, 0xaf, 0x9d, 0x3a, 0x7c, 0xb4, // IID468 - 0x62, 0x0c, 0x24, 0x18, 0x45, 0x8c, 0x70, 0x51, 0xf8, 0x9a, 0xbb, // IID469 - 0x62, 0x0c, 0x0c, 0x18, 0x46, 0x84, 0xae, 0x1d, 0x66, 0xd0, 0x00, // IID470 - 0x62, 0x04, 0x10, 0x18, 0x47, 0x8c, 0xde, 0x03, 0x14, 0x7e, 0x04, // IID471 - 0x62, 0xe4, 0x3c, 0x10, 0x48, 0x9c, 0xd1, 0xe8, 0xac, 0xb5, 0x9b, // IID472 - 0x62, 0x6c, 0x28, 0x10, 0x49, 0x84, 0x36, 0x46, 0x24, 0x35, 0x70, // IID473 - 0x62, 0x04, 0x60, 0x10, 0x4a, 0x94, 0xb0, 0x5c, 0x2f, 0xa1, 0x78, // IID474 - 0x62, 0x5c, 0x10, 0x10, 0x4b, 0x9c, 0x21, 0x3a, 0x30, 0xa8, 0x27, // IID475 - 0x62, 0x4c, 0x48, 0x10, 0x4c, 0x84, 0x43, 0x10, 0x1a, 0x54, 0x02, // IID476 - 0x62, 0x54, 0x00, 0x10, 0x4d, 0xbc, 0xc0, 0x51, 0x32, 0x8e, 0x55, // IID477 - 0x62, 0x84, 0x24, 0x10, 0x4e, 0x94, 0x10, 0x49, 0x78, 0xe6, 0xb8, // IID478 - 0x62, 0xec, 0x68, 0x10, 0x4f, 0x84, 0x9a, 0xe2, 0x17, 0xf5, 0xed, // IID479 - 0xd5, 0x19, 0x13, 0xdf, // IID480 - 0xd5, 0x5d, 0x3b, 0xf7, // IID481 - 0xd5, 0xdd, 0xaf, 0xec, // IID482 - 0xf3, 0xd5, 0xcd, 0xb8, 0xca, // IID483 - 0xd5, 0x5c, 0x1b, 0xc4, // IID484 - 0xd5, 0x48, 0x2b, 0xc2, // IID485 - 0xf3, 0xd5, 0xdd, 0xbc, 0xd4, // IID486 - 0xf3, 0xd5, 0xcd, 0xbd, 0xe1, // IID487 - 0xd5, 0x59, 0x03, 0xe0, // IID488 - 0xd5, 0x5d, 0x23, 0xc5, // IID489 - 0xd5, 0x59, 0x0b, 0xfb, // IID490 - 0x4d, 0x33, 0xfc, // IID491 - 0xd5, 0x58, 0x8b, 0xd3, // IID492 - 0xd5, 0xcc, 0xbc, 0xf9, // IID493 - 0x4d, 0x0f, 0xbd, 0xcd, // IID494 - 0xd5, 0x98, 0xa3, 0xcc, // IID495 - 0xd5, 0x1c, 0x87, 0xc5, // IID496 - 0xd5, 0x4d, 0x85, 0xc6, // IID497 - 0xd5, 0x6c, 0x01, 0xac, 0xb9, 0x4d, 0x6c, 0xf0, 0x4f, // IID498 - 0xd5, 0x5f, 0x21, 0x94, 0x50, 0x77, 0x5e, 0x26, 0x8a, // IID499 - 0xd5, 0x48, 0x39, 0x8c, 0x1b, 0x9c, 0xd5, 0x33, 0x40, // IID500 - 0xd5, 0x5a, 0x09, 0x94, 0xe6, 0x83, 0xcb, 0x6c, 0xc7, // IID501 - 0xd5, 0x3c, 0x31, 0xa4, 0xfc, 0x60, 0x15, 0x31, 0x4b, // IID502 - 0xd5, 0x6f, 0x29, 0xac, 0xa2, 0x57, 0x26, 0x3a, 0x5c, // IID503 - 0xd5, 0x6b, 0x89, 0xb4, 0xcd, 0x3f, 0x6f, 0x3d, 0x1a, // IID504 - 0xd5, 0xfe, 0xc1, 0x8c, 0xc1, 0x28, 0x24, 0x52, 0xca, // IID505 - 0xd5, 0x19, 0x81, 0xa1, 0xc3, 0x84, 0x21, 0x63, 0x00, 0x00, 0x00, 0x01, // IID506 - 0x4b, 0x81, 0x84, 0x2d, 0x3a, 0x15, 0x8d, 0xc6, 0x00, 0x00, 0x00, 0x01, // IID507 - 0x49, 0x81, 0xb9, 0xfa, 0x37, 0x4b, 0xec, 0x00, 0x10, 0x00, 0x00, // IID508 - 0xd5, 0x19, 0xd1, 0xbf, 0x51, 0xf5, 0xa7, 0x4f, // IID509 - 0xd5, 0x3a, 0xd1, 0xa4, 0xbd, 0x32, 0x82, 0xaa, 0x31, // IID510 - 0xd5, 0x3b, 0x81, 0x9c, 0xb8, 0x49, 0xc7, 0x9a, 0xb9, 0x00, 0x00, 0x00, 0x10, // IID511 - 0xd5, 0x39, 0xc1, 0xac, 0x34, 0x4f, 0x7a, 0x01, 0xc1, 0x02, // IID512 - 0xd5, 0x18, 0x81, 0xa8, 0x15, 0x5c, 0x76, 0xec, 0x00, 0x00, 0x10, 0x00, // IID513 - 0xd5, 0x1b, 0x83, 0xb4, 0x05, 0x15, 0x26, 0x02, 0x1d, 0x10, // IID514 - 0xd5, 0x2b, 0x83, 0x8c, 0x64, 0x1e, 0x67, 0x37, 0xcb, 0x01, // IID515 - 0xd5, 0x2a, 0xc7, 0x84, 0x81, 0xf8, 0x14, 0xbb, 0xe9, 0x00, 0x01, 0x00, 0x00, // IID516 - 0xd5, 0x19, 0xf7, 0x85, 0xf5, 0x76, 0xdc, 0x82, 0x00, 0x00, 0xff, 0xff, // IID517 - 0xd5, 0x68, 0x03, 0xbc, 0x99, 0x54, 0xc6, 0xea, 0x70, // IID518 - 0xd5, 0x1b, 0x23, 0x94, 0x38, 0x57, 0x25, 0xb2, 0xdf, // IID519 - 0xd5, 0x1a, 0x3b, 0x94, 0xdf, 0xbd, 0x30, 0xc9, 0x32, // IID520 - 0xf3, 0xd5, 0xcc, 0xbd, 0xa2, 0x71, 0x3d, 0xcc, 0xab, // IID521 - 0xd5, 0x5a, 0x0b, 0xb4, 0x73, 0xc8, 0x82, 0x39, 0xd3, // IID522 - 0x4d, 0x13, 0x92, 0x9f, 0xc5, 0xd7, 0x03, // IID523 - 0x4f, 0x0f, 0xaf, 0x94, 0xc0, 0xd3, 0x79, 0x9e, 0xf1, // IID524 - 0xf3, 0xd5, 0xd9, 0xb8, 0xbd, 0x93, 0x26, 0x81, 0x88, // IID525 - 0xd5, 0x28, 0x1b, 0x8c, 0x5b, 0xcb, 0x01, 0xc6, 0x53, // IID526 - 0xd5, 0x1c, 0x2b, 0xb4, 0x19, 0x8d, 0x0f, 0x74, 0x89, // IID527 - 0xf3, 0xd5, 0xed, 0xbc, 0xac, 0x5a, 0x3c, 0x8d, 0xc9, 0x30, // IID528 - 0xd5, 0x3e, 0x33, 0x94, 0x18, 0xfe, 0x29, 0xf7, 0xc2, // IID529 - 0xd5, 0x7b, 0x8b, 0x94, 0xe4, 0x6f, 0x53, 0x04, 0x9d, // IID530 - 0x48, 0x8d, 0x99, 0xa5, 0x02, 0x06, 0x45, // IID531 - 0xf2, 0xd5, 0xbf, 0x2c, 0xa4, 0x3e, 0xd0, 0x59, 0x67, 0x98, // IID532 - 0xd5, 0x5f, 0x87, 0xbc, 0x50, 0x13, 0xed, 0x98, 0x8f, // IID533 - 0xd5, 0x2d, 0x85, 0xb4, 0xe5, 0xf2, 0x81, 0x10, 0x17, // IID534 - 0xd5, 0x19, 0x83, 0xc7, 0x10, // IID535 - 0xd5, 0x19, 0x83, 0xe1, 0x10, // IID536 - 0xd5, 0x18, 0x81, 0xd7, 0x00, 0x01, 0x00, 0x00, // IID537 - 0xd5, 0x18, 0x81, 0xfb, 0x00, 0x00, 0x00, 0x10, // IID538 - 0xd5, 0x19, 0xd1, 0xd7, // IID539 - 0xd5, 0x18, 0xd1, 0xd9, // IID540 - 0xd5, 0x19, 0xc1, 0xc1, 0x02, // IID541 - 0xd5, 0x18, 0xc1, 0xc9, 0x04, // IID542 - 0xd5, 0x19, 0xd1, 0xfc, // IID543 - 0x49, 0xc1, 0xe7, 0x04, // IID544 - 0x48, 0x81, 0xdb, 0x00, 0x00, 0x01, 0x00, // IID545 - 0xd5, 0x18, 0xd1, 0xe5, // IID546 - 0x49, 0xd1, 0xea, // IID547 - 0x49, 0x83, 0xee, 0x10, // IID548 - 0xd5, 0x18, 0x81, 0xf2, 0x00, 0x00, 0x00, 0x10, // IID549 - 0xd5, 0x18, 0xc7, 0xc7, 0x10, 0x00, 0x00, 0x00, // IID550 - 0x49, 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // IID551 - 0x49, 0x0f, 0xba, 0xe6, 0x04, // IID552 - 0xd5, 0x19, 0xf7, 0xc0, 0x00, 0xf0, 0xff, 0xff, // IID553 - 0xd5, 0x18, 0x81, 0xcb, 0x00, 0x00, 0x10, 0x00, // IID554 - 0x48, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x10, // IID555 - 0xd5, 0x98, 0x40, 0x94, 0xdb, 0xc4, 0xc8, 0x11, 0x02, // IID556 - 0xd5, 0x98, 0x41, 0x9d, 0x43, 0x77, 0x26, 0x49, // IID557 - 0xd5, 0xeb, 0x42, 0xac, 0x60, 0xba, 0xd6, 0x73, 0xb3, // IID558 - 0xd5, 0xbd, 0x43, 0xa4, 0x22, 0x64, 0x07, 0xb2, 0xd9, // IID559 - 0xd5, 0xdb, 0x44, 0x8c, 0x8c, 0x6b, 0x19, 0x97, 0x34, // IID560 - 0xd5, 0xad, 0x45, 0xac, 0x7f, 0x67, 0xf6, 0x5c, 0xd8, // IID561 - 0xd5, 0xd8, 0x46, 0xb6, 0x05, 0xab, 0x39, 0x0f, // IID562 - 0xd5, 0xba, 0x47, 0x8c, 0xd6, 0xb4, 0x6a, 0x73, 0xfb, // IID563 - 0xd5, 0xfc, 0x48, 0x8c, 0x2b, 0x0b, 0x5b, 0x40, 0x0e, // IID564 - 0xd5, 0xbe, 0x49, 0xa4, 0xeb, 0xb5, 0xfb, 0x9d, 0x88, // IID565 - 0xd5, 0x9b, 0x4a, 0x9c, 0x56, 0xdd, 0x7c, 0x86, 0xe6, // IID566 - 0xd5, 0xfb, 0x4b, 0xac, 0x38, 0xd5, 0x9a, 0xce, 0xa8, // IID567 - 0xd5, 0xcb, 0x4c, 0x94, 0x50, 0x77, 0x41, 0xec, 0xa9, // IID568 - 0xd5, 0xfc, 0x4d, 0xa4, 0xed, 0xc4, 0xfd, 0xa0, 0x65, // IID569 - 0xd5, 0xe9, 0x4e, 0xbc, 0x13, 0xf4, 0x0e, 0xe5, 0xe2, // IID570 - 0xd5, 0xda, 0x4f, 0xb4, 0x62, 0x38, 0x1c, 0x5f, 0x1a, // IID571 - 0xd5, 0x10, 0xff, 0xd7, // IID572 - 0xd5, 0x19, 0xf7, 0xf6, // IID573 - 0xd5, 0x18, 0xf7, 0xfb, // IID574 - 0x49, 0xf7, 0xe9, // IID575 - 0x49, 0xf7, 0xe5, // IID576 - 0xd5, 0x18, 0xf7, 0xd8, // IID577 - 0xd5, 0x19, 0xf7, 0xd5, // IID578 - 0x48, 0xd3, 0xc1, // IID579 - 0xd5, 0x19, 0xd3, 0xc9, // IID580 - 0x49, 0xd3, 0xf8, // IID581 - 0xd5, 0x19, 0xd3, 0xe3, // IID582 - 0xd5, 0x19, 0xd3, 0xe6, // IID583 - 0xd5, 0x18, 0xd3, 0xef, // IID584 - 0x48, 0xff, 0xc3, // IID585 - 0x49, 0xff, 0xce, // IID586 - 0xd5, 0x18, 0x55, // IID587 - 0xd5, 0x18, 0x5d, // IID588 - 0xd5, 0x30, 0xff, 0x94, 0x6c, 0x2f, 0xaf, 0xc6, 0x56, // IID589 - 0xd5, 0x39, 0xf7, 0xa4, 0xdf, 0xdd, 0x14, 0x4b, 0xfe, // IID590 - 0xd5, 0x3b, 0xf7, 0x9c, 0x1b, 0xe1, 0x03, 0x24, 0xa7, // IID591 - 0xd5, 0x28, 0xd3, 0xbc, 0xb3, 0x2f, 0xb6, 0x9c, 0x9f, // IID592 - 0xd5, 0x39, 0xd3, 0xa4, 0xfa, 0x79, 0xa0, 0x95, 0x0b, // IID593 - 0xd5, 0x2b, 0xd3, 0xac, 0x16, 0x09, 0x4e, 0x54, 0x03, // IID594 - 0xd5, 0x19, 0xff, 0x84, 0x13, 0x50, 0x32, 0x0b, 0x12, // IID595 - 0xd5, 0x2b, 0xff, 0x8c, 0x89, 0x35, 0x13, 0x55, 0xcb, // IID596 - 0xd5, 0x7a, 0x69, 0xa4, 0x60, 0x5b, 0xfa, 0x21, 0xa6, 0x00, 0x00, 0x10, 0x00, // IID597 - 0xd5, 0x58, 0x69, 0xcf, 0x00, 0x01, 0x00, 0x00, // IID598 - 0xd5, 0x9c, 0xa4, 0xdb, 0x08, // IID599 - 0xd5, 0x9d, 0xac, 0xd4, 0x08, // IID600 - 0x62, 0xdc, 0x2c, 0x10, 0x8f, 0xc5, // IID601 - 0x62, 0xfc, 0xac, 0x18, 0x8f, 0xc6, // IID602 - 0x62, 0xdc, 0x0c, 0x10, 0xff, 0xf1, // IID603 - 0x62, 0xdc, 0x84, 0x18, 0xff, 0xf4, // IID604 - 0xd5, 0xbd, 0xb6, 0x9c, 0x9d, 0xcc, 0x72, 0xc9, 0xed, // IID605 - 0xd5, 0xaf, 0xb7, 0xb4, 0xb0, 0x22, 0x6d, 0x6c, 0xb5, // IID606 - 0xd5, 0xde, 0xbe, 0xa4, 0x3f, 0x54, 0xcb, 0x89, 0x61, // IID607 - 0xd5, 0xec, 0xbf, 0xa4, 0xfb, 0x9f, 0x9a, 0x17, 0xd2, // IID608 - 0x4c, 0x0f, 0xb6, 0xd9, // IID609 - 0xd5, 0xcd, 0xb7, 0xf7, // IID610 - 0x4c, 0x0f, 0xbe, 0xf1, // IID611 - 0xd5, 0xc9, 0xbf, 0xf9, // IID612 - 0x4f, 0x0f, 0xb1, 0xa4, 0x55, 0xc6, 0xd3, 0x39, 0xf8, // IID613 - 0x62, 0xf4, 0xfc, 0x08, 0xf7, 0xf9, // IID614 - 0x62, 0xd4, 0xfc, 0x0c, 0xf7, 0xff, // IID615 - 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xf7, // IID616 - 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xf0, // IID617 - 0x62, 0xdc, 0xfc, 0x08, 0xf7, 0xeb, // IID618 - 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xee, // IID619 - 0x62, 0xd4, 0xfc, 0x08, 0xf7, 0xe4, // IID620 - 0x62, 0xf4, 0xfc, 0x0c, 0xf7, 0xe1, // IID621 - 0x62, 0x94, 0xfc, 0x08, 0xf7, 0xa4, 0xcd, 0x6c, 0x54, 0x95, 0xdd, // IID622 - 0x62, 0x94, 0xf8, 0x0c, 0xf7, 0xa4, 0xc5, 0xfb, 0x89, 0x93, 0xd7, // IID623 - 0x62, 0xcc, 0xfc, 0x08, 0xaf, 0xee, // IID624 - 0xd5, 0x18, 0xf7, 0xe9, // IID625 - 0x62, 0x44, 0xfc, 0x0c, 0xaf, 0xec, // IID626 - 0x62, 0x4c, 0xfc, 0x0c, 0xaf, 0xf6, // IID627 - 0x62, 0x44, 0xfc, 0x08, 0xf5, 0xc7, // IID628 - 0x62, 0x4c, 0xfc, 0x08, 0xf5, 0xc9, // IID629 - 0x62, 0x6c, 0xfc, 0x0c, 0xf5, 0xcd, // IID630 - 0x62, 0xec, 0xfc, 0x0c, 0xf5, 0xf6, // IID631 - 0x62, 0xdc, 0xf4, 0x10, 0xf7, 0xde, // IID632 - 0xd5, 0x18, 0xf7, 0xd9, // IID633 - 0x62, 0xfc, 0x84, 0x14, 0xf7, 0xd9, // IID634 - 0x62, 0xdc, 0x94, 0x14, 0xf7, 0xdd, // IID635 - 0x62, 0xd4, 0xac, 0x18, 0xf7, 0xd1, // IID636 - 0xd5, 0x19, 0xf7, 0xd0, // IID637 - 0x62, 0x44, 0xfc, 0x08, 0x88, 0xe7, // IID638 - 0x62, 0x54, 0xfc, 0x08, 0x88, 0xd2, // IID639 - 0x62, 0x4c, 0xfc, 0x0c, 0x88, 0xde, // IID640 - 0x62, 0x4c, 0xfc, 0x0c, 0x88, 0xe4, // IID641 - 0x62, 0xd4, 0x9c, 0x10, 0xd3, 0xc6, // IID642 - 0xd5, 0x18, 0xd3, 0xc7, // IID643 - 0x62, 0xdc, 0xc4, 0x14, 0xd3, 0xc0, // IID644 - 0x62, 0xfc, 0xd4, 0x14, 0xd3, 0xc5, // IID645 - 0x62, 0xfc, 0x84, 0x10, 0xd3, 0xce, // IID646 - 0xd5, 0x19, 0xd3, 0xcc, // IID647 - 0x62, 0xd4, 0xf4, 0x14, 0xd3, 0xca, // IID648 - 0x62, 0xd4, 0xb4, 0x1c, 0xd3, 0xc9, // IID649 - 0x62, 0xdc, 0x94, 0x10, 0xd3, 0xe6, // IID650 - 0x49, 0xd3, 0xe3, // IID651 - 0x62, 0xd4, 0xac, 0x14, 0xd3, 0xe3, // IID652 - 0x62, 0xfc, 0xfc, 0x14, 0xd3, 0xe0, // IID653 - 0x62, 0xd4, 0xe4, 0x18, 0xd3, 0xff, // IID654 - 0x49, 0xd3, 0xfe, // IID655 - 0x62, 0xfc, 0xb4, 0x14, 0xd3, 0xf8, // IID656 - 0x62, 0xd4, 0xbc, 0x1c, 0xd3, 0xf8, // IID657 - 0x62, 0xd4, 0xa4, 0x18, 0xff, 0xcd, // IID658 - 0x48, 0xff, 0xc9, // IID659 - 0x62, 0xfc, 0xd4, 0x14, 0xff, 0xca, // IID660 - 0x62, 0xdc, 0x9c, 0x14, 0xff, 0xcc, // IID661 - 0xd5, 0x18, 0xff, 0xc0, // IID662 - 0xd5, 0x19, 0xff, 0xc5, // IID663 - 0x62, 0xd4, 0xec, 0x14, 0xff, 0xc1, // IID664 - 0x62, 0xfc, 0xe4, 0x14, 0xff, 0xc3, // IID665 - 0x62, 0xfc, 0xe4, 0x10, 0xd3, 0xe2, // IID666 - 0x49, 0xd3, 0xe0, // IID667 - 0x62, 0xd4, 0x9c, 0x1c, 0xd3, 0xe7, // IID668 - 0x62, 0xdc, 0x94, 0x14, 0xd3, 0xe5, // IID669 - 0x62, 0xdc, 0x9c, 0x10, 0xd3, 0xe8, // IID670 - 0xd5, 0x18, 0xd3, 0xeb, // IID671 - 0x62, 0xdc, 0xbc, 0x1c, 0xd3, 0xec, // IID672 - 0x62, 0xfc, 0xf4, 0x14, 0xd3, 0xe9, // IID673 - 0x62, 0x6c, 0xfc, 0x08, 0xf4, 0xe0, // IID674 - 0x62, 0x54, 0xfc, 0x08, 0xf4, 0xf6, // IID675 - 0x62, 0x5c, 0xfc, 0x0c, 0xf4, 0xe7, // IID676 - 0x62, 0x54, 0xfc, 0x0c, 0xf4, 0xf6, // IID677 - 0x62, 0x44, 0xfc, 0x08, 0xaf, 0xbd, 0xae, 0x4c, 0x3b, 0x96, // IID678 - 0x62, 0xec, 0xfc, 0x0c, 0xaf, 0x8a, 0xfb, 0xee, 0x54, 0x9f, // IID679 - 0x62, 0x04, 0xf8, 0x08, 0xf5, 0x9c, 0x8e, 0x83, 0xbf, 0x98, 0x27, // IID680 - 0x62, 0x84, 0xfc, 0x0c, 0xf5, 0xbc, 0x1a, 0xa3, 0x9c, 0x71, 0xc8, // IID681 - 0x62, 0xbc, 0xf4, 0x18, 0xf7, 0x9c, 0xcb, 0xc0, 0x2b, 0xb8, 0x97, // IID682 - 0x62, 0xf4, 0xf4, 0x1c, 0xf7, 0x9c, 0x0b, 0x8d, 0xd3, 0x92, 0x6f, // IID683 - 0x62, 0xc4, 0xfc, 0x08, 0x88, 0xa4, 0x24, 0x2a, 0xd8, 0x74, 0xd5, // IID684 - 0x62, 0x4c, 0xfc, 0x0c, 0x88, 0xbe, 0xd0, 0xf6, 0x03, 0x46, // IID685 - 0x62, 0xdc, 0xe4, 0x18, 0xd3, 0xa0, 0xf9, 0x06, 0x7d, 0x56, // IID686 - 0x62, 0x9c, 0x98, 0x1c, 0xd3, 0xa4, 0x20, 0xb2, 0xa7, 0xb3, 0xe3, // IID687 - 0x62, 0xbc, 0x98, 0x18, 0xd3, 0xbc, 0x87, 0x46, 0x43, 0xa8, 0xce, // IID688 - 0x62, 0x94, 0xb8, 0x1c, 0xd3, 0xbc, 0x86, 0x5b, 0x6f, 0xbd, 0x8e, // IID689 - 0x62, 0x94, 0xc4, 0x10, 0xff, 0x8c, 0x78, 0x23, 0x8d, 0x1d, 0xa5, // IID690 - 0x62, 0x9c, 0x94, 0x1c, 0xff, 0x8c, 0xcd, 0x57, 0x8b, 0xae, 0xa4, // IID691 - 0x62, 0xbc, 0xa0, 0x18, 0xff, 0x84, 0xfd, 0x24, 0x4b, 0x89, 0xde, // IID692 - 0x62, 0xf4, 0x90, 0x1c, 0xff, 0x84, 0x01, 0x37, 0xb7, 0x4b, 0xc9, // IID693 - 0x62, 0xdc, 0xac, 0x10, 0xd3, 0xac, 0x89, 0x6d, 0xb6, 0x76, 0xa0, // IID694 - 0x62, 0xd4, 0xb4, 0x14, 0xd3, 0xa9, 0x21, 0x8d, 0x79, 0x51, // IID695 - 0x62, 0x04, 0xf8, 0x08, 0xf4, 0xa4, 0x95, 0xf6, 0x96, 0x71, 0x20, // IID696 - 0x62, 0xbc, 0xfc, 0x0c, 0xf4, 0x9c, 0x2b, 0x2b, 0xc8, 0x26, 0xdb, // IID697 - 0x62, 0x4c, 0xf4, 0x10, 0x01, 0xbe, 0xff, 0xcc, 0x35, 0x39, // IID698 - 0x62, 0x1c, 0x8c, 0x18, 0x01, 0xb4, 0x93, 0x55, 0x64, 0x52, 0xcb, // IID699 - 0x62, 0x6c, 0xe8, 0x14, 0x01, 0xb4, 0x3c, 0x4b, 0xed, 0xd3, 0x5a, // IID700 - 0x62, 0xe4, 0xdc, 0x14, 0x01, 0xa2, 0x1b, 0x66, 0xd5, 0xcd, // IID701 - 0x62, 0xa4, 0x80, 0x10, 0x21, 0x8c, 0xdb, 0xd2, 0x47, 0xe2, 0x4c, // IID702 - 0x62, 0x6c, 0x88, 0x10, 0x21, 0xb4, 0x5a, 0xec, 0xc2, 0x11, 0xfb, // IID703 - 0x62, 0x44, 0x9c, 0x14, 0x21, 0x84, 0xdb, 0x41, 0xb4, 0x66, 0xd7, // IID704 - 0x62, 0x6c, 0x8c, 0x14, 0x21, 0xb6, 0x24, 0x1c, 0xd2, 0x07, // IID705 - 0x62, 0xc4, 0xa8, 0x10, 0x09, 0xa4, 0xdf, 0x92, 0x17, 0xc2, 0x58, // IID706 - 0x62, 0x14, 0x90, 0x18, 0x09, 0xac, 0x9a, 0xcd, 0x2c, 0x8f, 0xd3, // IID707 - 0x62, 0x44, 0xe4, 0x1c, 0x09, 0x94, 0x1c, 0x44, 0x0e, 0x4f, 0xe0, // IID708 - 0x62, 0x0c, 0x80, 0x14, 0x09, 0xbc, 0x7b, 0x56, 0x17, 0x8d, 0x02, // IID709 - 0x62, 0x4c, 0xb8, 0x10, 0x29, 0x9c, 0x7c, 0x10, 0xf6, 0x80, 0x69, // IID710 - 0x62, 0x14, 0x80, 0x18, 0x29, 0xbc, 0xf3, 0x19, 0x88, 0x68, 0xfb, // IID711 - 0x62, 0x0c, 0xf4, 0x14, 0x29, 0xbc, 0xa9, 0x46, 0x9e, 0x61, 0x31, // IID712 - 0x62, 0x84, 0xec, 0x14, 0x29, 0x94, 0x93, 0x1a, 0x86, 0x22, 0x19, // IID713 - 0x62, 0xc4, 0xe4, 0x18, 0x31, 0xab, 0xe0, 0x2b, 0xe9, 0xb8, // IID714 - 0x62, 0x34, 0xbc, 0x18, 0x31, 0x84, 0x8a, 0x64, 0x1c, 0x30, 0xfb, // IID715 - 0x62, 0x04, 0xf8, 0x14, 0x31, 0x8c, 0x1e, 0xd9, 0x54, 0x66, 0x7c, // IID716 - 0x62, 0x44, 0x94, 0x14, 0x31, 0xaf, 0x87, 0x4b, 0x05, 0xa1, // IID717 - 0x62, 0xd4, 0xe0, 0x10, 0x81, 0x84, 0xb5, 0x59, 0x45, 0xb6, 0x68, 0x00, 0x00, 0x00, 0x01, // IID718 - 0x62, 0x94, 0xf8, 0x14, 0x83, 0x84, 0xfd, 0x0b, 0xc5, 0xeb, 0x9a, 0x01, // IID719 - 0x62, 0x9c, 0x84, 0x10, 0x83, 0xa4, 0x68, 0xf2, 0x95, 0x4e, 0xda, 0x01, // IID720 - 0x62, 0xdc, 0xa4, 0x1c, 0x81, 0xa4, 0x24, 0xda, 0xb4, 0x92, 0xf0, 0x00, 0x00, 0x01, 0x00, // IID721 - 0x62, 0xbc, 0xfc, 0x08, 0x69, 0x8c, 0x12, 0xa1, 0x6d, 0xec, 0x46, 0x00, 0x00, 0x00, 0x01, // IID722 - 0x62, 0x14, 0xfc, 0x0c, 0x6b, 0xbc, 0xd1, 0x0d, 0x95, 0x3c, 0x80, 0x10, // IID723 - 0x62, 0x9c, 0xf0, 0x10, 0x83, 0x8c, 0x33, 0x2c, 0xda, 0x4c, 0x1b, 0x01, // IID724 - 0x62, 0x9c, 0xec, 0x1c, 0x81, 0x8c, 0xb1, 0x7b, 0x94, 0x55, 0xa6, 0x00, 0x10, 0x00, 0x00, // IID725 - 0x62, 0xdc, 0xf0, 0x10, 0xc1, 0xa4, 0x6a, 0xa1, 0x0e, 0x4e, 0x95, 0x08, // IID726 - 0x62, 0xfc, 0x98, 0x1c, 0xc1, 0xa4, 0x0e, 0x55, 0xeb, 0x53, 0xbc, 0x02, // IID727 - 0x62, 0xfc, 0x90, 0x10, 0xc1, 0xbc, 0x02, 0x9f, 0xf3, 0x23, 0xa6, 0x04, // IID728 - 0x62, 0xd4, 0xfc, 0x14, 0xc1, 0xbb, 0xec, 0x2c, 0x42, 0xf8, 0x04, // IID729 - 0x62, 0xbc, 0xa8, 0x10, 0xc1, 0xac, 0xdf, 0xec, 0xdc, 0x46, 0xaa, 0x10, // IID730 - 0x62, 0xbc, 0xc0, 0x14, 0xc1, 0xac, 0x68, 0x1d, 0x1a, 0x31, 0x71, 0x02, // IID731 - 0x62, 0xd4, 0xb4, 0x10, 0x81, 0xa9, 0x54, 0xd4, 0xac, 0xf6, 0x00, 0x00, 0x10, 0x00, // IID732 - 0x62, 0xd4, 0xf0, 0x14, 0x81, 0xac, 0x38, 0xa2, 0x6c, 0xd0, 0x55, 0x00, 0x00, 0x10, 0x00, // IID733 - 0x62, 0x94, 0x90, 0x10, 0x81, 0xb4, 0x01, 0x3f, 0xbe, 0x3e, 0xfd, 0x00, 0x00, 0x10, 0x00, // IID734 - 0x62, 0xfc, 0x98, 0x14, 0x83, 0xb4, 0x5e, 0x43, 0x65, 0x62, 0xd2, 0x10, // IID735 - 0x62, 0xd4, 0xcc, 0x10, 0x83, 0xc6, 0x10, // IID736 - 0x62, 0xd4, 0xfc, 0x18, 0x83, 0xc4, 0x10, // IID737 - 0xd5, 0x19, 0x81, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID738 - 0x62, 0xf4, 0xd4, 0x14, 0x81, 0xc3, 0x00, 0x00, 0x01, 0x00, // IID739 - 0x62, 0xf4, 0xfc, 0x1c, 0x81, 0xc3, 0x00, 0x00, 0x01, 0x00, // IID740 - 0x62, 0xdc, 0xbc, 0x14, 0x81, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID741 - 0x62, 0xdc, 0xd4, 0x10, 0x81, 0xe3, 0x00, 0x00, 0x00, 0x01, // IID742 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xe3, 0x00, 0x00, 0x00, 0x01, // IID743 - 0xd5, 0x19, 0x81, 0xe0, 0x00, 0x00, 0x01, 0x00, // IID744 - 0x62, 0xdc, 0x94, 0x1c, 0x81, 0xe7, 0x00, 0x00, 0x10, 0x00, // IID745 - 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xe5, 0x00, 0x00, 0x10, 0x00, // IID746 - 0x62, 0xdc, 0x8c, 0x14, 0x81, 0xe6, 0x00, 0x00, 0x10, 0x00, // IID747 - 0x62, 0x54, 0xfc, 0x08, 0x69, 0xc5, 0x00, 0x00, 0x00, 0x10, // IID748 - 0x62, 0xdc, 0xfc, 0x08, 0x69, 0xc7, 0x00, 0x00, 0x00, 0x10, // IID749 - 0x62, 0x54, 0xfc, 0x08, 0x69, 0xed, 0x00, 0x00, 0x01, 0x00, // IID750 - 0x62, 0x5c, 0xfc, 0x0c, 0x69, 0xf5, 0x00, 0x00, 0x10, 0x00, // IID751 - 0x62, 0xfc, 0xfc, 0x0c, 0x69, 0xc6, 0x00, 0x00, 0x10, 0x00, // IID752 - 0x62, 0x54, 0xfc, 0x0c, 0x69, 0xc0, 0x00, 0x00, 0x00, 0x10, // IID753 - 0x62, 0xd4, 0x8c, 0x10, 0x81, 0xcf, 0x00, 0x10, 0x00, 0x00, // IID754 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xcc, 0x00, 0x10, 0x00, 0x00, // IID755 - 0xd5, 0x19, 0x81, 0xca, 0x00, 0x00, 0x10, 0x00, // IID756 - 0x62, 0xd4, 0xfc, 0x14, 0x81, 0xcc, 0x00, 0x00, 0x00, 0x10, // IID757 - 0x62, 0xd4, 0xfc, 0x1c, 0x81, 0xc9, 0x00, 0x00, 0x00, 0x10, // IID758 - 0x62, 0xfc, 0xc4, 0x14, 0x81, 0xcf, 0x00, 0x01, 0x00, 0x00, // IID759 - 0x62, 0xd4, 0x84, 0x18, 0xc1, 0xd1, 0x10, // IID760 - 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xd0, 0x10, // IID761 - 0xd5, 0x19, 0xd1, 0xd1, // IID762 - 0x62, 0xfc, 0xb4, 0x18, 0xc1, 0xc1, 0x10, // IID763 - 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xc4, 0x10, // IID764 - 0xd5, 0x19, 0xd1, 0xc3, // IID765 - 0x62, 0xdc, 0xdc, 0x14, 0xd1, 0xc7, // IID766 - 0x62, 0xfc, 0xfc, 0x1c, 0xd1, 0xc2, // IID767 - 0x62, 0xdc, 0x9c, 0x14, 0xc1, 0xc4, 0x10, // IID768 - 0x62, 0xfc, 0xac, 0x10, 0xc1, 0xca, 0x10, // IID769 - 0x62, 0xdc, 0xfc, 0x18, 0xc1, 0xc8, 0x10, // IID770 - 0xd5, 0x18, 0xc1, 0xce, 0x10, // IID771 - 0x62, 0xdc, 0xa4, 0x14, 0xd1, 0xcd, // IID772 - 0x62, 0xfc, 0xfc, 0x1c, 0xd1, 0xca, // IID773 - 0x62, 0xfc, 0xd4, 0x14, 0xd1, 0xcd, // IID774 - 0x62, 0xf4, 0x9c, 0x18, 0xc1, 0xe1, 0x02, // IID775 - 0x62, 0xdc, 0xfc, 0x18, 0xc1, 0xe0, 0x02, // IID776 - 0xd5, 0x18, 0xc1, 0xe6, 0x08, // IID777 - 0x62, 0xfc, 0xf4, 0x14, 0xc1, 0xe7, 0x08, // IID778 - 0x62, 0xdc, 0xfc, 0x1c, 0xc1, 0xe3, 0x08, // IID779 - 0x62, 0xfc, 0xc4, 0x14, 0xd1, 0xe7, // IID780 - 0x62, 0xdc, 0xbc, 0x18, 0xc1, 0xf9, 0x10, // IID781 - 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xff, 0x10, // IID782 - 0x49, 0xc1, 0xf9, 0x04, // IID783 - 0x62, 0xd4, 0xcc, 0x14, 0xd1, 0xfd, // IID784 - 0x62, 0xd4, 0xfc, 0x1c, 0xd1, 0xfb, // IID785 - 0x62, 0xd4, 0x9c, 0x1c, 0xc1, 0xfc, 0x02, // IID786 - 0x62, 0xdc, 0xf4, 0x18, 0xc1, 0xe6, 0x08, // IID787 - 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xe3, 0x08, // IID788 - 0x49, 0xc1, 0xe5, 0x02, // IID789 - 0x62, 0xd4, 0xec, 0x14, 0xc1, 0xe3, 0x08, // IID790 - 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xe1, 0x08, // IID791 - 0x62, 0xf4, 0xf4, 0x1c, 0xc1, 0xe1, 0x10, // IID792 - 0x62, 0xfc, 0xac, 0x18, 0xc1, 0xee, 0x04, // IID793 - 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xe9, 0x04, // IID794 - 0x49, 0xc1, 0xec, 0x02, // IID795 - 0x62, 0xdc, 0xac, 0x14, 0xc1, 0xef, 0x08, // IID796 - 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xec, 0x08, // IID797 - 0x62, 0xdc, 0x9c, 0x14, 0xd1, 0xec, // IID798 - 0x62, 0xdc, 0x84, 0x18, 0x81, 0xee, 0x00, 0x00, 0x01, 0x00, // IID799 - 0x62, 0xf4, 0xfc, 0x18, 0x81, 0xe9, 0x00, 0x00, 0x01, 0x00, // IID800 - 0xd5, 0x19, 0x83, 0xea, 0x10, // IID801 - 0x62, 0xd4, 0x9c, 0x1c, 0x83, 0xee, 0x01, // IID802 - 0x62, 0xfc, 0xfc, 0x1c, 0x83, 0xed, 0x01, // IID803 - 0x62, 0xfc, 0xdc, 0x14, 0x81, 0xec, 0x00, 0x00, 0x10, 0x00, // IID804 - 0x62, 0xf4, 0xa4, 0x18, 0x81, 0xf3, 0x00, 0x00, 0x00, 0x01, // IID805 - 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xf7, 0x00, 0x00, 0x00, 0x01, // IID806 - 0xd5, 0x19, 0x81, 0xf7, 0x00, 0x00, 0x00, 0x10, // IID807 - 0x62, 0xdc, 0x94, 0x14, 0x81, 0xf4, 0x00, 0x10, 0x00, 0x00, // IID808 - 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xf3, 0x00, 0x10, 0x00, 0x00, // IID809 - 0x62, 0xf4, 0xec, 0x1c, 0x81, 0xf2, 0x00, 0x00, 0x00, 0x10, // IID810 - 0x48, 0x81, 0xca, 0x00, 0x00, 0x10, 0x00, // IID811 - 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xce, 0x00, 0x00, 0x10, 0x00, // IID812 - 0xd5, 0x19, 0x81, 0xcd, 0x00, 0x00, 0x10, 0x00, // IID813 - 0x62, 0xf4, 0xf4, 0x10, 0x81, 0xc9, 0x00, 0x00, 0x40, 0x00, // IID814 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xc9, 0x00, 0x00, 0x40, 0x00, // IID815 - 0xd5, 0x19, 0x81, 0xcb, 0x00, 0x00, 0x00, 0x40, // IID816 - 0x62, 0xfc, 0xfc, 0x10, 0x81, 0xeb, 0x00, 0x00, 0x40, 0x00, // IID817 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xef, 0x00, 0x00, 0x40, 0x00, // IID818 - 0xd5, 0x19, 0x81, 0xea, 0x00, 0x00, 0x04, 0x00, // IID819 - 0x62, 0xfc, 0xf4, 0x14, 0x81, 0xee, 0x00, 0x00, 0x00, 0x40, // IID820 - 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xea, 0x00, 0x00, 0x00, 0x40, // IID821 - 0x62, 0xfc, 0xc4, 0x14, 0x81, 0xef, 0x00, 0x00, 0x00, 0x10, // IID822 - 0x62, 0x4c, 0x90, 0x18, 0x03, 0xb4, 0x58, 0x3b, 0x3a, 0xea, 0x56, // IID823 - 0x62, 0x1c, 0x90, 0x14, 0x03, 0xbc, 0xda, 0xa8, 0xc6, 0xee, 0xb4, // IID824 - 0x62, 0x4c, 0x9c, 0x18, 0x23, 0xb7, 0x8c, 0xc3, 0xef, 0xb9, // IID825 - 0x62, 0x3c, 0xa0, 0x14, 0x23, 0x94, 0x4e, 0xe5, 0xbe, 0x1e, 0x6a, // IID826 - 0x62, 0x44, 0x88, 0x10, 0x0b, 0x94, 0x93, 0xd7, 0x00, 0x60, 0xd4, // IID827 - 0x62, 0x7c, 0xb0, 0x1c, 0x0b, 0xa4, 0x0a, 0xf6, 0x59, 0x48, 0x0b, // IID828 - 0x62, 0xcc, 0xec, 0x18, 0xaf, 0x8c, 0x90, 0xd8, 0x4c, 0x28, 0x3d, // IID829 - 0x62, 0x0c, 0x94, 0x14, 0xaf, 0x94, 0x66, 0x24, 0x31, 0x81, 0x6e, // IID830 - 0x62, 0x7c, 0xe4, 0x18, 0x2b, 0xae, 0x62, 0xd7, 0xd5, 0x8f, // IID831 - 0x62, 0x4c, 0xc4, 0x14, 0x2b, 0xac, 0x11, 0x13, 0x58, 0xad, 0x9d, // IID832 - 0x62, 0xac, 0xbc, 0x18, 0x33, 0x94, 0xb3, 0x69, 0x59, 0x40, 0xf1, // IID833 - 0x62, 0x4c, 0xac, 0x1c, 0x33, 0xa2, 0xca, 0x81, 0x83, 0x16, // IID834 - 0x62, 0xc4, 0xf4, 0x18, 0x03, 0xd0, // IID835 - 0x49, 0x03, 0xce, // IID836 - 0x62, 0x7c, 0xc4, 0x14, 0x03, 0xd0, // IID837 - 0x62, 0x5c, 0xa4, 0x1c, 0x03, 0xd8, // IID838 - 0x62, 0xe4, 0xb5, 0x18, 0x66, 0xd2, // IID839 - 0x66, 0x4d, 0x0f, 0x38, 0xf6, 0xc7, // IID840 - 0x62, 0xcc, 0x86, 0x18, 0x66, 0xf2, // IID841 - 0xf3, 0x4c, 0x0f, 0x38, 0xf6, 0xda, // IID842 - 0x62, 0xfc, 0xe4, 0x10, 0x23, 0xd6, // IID843 - 0xd5, 0x5c, 0x23, 0xe9, // IID844 - 0x62, 0x44, 0xc4, 0x14, 0x23, 0xdf, // IID845 - 0x62, 0x54, 0xb4, 0x1c, 0x23, 0xcd, // IID846 - 0x62, 0x7c, 0xec, 0x10, 0xaf, 0xf8, // IID847 - 0xd5, 0x98, 0xaf, 0xc9, // IID848 - 0x62, 0x7c, 0xc4, 0x14, 0xaf, 0xe4, // IID849 - 0x62, 0x54, 0xac, 0x1c, 0xaf, 0xd1, // IID850 - 0x62, 0xc4, 0xec, 0x18, 0x0b, 0xde, // IID851 - 0x49, 0x0b, 0xcd, // IID852 - 0x62, 0x4c, 0xb4, 0x1c, 0x0b, 0xcd, // IID853 - 0x62, 0xdc, 0xec, 0x1c, 0x0b, 0xd1, // IID854 - 0x62, 0x7c, 0xc4, 0x10, 0x2b, 0xc0, // IID855 - 0x4d, 0x2b, 0xed, // IID856 - 0x62, 0x54, 0xe4, 0x14, 0x2b, 0xe7, // IID857 - 0x62, 0x74, 0xb4, 0x1c, 0x2b, 0xca, // IID858 - 0x62, 0xcc, 0x94, 0x18, 0x33, 0xc7, // IID859 - 0xd5, 0x59, 0x33, 0xce, // IID860 - 0x62, 0x6c, 0xe4, 0x14, 0x33, 0xf4, // IID861 - 0x62, 0x44, 0x84, 0x14, 0x33, 0xfd, // IID862 - 0x62, 0x54, 0xcc, 0x10, 0x24, 0xea, 0x04, // IID863 - 0xd5, 0xd9, 0xa4, 0xe8, 0x10, // IID864 - 0x62, 0x44, 0xdc, 0x14, 0x24, 0xdd, 0x10, // IID865 - 0x62, 0xcc, 0x84, 0x14, 0x24, 0xdf, 0x02, // IID866 - 0x62, 0x7c, 0x8c, 0x10, 0x2c, 0xdc, 0x08, // IID867 - 0x4c, 0x0f, 0xac, 0xfa, 0x01, // IID868 - 0x62, 0x5c, 0x9c, 0x14, 0x2c, 0xf6, 0x02, // IID869 - 0x62, 0xec, 0xdc, 0x14, 0x2c, 0xc4, 0x01, // IID870 - 0x62, 0xcc, 0xd4, 0x10, 0x40, 0xcc, // IID871 - 0xd5, 0x9d, 0x40, 0xfe, // IID872 - 0x62, 0x54, 0xf4, 0x18, 0x41, 0xff, // IID873 - 0x49, 0x0f, 0x41, 0xcd, // IID874 - 0x62, 0x4c, 0xec, 0x18, 0x42, 0xd2, // IID875 - 0xd5, 0xcd, 0x42, 0xe7, // IID876 - 0x62, 0xf4, 0xbc, 0x18, 0x43, 0xd1, // IID877 - 0x48, 0x0f, 0x43, 0xc9, // IID878 - 0x62, 0x54, 0xac, 0x18, 0x44, 0xe9, // IID879 - 0xd5, 0x9d, 0x44, 0xf3, // IID880 - 0x62, 0xc4, 0xa4, 0x18, 0x45, 0xf9, // IID881 - 0x4c, 0x0f, 0x45, 0xda, // IID882 - 0x62, 0x5c, 0x84, 0x10, 0x46, 0xf1, // IID883 - 0xd5, 0xc9, 0x46, 0xe4, // IID884 - 0x62, 0x5c, 0xec, 0x18, 0x47, 0xd4, // IID885 - 0xd5, 0x9c, 0x47, 0xc1, // IID886 - 0x62, 0x6c, 0xf4, 0x18, 0x48, 0xf7, // IID887 - 0xd5, 0xdc, 0x48, 0xd2, // IID888 - 0x62, 0xfc, 0xec, 0x18, 0x49, 0xda, // IID889 - 0xd5, 0xc9, 0x49, 0xed, // IID890 - 0x62, 0x4c, 0xa4, 0x10, 0x4a, 0xe3, // IID891 - 0xd5, 0x9d, 0x4a, 0xde, // IID892 - 0x62, 0xec, 0xf4, 0x18, 0x4b, 0xea, // IID893 - 0xd5, 0x99, 0x4b, 0xcd, // IID894 - 0x62, 0xc4, 0xec, 0x18, 0x4c, 0xec, // IID895 - 0xd5, 0x99, 0x4c, 0xd2, // IID896 - 0x62, 0xfc, 0xf4, 0x10, 0x4d, 0xde, // IID897 - 0x49, 0x0f, 0x4d, 0xd3, // IID898 - 0x62, 0x54, 0xec, 0x18, 0x4e, 0xf0, // IID899 - 0x4d, 0x0f, 0x4e, 0xf0, // IID900 - 0x62, 0x6c, 0xb4, 0x10, 0x4f, 0xed, // IID901 - 0xd5, 0xdd, 0x4f, 0xd6, // IID902 - 0x62, 0x84, 0xbc, 0x10, 0x40, 0xac, 0x5d, 0x1e, 0x52, 0x9c, 0x43, // IID903 - 0x62, 0xcc, 0xa0, 0x18, 0x41, 0x94, 0x05, 0x7f, 0x12, 0x32, 0x06, // IID904 - 0x62, 0x14, 0xf8, 0x10, 0x42, 0x84, 0x50, 0xef, 0x3d, 0x63, 0x10, // IID905 - 0x62, 0x7c, 0x94, 0x18, 0x43, 0xb2, 0xc8, 0x61, 0x09, 0xab, // IID906 - 0x62, 0x1c, 0x98, 0x18, 0x44, 0x84, 0x57, 0x86, 0xbb, 0xe1, 0x85, // IID907 - 0xd5, 0xde, 0x45, 0xac, 0x9b, 0x2e, 0xd2, 0x27, 0xca, // IID908 - 0x62, 0x8c, 0xf0, 0x18, 0x46, 0x94, 0x21, 0xa0, 0x64, 0xbe, 0x30, // IID909 - 0x62, 0x54, 0x98, 0x10, 0x47, 0xa4, 0x42, 0x06, 0x01, 0x47, 0xdd, // IID910 - 0x62, 0x34, 0xa4, 0x18, 0x48, 0x84, 0xdb, 0x96, 0x9e, 0xcc, 0x25, // IID911 - 0x62, 0x44, 0x9c, 0x18, 0x49, 0x9b, 0x02, 0x8f, 0xd2, 0xf3, // IID912 - 0x62, 0x6c, 0xbc, 0x18, 0x4a, 0x94, 0x5b, 0x16, 0x48, 0x92, 0xb7, // IID913 - 0x62, 0x54, 0x88, 0x10, 0x4b, 0x94, 0xd6, 0x84, 0x48, 0x88, 0x14, // IID914 - 0x62, 0x1c, 0xa4, 0x10, 0x4c, 0x84, 0xb5, 0xa8, 0xb7, 0x92, 0x00, // IID915 - 0x62, 0x6c, 0x8c, 0x18, 0x4d, 0xa4, 0x13, 0x45, 0x2d, 0x9c, 0x00, // IID916 - 0x62, 0x74, 0xb0, 0x10, 0x4e, 0x84, 0x91, 0x6b, 0xc8, 0x55, 0x66, // IID917 - 0x62, 0x84, 0xe0, 0x10, 0x4f, 0xac, 0x0a, 0xf5, 0xbc, 0xfa, 0xef, // IID918 + 0x62, 0x6c, 0x74, 0x18, 0x0b, 0xc6, // IID432 + 0xd5, 0x10, 0x0b, 0xcb, // IID433 + 0xd5, 0x55, 0x0b, 0xdb, // IID434 + 0x62, 0x54, 0x04, 0x14, 0x0b, 0xcd, // IID435 + 0x62, 0x6c, 0x04, 0x14, 0x0b, 0xff, // IID436 + 0x62, 0xec, 0x64, 0x14, 0x0b, 0xcb, // IID437 + 0x62, 0x6c, 0x5c, 0x10, 0xa5, 0xc0, // IID438 + 0x44, 0x0f, 0xa5, 0xe2, // IID439 + 0x62, 0x44, 0x14, 0x14, 0xa5, 0xf9, // IID440 + 0x62, 0xec, 0x74, 0x14, 0xa5, 0xe1, // IID441 + 0x62, 0xc4, 0x5c, 0x10, 0xad, 0xd7, // IID442 + 0x44, 0x0f, 0xad, 0xe1, // IID443 + 0x62, 0xc4, 0x0c, 0x1c, 0xad, 0xf9, // IID444 + 0x62, 0x7c, 0x64, 0x14, 0xad, 0xeb, // IID445 + 0x62, 0x4c, 0x0c, 0x10, 0x2b, 0xdb, // IID446 + 0x41, 0x2b, 0xd3, // IID447 + 0x62, 0x5c, 0x04, 0x1c, 0x2b, 0xd8, // IID448 + 0x62, 0x5c, 0x0c, 0x1c, 0x2b, 0xf1, // IID449 + 0x62, 0xc4, 0x04, 0x10, 0x33, 0xc4, // IID450 + 0xd5, 0x41, 0x33, 0xe6, // IID451 + 0xd5, 0x45, 0x33, 0xf5, // IID452 + 0x62, 0xec, 0x3c, 0x14, 0x33, 0xc9, // IID453 + 0x62, 0x6c, 0x2c, 0x14, 0x33, 0xd5, // IID454 + 0x62, 0x54, 0x24, 0x1c, 0x33, 0xeb, // IID455 + 0x62, 0xcc, 0x24, 0x10, 0x24, 0xe9, 0x04, // IID456 + 0xd5, 0x94, 0xa4, 0xd6, 0x04, // IID457 + 0x62, 0x44, 0x54, 0x14, 0x24, 0xc7, 0x10, // IID458 + 0x62, 0xe4, 0x6c, 0x1c, 0x24, 0xda, 0x01, // IID459 + 0x62, 0x54, 0x44, 0x10, 0x2c, 0xc5, 0x10, // IID460 + 0xd5, 0xd1, 0xac, 0xf2, 0x01, // IID461 + 0x62, 0x44, 0x3c, 0x14, 0x2c, 0xf1, 0x10, // IID462 + 0x62, 0x7c, 0x64, 0x14, 0x2c, 0xc3, 0x04, // IID463 + 0x62, 0x6c, 0x0c, 0x10, 0x40, 0xd1, // IID464 + 0xd5, 0x95, 0x40, 0xf2, // IID465 + 0x62, 0xcc, 0x3c, 0x10, 0x41, 0xdd, // IID466 + 0xd5, 0xd4, 0x41, 0xcc, // IID467 + 0x62, 0x54, 0x24, 0x18, 0x42, 0xd6, // IID468 + 0xd5, 0xd5, 0x42, 0xf1, // IID469 + 0x62, 0xcc, 0x14, 0x18, 0x43, 0xf3, // IID470 + 0xd5, 0xd1, 0x43, 0xc0, // IID471 + 0x62, 0x5c, 0x1c, 0x10, 0x44, 0xee, // IID472 + 0xd5, 0xd5, 0x44, 0xf0, // IID473 + 0x62, 0xcc, 0x54, 0x10, 0x45, 0xe7, // IID474 + 0xd5, 0x94, 0x45, 0xc0, // IID475 + 0x62, 0x6c, 0x04, 0x18, 0x46, 0xd6, // IID476 + 0xd5, 0xc4, 0x46, 0xfa, // IID477 + 0x62, 0x54, 0x24, 0x10, 0x47, 0xc2, // IID478 + 0xd5, 0xc1, 0x47, 0xd3, // IID479 + 0x62, 0xfc, 0x24, 0x10, 0x48, 0xdd, // IID480 + 0xd5, 0x95, 0x48, 0xe7, // IID481 + 0x62, 0xfc, 0x1c, 0x18, 0x49, 0xd2, // IID482 + 0xd5, 0xd0, 0x49, 0xd3, // IID483 + 0x62, 0xec, 0x7c, 0x10, 0x4a, 0xe7, // IID484 + 0xd5, 0xd0, 0x4a, 0xd0, // IID485 + 0x62, 0x4c, 0x64, 0x18, 0x4b, 0xfe, // IID486 + 0xd5, 0xd5, 0x4b, 0xfd, // IID487 + 0x62, 0x44, 0x1c, 0x10, 0x4c, 0xca, // IID488 + 0xd5, 0xd4, 0x4c, 0xc4, // IID489 + 0x62, 0xdc, 0x7c, 0x10, 0x4d, 0xd2, // IID490 + 0xd5, 0xd5, 0x4d, 0xe4, // IID491 + 0x62, 0xcc, 0x34, 0x18, 0x4e, 0xe0, // IID492 + 0xd5, 0xd5, 0x4e, 0xc5, // IID493 + 0x62, 0x44, 0x44, 0x10, 0x4f, 0xdf, // IID494 + 0xd5, 0x94, 0x4f, 0xe2, // IID495 + 0x62, 0x5c, 0x64, 0x10, 0x40, 0x8c, 0x4f, 0x43, 0x67, 0x41, 0xfd, // IID496 + 0xd5, 0xb6, 0x40, 0x84, 0x45, 0xcb, 0xa0, 0xe6, 0x41, // IID497 + 0x62, 0x3c, 0x40, 0x10, 0x41, 0xbc, 0xf3, 0xd3, 0x01, 0x52, 0xaa, // IID498 + 0xd5, 0x91, 0x41, 0x94, 0x13, 0xcb, 0xd8, 0x5e, 0xe5, // IID499 + 0x62, 0x0c, 0x64, 0x18, 0x42, 0xac, 0x27, 0x46, 0x22, 0xd4, 0x0b, // IID500 + 0xd5, 0xf0, 0x42, 0xac, 0x6b, 0xe8, 0x77, 0xae, 0xbe, // IID501 + 0x62, 0x2c, 0x44, 0x10, 0x43, 0xac, 0x8e, 0x28, 0x24, 0x52, 0xca, // IID502 + 0xd5, 0xd1, 0x43, 0x91, 0xc3, 0x84, 0x21, 0x63, // IID503 + 0x62, 0x3c, 0x14, 0x10, 0x44, 0xac, 0x2a, 0x3a, 0x15, 0x8d, 0xc6, // IID504 + 0xd5, 0xc7, 0x44, 0xac, 0xcc, 0x2e, 0x20, 0x73, 0x99, // IID505 + 0x62, 0xc4, 0x30, 0x10, 0x45, 0x94, 0xb1, 0x1f, 0xc9, 0x6a, 0x7f, // IID506 + 0xd5, 0xd5, 0x45, 0xa6, 0x94, 0x65, 0x2e, 0x56, // IID507 + 0x62, 0x44, 0x20, 0x10, 0x46, 0x84, 0xa7, 0x49, 0xc7, 0x9a, 0xb9, // IID508 + 0xd5, 0xd7, 0x46, 0x8c, 0xda, 0x57, 0xed, 0xc7, 0xa6, // IID509 + 0x62, 0xec, 0x74, 0x18, 0x47, 0xa0, 0x15, 0x5c, 0x76, 0xec, // IID510 + 0xd5, 0x93, 0x47, 0x9c, 0x05, 0x15, 0x26, 0x02, 0x1d, // IID511 + 0x62, 0x14, 0x68, 0x18, 0x48, 0xb4, 0x64, 0x6f, 0xa5, 0x8d, 0xae, // IID512 + 0xd5, 0xf4, 0x48, 0x84, 0x51, 0x1c, 0x3b, 0xda, 0xe8, // IID513 + 0xd5, 0xa1, 0x49, 0x8c, 0xbf, 0x31, 0xd3, 0x2d, 0x94, // IID514 + 0xd5, 0xc7, 0x49, 0x84, 0x57, 0xf7, 0xc2, 0x04, 0x80, // IID515 + 0x62, 0xd4, 0x40, 0x10, 0x4a, 0x8c, 0x3b, 0x85, 0xc5, 0x38, 0x37, // IID516 + 0xd5, 0xd7, 0x4a, 0x84, 0x16, 0xa8, 0x15, 0xcc, 0x0f, // IID517 + 0x62, 0x44, 0x08, 0x18, 0x4b, 0x94, 0x6e, 0x61, 0x31, 0xcf, 0xbb, // IID518 + 0xd5, 0x95, 0x4b, 0x94, 0x24, 0x9f, 0xc5, 0xd7, 0x03, // IID519 + 0x62, 0x84, 0x2c, 0x18, 0x4c, 0xac, 0xc0, 0xb4, 0x84, 0x65, 0x4a, // IID520 + 0xd5, 0xf4, 0x4c, 0x94, 0xe3, 0xef, 0x60, 0xc6, 0x47, // IID521 + 0x62, 0x74, 0x2c, 0x10, 0x4d, 0x91, 0x97, 0x7a, 0x97, 0x61, // IID522 + 0xd5, 0xe5, 0x4d, 0xb4, 0xdf, 0xcb, 0x01, 0xc6, 0x53, // IID523 + 0x62, 0x7c, 0x0c, 0x18, 0x4e, 0x89, 0x1e, 0x11, 0x93, 0xa9, // IID524 + 0xd5, 0xb5, 0x4e, 0xbc, 0x23, 0x92, 0x47, 0x16, 0x76, // IID525 + 0x62, 0x14, 0x24, 0x10, 0x4f, 0xb4, 0xa9, 0xd7, 0x52, 0x57, 0x0f, // IID526 + 0x44, 0x0f, 0x4f, 0xa4, 0xcb, 0x3a, 0x4b, 0xfe, 0xaa, // IID527 + 0xd5, 0x5d, 0x13, 0xf7, // IID528 + 0x4c, 0x3b, 0xe2, // IID529 + 0xd5, 0xd9, 0xaf, 0xe8, // IID530 + 0xf3, 0xd5, 0x9d, 0xb8, 0xc9, // IID531 + 0x4d, 0x1b, 0xc4, // IID532 + 0xd5, 0x5d, 0x2b, 0xf8, // IID533 + 0xf3, 0xd5, 0x9c, 0xbc, 0xd0, // IID534 + 0xf3, 0xd5, 0xd8, 0xbd, 0xe5, // IID535 + 0xd5, 0x18, 0x03, 0xd1, // IID536 + 0x4d, 0x23, 0xf5, // IID537 + 0xd5, 0x59, 0x0b, 0xe0, // IID538 + 0xd5, 0x58, 0x33, 0xee, // IID539 + 0xd5, 0x1d, 0x8b, 0xe3, // IID540 + 0xd5, 0xc8, 0xbc, 0xfa, // IID541 + 0xd5, 0xdd, 0xbd, 0xfc, // IID542 + 0xd5, 0xcd, 0xa3, 0xc8, // IID543 + 0xd5, 0x48, 0x87, 0xeb, // IID544 + 0xd5, 0x58, 0x85, 0xff, // IID545 + 0xd5, 0x1c, 0x01, 0x93, 0x5f, 0xc1, 0xf2, 0xe7, // IID546 + 0xd5, 0x6d, 0x21, 0x8c, 0x4b, 0x18, 0x94, 0x68, 0x87, // IID547 + 0xd5, 0x2e, 0x39, 0xac, 0xe3, 0x02, 0x21, 0xf7, 0x35, // IID548 + 0xd5, 0x49, 0x09, 0xa8, 0xef, 0xaf, 0xb9, 0xcb, // IID549 + 0xd5, 0x58, 0x31, 0x93, 0x23, 0xdd, 0xb4, 0xbf, // IID550 + 0xd5, 0x3e, 0x29, 0xb4, 0xdf, 0xba, 0xd9, 0x72, 0xbd, // IID551 + 0xd5, 0x4d, 0x89, 0xa4, 0x89, 0x9f, 0xe9, 0x9e, 0x8d, // IID552 + 0xd5, 0xb9, 0xc1, 0x9c, 0xa8, 0x90, 0xe9, 0x6b, 0x3a, // IID553 + 0xd5, 0x1a, 0x81, 0xa4, 0x16, 0xdd, 0x8b, 0xef, 0x07, 0x00, 0x00, 0x10, 0x00, // IID554 + 0xd5, 0x2b, 0x81, 0x84, 0x25, 0x4f, 0x76, 0xb8, 0x8a, 0x00, 0x00, 0x01, 0x00, // IID555 + 0x49, 0x81, 0xba, 0x26, 0x57, 0x2d, 0xf4, 0x00, 0x00, 0x00, 0x10, // IID556 + 0xd5, 0x1a, 0xc1, 0xbc, 0x77, 0xf5, 0xd9, 0x16, 0x6a, 0x04, // IID557 + 0xd5, 0x28, 0xc1, 0xa4, 0x69, 0x1e, 0xac, 0x66, 0x5f, 0x08, // IID558 + 0xd5, 0x28, 0x81, 0x9c, 0xf1, 0xb4, 0x6a, 0x73, 0xfb, 0x00, 0x00, 0x00, 0x10, // IID559 + 0xd5, 0x3a, 0xc1, 0xac, 0x35, 0x0b, 0x5b, 0x40, 0x0e, 0x08, // IID560 + 0xd5, 0x3a, 0x81, 0xac, 0xeb, 0xb5, 0xfb, 0x9d, 0x88, 0x00, 0x10, 0x00, 0x00, // IID561 + 0xd5, 0x1b, 0x83, 0xb4, 0x56, 0xdd, 0x7c, 0x86, 0xe6, 0x10, // IID562 + 0xd5, 0x2a, 0x81, 0x8c, 0xc2, 0x16, 0xb3, 0xd9, 0x18, 0x00, 0x10, 0x00, 0x00, // IID563 + 0x48, 0xc7, 0x83, 0xb3, 0xf8, 0xa7, 0xcf, 0x00, 0x01, 0x00, 0x00, // IID564 + 0xd5, 0x39, 0xf7, 0x84, 0xec, 0xc4, 0xfd, 0xa0, 0x65, 0x00, 0x00, 0x00, 0xf0, // IID565 + 0xd5, 0x69, 0x03, 0xbc, 0x13, 0xf4, 0x0e, 0xe5, 0xe2, // IID566 + 0xd5, 0x5a, 0x23, 0xb4, 0x62, 0x38, 0x1c, 0x5f, 0x1a, // IID567 + 0xd5, 0x79, 0x3b, 0xbc, 0x1e, 0x81, 0xd0, 0x6e, 0xc1, // IID568 + 0xf3, 0xd5, 0xcc, 0xbd, 0xa9, 0xe4, 0xfb, 0xe3, 0x12, // IID569 + 0xd5, 0x3c, 0x0b, 0xb4, 0xad, 0x42, 0x30, 0xd7, 0x00, // IID570 + 0xd5, 0x7e, 0x13, 0xbc, 0xb9, 0x12, 0xe9, 0xbd, 0x0a, // IID571 + 0xd5, 0xeb, 0xaf, 0xa4, 0x1d, 0xe1, 0x03, 0x24, 0xa7, // IID572 + 0xf3, 0xd5, 0x98, 0xb8, 0x9e, 0xdd, 0x93, 0x39, 0x8d, // IID573 + 0xd5, 0x4d, 0x1b, 0x91, 0x09, 0xba, 0x4a, 0x33, // IID574 + 0xd5, 0x2f, 0x2b, 0x8c, 0xf1, 0xfe, 0x9e, 0x65, 0xde, // IID575 + 0xf3, 0xd5, 0xdc, 0xbc, 0x8c, 0x24, 0x4f, 0x45, 0xce, 0xde, // IID576 + 0xd5, 0x79, 0x33, 0x84, 0x44, 0xb9, 0x83, 0xc4, 0x48, // IID577 + 0xd5, 0x6d, 0x8b, 0xb4, 0x01, 0xb1, 0x17, 0x73, 0xf7, // IID578 + 0xd5, 0x3f, 0x8d, 0x9c, 0xae, 0xd0, 0x8f, 0xeb, 0x3e, // IID579 + 0xf2, 0xd5, 0xdf, 0x2c, 0x94, 0xd5, 0x2e, 0x82, 0xf4, 0x3e, // IID580 + 0xd5, 0x7c, 0x87, 0xac, 0xa3, 0x47, 0xc2, 0xf0, 0xc0, // IID581 + 0xd5, 0x3d, 0x85, 0x84, 0x26, 0x17, 0x6a, 0xb5, 0x15, // IID582 + 0xd5, 0x19, 0x81, 0xc2, 0x00, 0x10, 0x00, 0x00, // IID583 + 0xd5, 0x18, 0x83, 0xe4, 0x10, // IID584 + 0xd5, 0x18, 0x81, 0xd7, 0x00, 0x00, 0x10, 0x00, // IID585 + 0x49, 0x81, 0xfc, 0x00, 0x10, 0x00, 0x00, // IID586 + 0x48, 0xc1, 0xd1, 0x04, // IID587 + 0x49, 0xd1, 0xde, // IID588 + 0xd5, 0x18, 0xc1, 0xc7, 0x02, // IID589 + 0x49, 0xc1, 0xcc, 0x04, // IID590 + 0x49, 0xc1, 0xfa, 0x04, // IID591 + 0xd5, 0x18, 0xc1, 0xe4, 0x04, // IID592 + 0x48, 0x81, 0xd9, 0x00, 0x00, 0x10, 0x00, // IID593 + 0xd5, 0x18, 0xc1, 0xe7, 0x10, // IID594 + 0xd5, 0x19, 0xc1, 0xeb, 0x02, // IID595 + 0x48, 0x81, 0xe9, 0x00, 0x00, 0x01, 0x00, // IID596 + 0x49, 0x81, 0xf1, 0x00, 0x00, 0x10, 0x00, // IID597 + 0xd5, 0x18, 0xc7, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID598 + 0xd5, 0x19, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, // IID599 + 0xd5, 0x98, 0xba, 0xe2, 0x40, // IID600 + 0xd5, 0x19, 0xf7, 0xc5, 0x00, 0xf0, 0xff, 0xff, // IID601 + 0xd5, 0x19, 0x81, 0xce, 0x00, 0x00, 0x00, 0x04, // IID602 + 0xd5, 0x19, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x10, // IID603 + 0xd5, 0xfe, 0x40, 0xb4, 0xb9, 0xf0, 0x92, 0xff, 0x47, // IID604 + 0xd5, 0xbf, 0x41, 0x8c, 0x60, 0xc0, 0x04, 0x49, 0x38, // IID605 + 0xd5, 0xfa, 0x42, 0xbc, 0xc7, 0x9a, 0xed, 0x80, 0xe6, // IID606 + 0xd5, 0xbf, 0x43, 0x8c, 0x35, 0xc8, 0x49, 0x5d, 0x2b, // IID607 + 0xd5, 0xca, 0x44, 0x84, 0x7b, 0x81, 0x93, 0x37, 0x22, // IID608 + 0x4d, 0x0f, 0x45, 0x83, 0xa0, 0x67, 0x9d, 0x04, // IID609 + 0xd5, 0xfc, 0x46, 0xa4, 0x80, 0x57, 0xe2, 0x6b, 0xa1, // IID610 + 0xd5, 0xda, 0x47, 0x9c, 0x02, 0x14, 0xaa, 0xa1, 0xf5, // IID611 + 0xd5, 0xfe, 0x48, 0xa4, 0x61, 0xf4, 0xdd, 0x9b, 0xcd, // IID612 + 0xd5, 0xef, 0x49, 0xbc, 0x7e, 0x5a, 0xbc, 0x01, 0x50, // IID613 + 0xd5, 0xb8, 0x4a, 0x9c, 0x8a, 0x87, 0xdc, 0x90, 0xd7, // IID614 + 0xd5, 0xd8, 0x4b, 0x8c, 0x24, 0xc8, 0x07, 0xb6, 0xaa, // IID615 + 0xd5, 0xef, 0x4c, 0xb4, 0x61, 0x0d, 0xf3, 0x4f, 0xda, // IID616 + 0xd5, 0xc9, 0x4d, 0x99, 0xd4, 0x0d, 0x54, 0xd5, // IID617 + 0x4a, 0x0f, 0x4e, 0x9c, 0x61, 0x34, 0x97, 0xd2, 0xbc, // IID618 + 0xd5, 0xa9, 0x4f, 0x9c, 0xcf, 0xa2, 0xa9, 0x68, 0xd4, // IID619 + 0xd5, 0x11, 0xff, 0xd0, // IID620 + 0x49, 0xf7, 0xf1, // IID621 + 0xd5, 0x19, 0xf7, 0xfc, // IID622 + 0x48, 0xf7, 0xea, // IID623 + 0xd5, 0x19, 0xf7, 0xe7, // IID624 + 0x49, 0xf7, 0xdc, // IID625 + 0x49, 0xf7, 0xd4, // IID626 + 0xd5, 0x19, 0xd3, 0xc0, // IID627 + 0xd5, 0x19, 0xd3, 0xcc, // IID628 + 0x49, 0xd3, 0xfb, // IID629 + 0xd5, 0x19, 0xd3, 0xe3, // IID630 + 0xd5, 0x18, 0xd3, 0xe7, // IID631 + 0xd5, 0x18, 0xd3, 0xe9, // IID632 + 0xd5, 0x18, 0xff, 0xc0, // IID633 + 0x49, 0xff, 0xcc, // IID634 + 0xd5, 0x18, 0x57, // IID635 + 0xd5, 0x19, 0x58, // IID636 + 0xd5, 0x12, 0xff, 0x94, 0x32, 0xce, 0x62, 0x9c, 0x99, // IID637 + 0xd5, 0x19, 0xf7, 0xa0, 0xdf, 0xdb, 0xf5, 0x99, // IID638 + 0xd5, 0x29, 0xf7, 0x9c, 0x16, 0x6e, 0x93, 0xf3, 0x40, // IID639 + 0x4b, 0xd3, 0xbc, 0x2a, 0x72, 0xcb, 0x04, 0x7d, // IID640 + 0xd5, 0x1a, 0xd3, 0xa4, 0xda, 0x24, 0x4b, 0x89, 0xde, // IID641 + 0x49, 0xd3, 0xac, 0x4d, 0x80, 0xaa, 0x96, 0x79, // IID642 + 0x49, 0xff, 0x86, 0x2a, 0xd0, 0xc2, 0x67, // IID643 + 0xd5, 0x3a, 0xff, 0x8c, 0x16, 0xc0, 0x62, 0x4f, 0x22, // IID644 + 0xd5, 0x19, 0x69, 0x94, 0x5f, 0x10, 0xbb, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x01, // IID645 + 0xd5, 0x59, 0x69, 0xef, 0x00, 0x10, 0x00, 0x00, // IID646 + 0xd5, 0xc8, 0xa4, 0xdb, 0x01, // IID647 + 0xd5, 0xc9, 0xac, 0xfb, 0x04, // IID648 + 0x62, 0xfc, 0x0c, 0x10, 0x8f, 0xc0, // IID649 + 0x62, 0xfc, 0xe4, 0x18, 0x8f, 0xc1, // IID650 + 0x62, 0xfc, 0x0c, 0x10, 0xff, 0xf4, // IID651 + 0x62, 0xd4, 0x84, 0x10, 0xff, 0xf0, // IID652 + 0xd5, 0xcf, 0xb6, 0xa4, 0x30, 0x7a, 0xe6, 0x9a, 0x46, // IID653 + 0xd5, 0xad, 0xb7, 0xb4, 0x90, 0xfe, 0x61, 0x96, 0xb7, // IID654 + 0xd5, 0xc8, 0xbe, 0xab, 0x95, 0x1f, 0x25, 0x9b, // IID655 + 0xd5, 0xd9, 0xbf, 0x9c, 0x9f, 0x19, 0x88, 0x31, 0x60, // IID656 + 0xd5, 0xcd, 0xb6, 0xf5, // IID657 + 0xd5, 0xdc, 0xb7, 0xf2, // IID658 + 0xd5, 0xc9, 0xbe, 0xdf, // IID659 + 0xd5, 0xd8, 0xbf, 0xe0, // IID660 + 0xd5, 0xcd, 0xb1, 0xa4, 0xdb, 0x9d, 0x47, 0xc3, 0x0f, // IID661 + 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xfc, // IID662 + 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xfe, // IID663 + 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xf6, // IID664 + 0x62, 0xd4, 0xfc, 0x0c, 0xf7, 0xf3, // IID665 + 0x62, 0xf4, 0xfc, 0x08, 0xf7, 0xe9, // IID666 + 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xec, // IID667 + 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xe5, // IID668 + 0x62, 0xd4, 0xfc, 0x0c, 0xf7, 0xe5, // IID669 + 0x62, 0x9c, 0xfc, 0x08, 0xf7, 0xa4, 0xba, 0x6e, 0xce, 0xa1, 0x70, // IID670 + 0x62, 0xdc, 0xf8, 0x0c, 0xf7, 0xa4, 0x58, 0xa4, 0x7a, 0x8f, 0xe9, // IID671 + 0x62, 0x5c, 0xfc, 0x08, 0xaf, 0xd3, // IID672 + 0xd5, 0x18, 0xf7, 0xe9, // IID673 + 0x62, 0xfc, 0xfc, 0x0c, 0xaf, 0xd6, // IID674 + 0x62, 0xf4, 0xfc, 0x0c, 0xaf, 0xdb, // IID675 + 0x62, 0x44, 0xfc, 0x08, 0xf5, 0xe7, // IID676 + 0x62, 0x54, 0xfc, 0x08, 0xf5, 0xff, // IID677 + 0x62, 0xd4, 0xfc, 0x0c, 0xf5, 0xdc, // IID678 + 0x62, 0xf4, 0xfc, 0x0c, 0xf5, 0xdb, // IID679 + 0x62, 0xd4, 0xac, 0x10, 0xf7, 0xdb, // IID680 + 0xd5, 0x18, 0xf7, 0xd9, // IID681 + 0x62, 0xdc, 0xec, 0x1c, 0xf7, 0xdf, // IID682 + 0x62, 0xdc, 0xa4, 0x14, 0xf7, 0xdb, // IID683 + 0x62, 0xd4, 0x84, 0x10, 0xf7, 0xd7, // IID684 + 0xd5, 0x18, 0xf7, 0xd5, // IID685 + 0x62, 0xdc, 0xfc, 0x08, 0x88, 0xd8, // IID686 + 0x62, 0x4c, 0xfc, 0x08, 0x88, 0xe4, // IID687 + 0x62, 0xcc, 0xfc, 0x0c, 0x88, 0xfb, // IID688 + 0x62, 0x54, 0xfc, 0x0c, 0x88, 0xed, // IID689 + 0x62, 0xdc, 0xb4, 0x10, 0xd3, 0xc4, // IID690 + 0xd5, 0x19, 0xd3, 0xc7, // IID691 + 0x62, 0xfc, 0xb4, 0x14, 0xd3, 0xc7, // IID692 + 0x62, 0xf4, 0xf4, 0x1c, 0xd3, 0xc1, // IID693 + 0x62, 0xd4, 0xcc, 0x10, 0xd3, 0xce, // IID694 + 0x49, 0xd3, 0xcf, // IID695 + 0x62, 0xdc, 0xa4, 0x1c, 0xd3, 0xce, // IID696 + 0x62, 0xdc, 0xbc, 0x14, 0xd3, 0xc8, // IID697 + 0x62, 0xfc, 0xac, 0x18, 0xd3, 0xe4, // IID698 + 0xd5, 0x18, 0xd3, 0xe3, // IID699 + 0x62, 0xdc, 0xf4, 0x14, 0xd3, 0xe1, // IID700 + 0x62, 0xd4, 0x94, 0x1c, 0xd3, 0xe5, // IID701 + 0x62, 0xdc, 0x84, 0x10, 0xd3, 0xfe, // IID702 + 0xd5, 0x18, 0xd3, 0xfa, // IID703 + 0x62, 0xdc, 0xb4, 0x14, 0xd3, 0xf9, // IID704 + 0x62, 0xdc, 0x9c, 0x14, 0xd3, 0xfc, // IID705 + 0x62, 0xdc, 0xcc, 0x10, 0xff, 0xcb, // IID706 + 0x49, 0xff, 0xcc, // IID707 + 0x62, 0xd4, 0xec, 0x14, 0xff, 0xcb, // IID708 + 0x62, 0xd4, 0xac, 0x1c, 0xff, 0xca, // IID709 + 0x62, 0xdc, 0xdc, 0x10, 0xff, 0xc0, // IID710 + 0xd5, 0x18, 0xff, 0xc2, // IID711 + 0x62, 0xd4, 0xe4, 0x1c, 0xff, 0xc3, // IID712 + 0x62, 0xdc, 0xac, 0x14, 0xff, 0xc2, // IID713 + 0x62, 0xd4, 0xd4, 0x10, 0xd3, 0xe0, // IID714 + 0x48, 0xd3, 0xe3, // IID715 + 0x62, 0xfc, 0xcc, 0x14, 0xd3, 0xe5, // IID716 + 0x62, 0xdc, 0xa4, 0x14, 0xd3, 0xe3, // IID717 + 0x62, 0xfc, 0x9c, 0x18, 0xd3, 0xe8, // IID718 + 0x49, 0xd3, 0xe8, // IID719 + 0x62, 0xd4, 0xec, 0x1c, 0xd3, 0xe9, // IID720 + 0x62, 0xfc, 0xdc, 0x14, 0xd3, 0xec, // IID721 + 0x62, 0x6c, 0xfc, 0x08, 0xf4, 0xfd, // IID722 + 0x62, 0xec, 0xfc, 0x08, 0xf4, 0xe4, // IID723 + 0x62, 0xfc, 0xfc, 0x0c, 0xf4, 0xc8, // IID724 + 0x62, 0x54, 0xfc, 0x0c, 0xf4, 0xf6, // IID725 + 0x62, 0x0c, 0xfc, 0x08, 0xaf, 0x9c, 0x49, 0x93, 0x23, 0x5a, 0x44, // IID726 + 0x62, 0xa4, 0xfc, 0x0c, 0xaf, 0xbc, 0x49, 0xf4, 0x10, 0x7f, 0xeb, // IID727 + 0x62, 0x7c, 0xf8, 0x08, 0xf5, 0xac, 0x4e, 0x6a, 0xe6, 0xf3, 0x8a, // IID728 + 0x62, 0x5c, 0xfc, 0x0c, 0xf5, 0xaf, 0xa7, 0x9d, 0xd4, 0xcb, // IID729 + 0x62, 0x9c, 0x84, 0x10, 0xf7, 0x9c, 0x68, 0xf2, 0x95, 0x4e, 0xda, // IID730 + 0x62, 0x94, 0x90, 0x1c, 0xf7, 0x9c, 0xe3, 0xab, 0x13, 0x00, 0x5c, // IID731 + 0x62, 0xfc, 0xfc, 0x08, 0x88, 0x94, 0x8a, 0x51, 0x15, 0xec, 0x9e, // IID732 + 0x62, 0x54, 0xfc, 0x0c, 0x88, 0x8a, 0x78, 0x2a, 0x58, 0xa3, // IID733 + 0x62, 0x9c, 0xf0, 0x10, 0xd3, 0xa4, 0x33, 0x2c, 0xda, 0x4c, 0x1b, // IID734 + 0x62, 0xd4, 0xb4, 0x14, 0xd3, 0xa4, 0x54, 0xce, 0x3b, 0x82, 0x62, // IID735 + 0x62, 0xd4, 0xb0, 0x18, 0xd3, 0xbc, 0x92, 0xb8, 0x85, 0xb5, 0xd9, // IID736 + 0x62, 0x94, 0xe0, 0x1c, 0xd3, 0xbc, 0x1e, 0x00, 0x1e, 0x29, 0x20, // IID737 + 0x62, 0x94, 0x9c, 0x18, 0xff, 0x8c, 0xb7, 0x45, 0x25, 0x08, 0xdf, // IID738 + 0x62, 0x94, 0xb0, 0x1c, 0xff, 0x8c, 0x4a, 0x84, 0x1d, 0x41, 0x21, // IID739 + 0x62, 0xb4, 0xd8, 0x10, 0xff, 0x84, 0xcb, 0x9e, 0x32, 0xf0, 0x02, // IID740 + 0x62, 0x94, 0xa8, 0x1c, 0xff, 0x84, 0x3c, 0x74, 0xa3, 0xaf, 0xc8, // IID741 + 0x62, 0xbc, 0xbc, 0x10, 0xd3, 0xac, 0xf7, 0x50, 0xa5, 0x18, 0x8e, // IID742 + 0x62, 0xbc, 0xb4, 0x14, 0xd3, 0xac, 0x53, 0x88, 0x0a, 0x7b, 0x50, // IID743 + 0x62, 0x64, 0xf8, 0x08, 0xf4, 0xbc, 0x03, 0x2a, 0x19, 0xd5, 0x19, // IID744 + 0x62, 0x3c, 0xf8, 0x0c, 0xf4, 0x8c, 0xa6, 0xcd, 0x07, 0x10, 0x21, // IID745 + 0x62, 0x6c, 0xfc, 0x10, 0x01, 0xa4, 0xdd, 0xe2, 0x05, 0xdc, 0xf7, // IID746 + 0x4e, 0x03, 0xbc, 0xc2, 0xa8, 0x5f, 0x46, 0xcb, // IID747 + 0x62, 0x14, 0xb8, 0x14, 0x01, 0xac, 0xc6, 0xd2, 0x59, 0xdc, 0x6c, // IID748 + 0x62, 0x9c, 0xe4, 0x1c, 0x01, 0x9c, 0xf3, 0xde, 0xe8, 0xc5, 0x36, // IID749 + 0x62, 0x0c, 0xd0, 0x10, 0x21, 0x9c, 0x5b, 0xed, 0xc4, 0xfd, 0xd3, // IID750 + 0xd5, 0x5e, 0x23, 0xbc, 0xbd, 0x4a, 0xc7, 0xf2, 0x6e, // IID751 + 0x62, 0x0c, 0x90, 0x1c, 0x21, 0x9c, 0x4f, 0xab, 0xe9, 0x4f, 0x73, // IID752 + 0x62, 0x14, 0x80, 0x1c, 0x21, 0xbc, 0xee, 0xaa, 0x7a, 0x19, 0xf9, // IID753 + 0x62, 0x0c, 0x9c, 0x18, 0x09, 0xa4, 0xfe, 0x9e, 0x3f, 0xa3, 0x3b, // IID754 + 0xd5, 0x4b, 0x0b, 0x84, 0x0c, 0xcd, 0xc4, 0x1f, 0xd7, // IID755 + 0x62, 0x04, 0xb8, 0x1c, 0x09, 0x9c, 0xc8, 0x6b, 0x42, 0xbd, 0xe1, // IID756 + 0x62, 0xdc, 0xf4, 0x1c, 0x09, 0x8c, 0x9b, 0xad, 0xbc, 0xe4, 0x7b, // IID757 + 0x62, 0xfc, 0xb8, 0x10, 0x29, 0x94, 0xb7, 0xd7, 0x27, 0x88, 0x6f, // IID758 + 0x62, 0xc4, 0xd4, 0x10, 0x29, 0xaa, 0x38, 0x47, 0xca, 0xf9, // IID759 + 0x62, 0x9c, 0xc0, 0x14, 0x29, 0x9c, 0xd3, 0xc0, 0xbc, 0x22, 0x09, // IID760 + 0x62, 0x2c, 0xb4, 0x14, 0x29, 0x8c, 0x3f, 0x54, 0x6b, 0x0b, 0xc7, // IID761 + 0x62, 0xd4, 0xa0, 0x18, 0x31, 0x8c, 0x9c, 0xe9, 0x13, 0x8e, 0xa4, // IID762 + 0xd5, 0x7c, 0x33, 0xa4, 0x13, 0x7e, 0x9b, 0x6b, 0x71, // IID763 + 0x62, 0xa4, 0xd0, 0x14, 0x31, 0x84, 0x29, 0xe2, 0xbb, 0x0f, 0xa5, // IID764 + 0x62, 0x3c, 0x98, 0x1c, 0x31, 0xa4, 0x14, 0xb1, 0x7f, 0x0b, 0x0e, // IID765 + 0x62, 0xf4, 0x8c, 0x10, 0x81, 0x81, 0x4f, 0x7b, 0x3b, 0x2d, 0x00, 0x00, 0x10, 0x00, // IID766 + 0x62, 0xbc, 0x8c, 0x1c, 0x81, 0x84, 0xbd, 0x18, 0x51, 0xdd, 0xed, 0x00, 0x10, 0x00, 0x00, // IID767 + 0x62, 0xbc, 0xc0, 0x10, 0x83, 0xa4, 0x3c, 0x96, 0xb2, 0x91, 0xf6, 0x10, // IID768 + 0x62, 0xf4, 0xac, 0x1c, 0x83, 0xa4, 0xd2, 0x7c, 0xf1, 0x75, 0x38, 0x01, // IID769 + 0x62, 0xa4, 0xf8, 0x08, 0x69, 0x8c, 0x89, 0x76, 0x10, 0xc7, 0x32, 0x00, 0x10, 0x00, 0x00, // IID770 + 0x62, 0xcc, 0xfc, 0x0c, 0x69, 0x9c, 0x9f, 0x0d, 0xa6, 0xad, 0x7b, 0x00, 0x00, 0x10, 0x00, // IID771 + 0x62, 0xfc, 0xb0, 0x10, 0x81, 0x8c, 0x7a, 0x44, 0x74, 0x14, 0x48, 0x00, 0x00, 0x00, 0x01, // IID772 + 0x62, 0x9c, 0x90, 0x14, 0x81, 0x8c, 0x5a, 0xa8, 0xc6, 0xee, 0xb4, 0x00, 0x00, 0x10, 0x00, // IID773 + 0x62, 0xfc, 0x84, 0x10, 0xc1, 0xa2, 0x8c, 0xc3, 0xef, 0xb9, 0x02, // IID774 + 0x62, 0x94, 0xb4, 0x14, 0xc1, 0xa4, 0x3a, 0xa4, 0x5d, 0x92, 0x48, 0x10, // IID775 + 0x62, 0xfc, 0xac, 0x10, 0xc1, 0xba, 0xbe, 0x3a, 0x5e, 0xa1, 0x08, // IID776 + 0x62, 0xd4, 0x98, 0x1c, 0xc1, 0xbc, 0xb2, 0x64, 0x82, 0x95, 0x5d, 0x08, // IID777 + 0x62, 0xfc, 0xe8, 0x18, 0xc1, 0xac, 0xa1, 0x23, 0xdd, 0x5a, 0x29, 0x10, // IID778 + 0x62, 0xbc, 0xe0, 0x1c, 0xc1, 0xac, 0x66, 0xcb, 0x29, 0x29, 0x78, 0x02, // IID779 + 0x62, 0xfc, 0xe4, 0x10, 0x83, 0xaf, 0x8e, 0xe2, 0x7e, 0xb6, 0x01, // IID780 + 0x62, 0xbc, 0xbc, 0x1c, 0x81, 0xac, 0xb3, 0x66, 0x51, 0xd4, 0xe4, 0x00, 0x00, 0x10, 0x00, // IID781 + 0x62, 0xb4, 0xe4, 0x10, 0x81, 0xb4, 0x11, 0xe9, 0x6e, 0xa6, 0x45, 0x00, 0x00, 0x10, 0x00, // IID782 + 0x62, 0x94, 0x98, 0x14, 0x83, 0xb4, 0x29, 0xec, 0x6c, 0x5e, 0xd7, 0x10, // IID783 + 0x62, 0xf4, 0xbc, 0x18, 0x81, 0xc1, 0x00, 0x00, 0x00, 0x01, // IID784 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xc6, 0x00, 0x00, 0x00, 0x01, // IID785 + 0xd5, 0x18, 0x81, 0xc0, 0x00, 0x01, 0x00, 0x00, // IID786 + 0x62, 0xd4, 0xbc, 0x14, 0x81, 0xc1, 0x00, 0x10, 0x00, 0x00, // IID787 + 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xc2, 0x00, 0x10, 0x00, 0x00, // IID788 + 0x62, 0xd4, 0xbc, 0x1c, 0x83, 0xc0, 0x01, // IID789 + 0x62, 0xfc, 0x84, 0x18, 0x81, 0xe6, 0x00, 0x00, 0x10, 0x00, // IID790 + 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xe2, 0x00, 0x00, 0x10, 0x00, // IID791 + 0x48, 0x81, 0xe2, 0x00, 0x10, 0x00, 0x00, // IID792 + 0x62, 0xfc, 0xec, 0x1c, 0x81, 0xe6, 0x00, 0x00, 0x00, 0x10, // IID793 + 0x62, 0xdc, 0xfc, 0x1c, 0x81, 0xe5, 0x00, 0x00, 0x00, 0x10, // IID794 + 0x62, 0xfc, 0xc4, 0x14, 0x81, 0xe7, 0x00, 0x00, 0x00, 0x01, // IID795 + 0x62, 0x54, 0xfc, 0x08, 0x69, 0xcd, 0x00, 0x00, 0x10, 0x00, // IID796 + 0x62, 0xfc, 0xfc, 0x08, 0x69, 0xc2, 0x00, 0x00, 0x10, 0x00, // IID797 + 0x62, 0xec, 0xfc, 0x08, 0x69, 0xc0, 0x00, 0x00, 0x10, 0x00, // IID798 + 0x62, 0xec, 0xfc, 0x0c, 0x6b, 0xcf, 0x01, // IID799 + 0x62, 0xd4, 0xfc, 0x0c, 0x6b, 0xc4, 0x01, // IID800 + 0x62, 0x54, 0xfc, 0x0c, 0x69, 0xd2, 0x00, 0x00, 0x00, 0x10, // IID801 + 0x62, 0xfc, 0xec, 0x18, 0x81, 0xcb, 0x00, 0x01, 0x00, 0x00, // IID802 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xce, 0x00, 0x01, 0x00, 0x00, // IID803 + 0x49, 0x83, 0xcd, 0x01, // IID804 + 0x62, 0xdc, 0xb4, 0x14, 0x81, 0xcd, 0x00, 0x01, 0x00, 0x00, // IID805 + 0x62, 0xf4, 0xfc, 0x1c, 0x81, 0xca, 0x00, 0x01, 0x00, 0x00, // IID806 + 0x62, 0xfc, 0xfc, 0x14, 0x83, 0xc8, 0x10, // IID807 + 0x62, 0xfc, 0x94, 0x18, 0xc1, 0xd3, 0x04, // IID808 + 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xd4, 0x04, // IID809 + 0x49, 0xc1, 0xd1, 0x04, // IID810 + 0x62, 0xfc, 0x94, 0x18, 0xd1, 0xc0, // IID811 + 0x62, 0xdc, 0xfc, 0x18, 0xd1, 0xc7, // IID812 + 0xd5, 0x19, 0xc1, 0xc6, 0x08, // IID813 + 0x62, 0xfc, 0x8c, 0x14, 0xc1, 0xc4, 0x08, // IID814 + 0x62, 0xdc, 0xfc, 0x1c, 0xc1, 0xc7, 0x08, // IID815 + 0x62, 0xdc, 0x84, 0x14, 0xc1, 0xc7, 0x04, // IID816 + 0x62, 0xd4, 0xcc, 0x10, 0xc1, 0xca, 0x04, // IID817 + 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xcd, 0x04, // IID818 + 0xd5, 0x19, 0xc1, 0xc8, 0x10, // IID819 + 0x62, 0xfc, 0x94, 0x14, 0xc1, 0xce, 0x10, // IID820 + 0x62, 0xfc, 0xfc, 0x1c, 0xc1, 0xcc, 0x10, // IID821 + 0x62, 0xdc, 0xa4, 0x14, 0xc1, 0xcb, 0x04, // IID822 + 0x62, 0xfc, 0x84, 0x10, 0xc1, 0xe3, 0x02, // IID823 + 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xe4, 0x02, // IID824 + 0x49, 0xc1, 0xe3, 0x08, // IID825 + 0x62, 0xd4, 0xec, 0x1c, 0xd1, 0xe7, // IID826 + 0x62, 0xd4, 0xfc, 0x1c, 0xd1, 0xe2, // IID827 + 0x62, 0xdc, 0x94, 0x14, 0xc1, 0xe5, 0x04, // IID828 + 0x62, 0xfc, 0xdc, 0x10, 0xd1, 0xf8, // IID829 + 0x62, 0xfc, 0xfc, 0x18, 0xd1, 0xfd, // IID830 + 0xd5, 0x19, 0xc1, 0xfc, 0x08, // IID831 + 0x62, 0xf4, 0x8c, 0x14, 0xc1, 0xf9, 0x04, // IID832 + 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xff, 0x04, // IID833 + 0x62, 0xf4, 0xf4, 0x1c, 0xc1, 0xf9, 0x04, // IID834 + 0x62, 0xdc, 0xec, 0x18, 0xc1, 0xe2, 0x04, // IID835 + 0x62, 0xdc, 0xfc, 0x18, 0xc1, 0xe2, 0x04, // IID836 + 0x49, 0xc1, 0xe0, 0x04, // IID837 + 0x62, 0xf4, 0xf4, 0x1c, 0xd1, 0xe1, // IID838 + 0x62, 0xf4, 0xfc, 0x1c, 0xd1, 0xe1, // IID839 + 0x62, 0xd4, 0x94, 0x1c, 0xc1, 0xe5, 0x02, // IID840 + 0x62, 0xdc, 0x8c, 0x18, 0xc1, 0xeb, 0x02, // IID841 + 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xeb, 0x02, // IID842 + 0x49, 0xc1, 0xe9, 0x10, // IID843 + 0x62, 0xdc, 0xec, 0x1c, 0xc1, 0xef, 0x02, // IID844 + 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xee, 0x02, // IID845 + 0x62, 0xd4, 0x9c, 0x1c, 0xc1, 0xec, 0x08, // IID846 + 0x62, 0xdc, 0xac, 0x18, 0x83, 0xec, 0x01, // IID847 + 0x62, 0xd4, 0xfc, 0x18, 0x83, 0xe8, 0x01, // IID848 + 0x48, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x01, // IID849 + 0x62, 0xf4, 0xec, 0x1c, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x01, // IID850 + 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xea, 0x00, 0x00, 0x00, 0x01, // IID851 + 0x62, 0xdc, 0xa4, 0x14, 0x81, 0xeb, 0x00, 0x00, 0x01, 0x00, // IID852 + 0x62, 0xf4, 0x8c, 0x10, 0x81, 0xf1, 0x00, 0x10, 0x00, 0x00, // IID853 + 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xf5, 0x00, 0x10, 0x00, 0x00, // IID854 + 0x48, 0x81, 0xf1, 0x00, 0x00, 0x00, 0x01, // IID855 + 0x62, 0xd4, 0xd4, 0x14, 0x83, 0xf4, 0x01, // IID856 + 0x62, 0xf4, 0xfc, 0x1c, 0x83, 0xf2, 0x01, // IID857 + 0x62, 0xf4, 0xe4, 0x1c, 0x81, 0xf3, 0x00, 0x00, 0x00, 0x01, // IID858 + 0x62, 0xf4, 0xa4, 0x18, 0x81, 0xca, 0x00, 0x00, 0x01, 0x00, // IID859 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xce, 0x00, 0x00, 0x01, 0x00, // IID860 + 0x49, 0x81, 0xce, 0x00, 0x00, 0x04, 0x00, // IID861 + 0x62, 0xdc, 0xb4, 0x10, 0x81, 0xcd, 0x00, 0x00, 0x04, 0x00, // IID862 + 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xcd, 0x00, 0x00, 0x04, 0x00, // IID863 + 0x49, 0x81, 0xcb, 0x00, 0x00, 0x00, 0x01, // IID864 + 0x62, 0xfc, 0x94, 0x10, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x04, // IID865 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x04, // IID866 + 0xd5, 0x18, 0x81, 0xea, 0x00, 0x00, 0x00, 0x04, // IID867 + 0x62, 0xfc, 0x9c, 0x14, 0x81, 0xef, 0x00, 0x00, 0x40, 0x00, // IID868 + 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xed, 0x00, 0x00, 0x40, 0x00, // IID869 + 0x62, 0xfc, 0xfc, 0x14, 0x81, 0xe8, 0x00, 0x00, 0x00, 0x01, // IID870 + 0x62, 0x0c, 0xbc, 0x18, 0x03, 0x8c, 0x42, 0xef, 0x3d, 0x63, 0x10, // IID871 + 0xd5, 0x3c, 0x03, 0xac, 0x42, 0xf8, 0xba, 0xdf, 0x8b, // IID872 + 0x62, 0x44, 0xf4, 0x14, 0x03, 0x94, 0x24, 0xbf, 0x0a, 0xa8, 0x23, // IID873 + 0x62, 0x5c, 0xb0, 0x1c, 0x03, 0x8c, 0x1d, 0xae, 0x61, 0x61, 0xfd, // IID874 + 0x62, 0x24, 0xb0, 0x18, 0x23, 0xa4, 0x89, 0xaa, 0xff, 0x61, 0x42, // IID875 + 0xd5, 0x6e, 0x23, 0x9c, 0x22, 0x3f, 0x36, 0x42, 0xd9, // IID876 + 0x62, 0x7c, 0x8c, 0x1c, 0x23, 0x98, 0xdf, 0x0d, 0xba, 0x63, // IID877 + 0x62, 0x3c, 0xb8, 0x1c, 0x23, 0x84, 0x4e, 0xbc, 0x54, 0x49, 0xbc, // IID878 + 0x62, 0xdc, 0xe4, 0x10, 0x0b, 0x8c, 0x8b, 0x3a, 0x80, 0x97, 0x80, // IID879 + 0xd5, 0x6a, 0x0b, 0x9c, 0x53, 0x16, 0x48, 0x92, 0xb7, // IID880 + 0x62, 0x54, 0x88, 0x14, 0x0b, 0x94, 0xd6, 0x84, 0x48, 0x88, 0x14, // IID881 + 0x62, 0x4c, 0xa4, 0x14, 0x0b, 0x9d, 0x80, 0x71, 0x33, 0x20, // IID882 + 0x62, 0xec, 0xf4, 0x18, 0xaf, 0xac, 0x1d, 0x72, 0x77, 0xfc, 0xcc, // IID883 + 0xd5, 0x9b, 0xaf, 0x94, 0xcc, 0xbf, 0x08, 0x27, 0x85, // IID884 + 0x62, 0x2c, 0xbc, 0x1c, 0xaf, 0xac, 0x21, 0x6a, 0x39, 0x85, 0x6e, // IID885 + 0x62, 0xac, 0xfc, 0x14, 0xaf, 0x84, 0xd3, 0x00, 0x6d, 0xa6, 0xb6, // IID886 + 0x62, 0xc4, 0xd8, 0x10, 0x2b, 0x8c, 0x35, 0x4f, 0x9a, 0x21, 0x1d, // IID887 + 0xd5, 0x7c, 0x2b, 0x8c, 0xed, 0x39, 0x57, 0x97, 0x97, // IID888 + 0x62, 0x64, 0xd8, 0x14, 0x2b, 0x84, 0xa3, 0xa6, 0x9d, 0xc5, 0x32, // IID889 + 0x62, 0x54, 0xb8, 0x1c, 0x2b, 0x84, 0x0c, 0x31, 0xd2, 0x41, 0xd9, // IID890 + 0x62, 0xc4, 0xec, 0x18, 0x33, 0x99, 0x91, 0x3b, 0x90, 0x7d, // IID891 + 0xd5, 0x7f, 0x33, 0xa4, 0x9d, 0x6f, 0x1f, 0x09, 0x53, // IID892 + 0x62, 0xcc, 0xf4, 0x14, 0x33, 0x83, 0x07, 0x92, 0x6e, 0x7c, // IID893 + 0x62, 0x14, 0x80, 0x1c, 0x33, 0xbc, 0xc5, 0xa0, 0x86, 0x37, 0x8a, // IID894 + 0x62, 0xfc, 0xfc, 0x10, 0x03, 0xda, // IID895 + 0xd5, 0x5c, 0x03, 0xc2, // IID896 + 0x4d, 0x03, 0xcf, // IID897 + 0x62, 0x44, 0xe4, 0x14, 0x03, 0xd5, // IID898 + 0x62, 0x6c, 0x9c, 0x14, 0x03, 0xe6, // IID899 + 0x62, 0x7c, 0xcc, 0x14, 0x03, 0xde, // IID900 + 0x62, 0x54, 0xf5, 0x18, 0x66, 0xe5, // IID901 + 0x62, 0x44, 0xfd, 0x08, 0x66, 0xf4, // IID902 + 0x62, 0x7c, 0x9e, 0x10, 0x66, 0xf2, // IID903 + 0x62, 0x6c, 0xfe, 0x08, 0x66, 0xf3, // IID904 + 0x62, 0x54, 0xdc, 0x10, 0x23, 0xf6, // IID905 + 0xd5, 0x58, 0x23, 0xcf, // IID906 + 0xd5, 0x49, 0x23, 0xce, // IID907 + 0x62, 0xc4, 0xe4, 0x14, 0x23, 0xe7, // IID908 + 0x62, 0xd4, 0xe4, 0x1c, 0x23, 0xdd, // IID909 + 0x62, 0x6c, 0xcc, 0x14, 0x23, 0xf6, // IID910 + 0x62, 0x64, 0xf4, 0x10, 0xaf, 0xc1, // IID911 + 0xd5, 0xc9, 0xaf, 0xe8, // IID912 + 0xd5, 0xdc, 0xaf, 0xed, // IID913 + 0x62, 0x7c, 0xa4, 0x14, 0xaf, 0xef, // IID914 + 0x62, 0x44, 0xac, 0x14, 0xaf, 0xd0, // IID915 + 0x62, 0x7c, 0xcc, 0x14, 0xaf, 0xee, // IID916 + 0x62, 0xdc, 0xa4, 0x18, 0x0b, 0xd5, // IID917 + 0xd5, 0x19, 0x0b, 0xd7, // IID918 + 0xd5, 0x1d, 0x0b, 0xd5, // IID919 + 0x62, 0x64, 0xa4, 0x14, 0x0b, 0xe1, // IID920 + 0x62, 0x44, 0xb4, 0x14, 0x0b, 0xc9, // IID921 + 0x62, 0x74, 0xf4, 0x1c, 0x0b, 0xc1, // IID922 + 0x62, 0x7c, 0xf4, 0x18, 0x2b, 0xd0, // IID923 + 0xd5, 0x48, 0x2b, 0xc9, // IID924 + 0x62, 0xcc, 0x94, 0x1c, 0x2b, 0xe8, // IID925 + 0x62, 0x4c, 0x84, 0x14, 0x2b, 0xfc, // IID926 + 0xd5, 0x59, 0x33, 0xfc, // IID927 + 0x4d, 0x33, 0xd3, // IID928 + 0xd5, 0x58, 0x33, 0xda, // IID929 + 0x62, 0x74, 0x84, 0x14, 0x33, 0xca, // IID930 + 0x62, 0x54, 0x94, 0x1c, 0x33, 0xe9, // IID931 + 0x62, 0x74, 0xf4, 0x1c, 0x33, 0xd1, // IID932 + 0x62, 0xcc, 0x9c, 0x18, 0x24, 0xf0, 0x08, // IID933 + 0xd5, 0xdd, 0xa4, 0xc9, 0x08, // IID934 + 0x62, 0x7c, 0xd4, 0x14, 0x24, 0xfc, 0x08, // IID935 + 0x62, 0x7c, 0xd4, 0x14, 0x24, 0xd5, 0x08, // IID936 + 0xd5, 0x9c, 0xac, 0xc2, 0x02, // IID937 + 0xd5, 0xdd, 0xac, 0xea, 0x08, // IID938 + 0x62, 0xcc, 0x94, 0x14, 0x2c, 0xda, 0x02, // IID939 + 0x62, 0xd4, 0x9c, 0x1c, 0x2c, 0xcc, 0x04, // IID940 + 0x62, 0xec, 0xd4, 0x10, 0x40, 0xf7, // IID941 + 0x4d, 0x0f, 0x40, 0xcd, // IID942 + 0x62, 0xcc, 0xf4, 0x18, 0x41, 0xf8, // IID943 + 0xd5, 0xcc, 0x41, 0xe2, // IID944 + 0x62, 0x6c, 0x8c, 0x18, 0x42, 0xff, // IID945 + 0xd5, 0xdc, 0x42, 0xf7, // IID946 + 0x62, 0x6c, 0xac, 0x18, 0x43, 0xee, // IID947 + 0xd5, 0x99, 0x43, 0xda, // IID948 + 0x62, 0xc4, 0xc4, 0x10, 0x44, 0xed, // IID949 + 0xd5, 0x9c, 0x44, 0xd4, // IID950 + 0x62, 0x5c, 0xe4, 0x18, 0x45, 0xcd, // IID951 + 0xd5, 0xd9, 0x45, 0xc6, // IID952 + 0x62, 0xdc, 0x94, 0x18, 0x46, 0xcd, // IID953 + 0xd5, 0xcd, 0x46, 0xfd, // IID954 + 0x62, 0x5c, 0xa4, 0x10, 0x47, 0xce, // IID955 + 0xd5, 0xdc, 0x47, 0xd4, // IID956 + 0x62, 0x7c, 0xbc, 0x18, 0x48, 0xe6, // IID957 + 0xd5, 0xdc, 0x48, 0xf9, // IID958 + 0x62, 0xdc, 0x94, 0x10, 0x49, 0xc9, // IID959 + 0xd5, 0xc9, 0x49, 0xf6, // IID960 + 0x62, 0x44, 0xf4, 0x18, 0x4a, 0xd9, // IID961 + 0xd5, 0xc9, 0x4a, 0xf3, // IID962 + 0x62, 0xcc, 0x8c, 0x18, 0x4b, 0xd8, // IID963 + 0xd5, 0xdc, 0x4b, 0xc1, // IID964 + 0x62, 0xcc, 0xf4, 0x10, 0x4c, 0xde, // IID965 + 0xd5, 0xc9, 0x4c, 0xde, // IID966 + 0x62, 0x5c, 0xb4, 0x10, 0x4d, 0xdd, // IID967 + 0xd5, 0x9d, 0x4d, 0xe2, // IID968 + 0x62, 0xd4, 0xa4, 0x18, 0x4e, 0xda, // IID969 + 0xd5, 0x98, 0x4e, 0xd6, // IID970 + 0x62, 0x7c, 0x8c, 0x18, 0x4f, 0xff, // IID971 + 0xd5, 0x9d, 0x4f, 0xc0, // IID972 + 0x62, 0x04, 0xe4, 0x18, 0x40, 0xbc, 0xc2, 0x20, 0x9f, 0xc0, 0xce, // IID973 + 0xd5, 0xea, 0x40, 0xbc, 0x81, 0x9c, 0x1d, 0xf4, 0x17, // IID974 + 0x62, 0x7c, 0x84, 0x10, 0x41, 0x98, 0x42, 0x89, 0x01, 0x2c, // IID975 + 0xd5, 0xbc, 0x41, 0x9c, 0xe0, 0x55, 0x6a, 0x4b, 0x67, // IID976 + 0x62, 0x54, 0xb4, 0x18, 0x42, 0xac, 0x09, 0xdf, 0x11, 0x4a, 0x39, // IID977 + 0xd5, 0xec, 0x42, 0xb4, 0x72, 0x78, 0xd4, 0xc9, 0x93, // IID978 + 0x62, 0xdc, 0x94, 0x18, 0x43, 0x8c, 0xc8, 0x66, 0x0b, 0x50, 0x46, // IID979 + 0xd5, 0xfe, 0x43, 0x84, 0x4a, 0x7c, 0x3b, 0x28, 0x53, // IID980 + 0x62, 0x04, 0xc4, 0x10, 0x44, 0x8c, 0x0f, 0xe2, 0xfc, 0xfc, 0xa0, // IID981 + 0xd5, 0xfd, 0x44, 0x8c, 0x44, 0xec, 0x0a, 0x31, 0xac, // IID982 + 0x62, 0x0c, 0xe0, 0x18, 0x45, 0x8c, 0x88, 0x79, 0x53, 0x35, 0x99, // IID983 + 0xd5, 0xfb, 0x45, 0x84, 0xf3, 0x5d, 0x45, 0x7f, 0x79, // IID984 + 0x62, 0x6c, 0xb0, 0x10, 0x46, 0xb4, 0x52, 0xcd, 0xaa, 0x9d, 0x1c, // IID985 + 0xd5, 0xea, 0x46, 0xb4, 0x49, 0x57, 0x05, 0x34, 0xc2, // IID986 + 0x62, 0x4c, 0xbc, 0x10, 0x47, 0x91, 0xb5, 0x60, 0x70, 0x74, // IID987 + 0xd5, 0xbd, 0x47, 0x84, 0xe0, 0xf6, 0x85, 0xd2, 0x47, // IID988 + 0x62, 0x84, 0x9c, 0x18, 0x48, 0x84, 0x95, 0x14, 0xb2, 0xe5, 0x34, // IID989 + 0xd5, 0xa9, 0x48, 0x94, 0x1f, 0x4f, 0xc7, 0xae, 0xbf, // IID990 + 0x62, 0xa4, 0xec, 0x10, 0x49, 0xac, 0xab, 0x97, 0x91, 0xb1, 0x51, // IID991 + 0xd5, 0xef, 0x49, 0x84, 0xfb, 0x0a, 0x52, 0x01, 0x3e, // IID992 + 0x62, 0x04, 0x90, 0x10, 0x4a, 0x94, 0xca, 0x8e, 0xc7, 0x83, 0xa0, // IID993 + 0xd5, 0x9e, 0x4a, 0x9c, 0xd6, 0xad, 0xeb, 0x8c, 0x97, // IID994 + 0x62, 0x14, 0xd8, 0x10, 0x4b, 0xbc, 0x09, 0xaa, 0xed, 0x37, 0x4a, // IID995 + 0xd5, 0xed, 0x4b, 0xbc, 0x21, 0x86, 0x9f, 0x99, 0x4f, // IID996 + 0x62, 0x84, 0xe8, 0x10, 0x4c, 0xbc, 0x19, 0xe3, 0xbb, 0xef, 0xcb, // IID997 + 0xd5, 0xdb, 0x4c, 0x84, 0xd0, 0xee, 0x66, 0xed, 0x52, // IID998 + 0x62, 0xe4, 0xa4, 0x18, 0x4d, 0x91, 0x63, 0x91, 0xe0, 0x1d, // IID999 + 0xd5, 0xbd, 0x4d, 0xb4, 0x78, 0xda, 0xb4, 0xf3, 0x5d, // IID1000 + 0x62, 0x5c, 0x80, 0x18, 0x4e, 0xb4, 0x66, 0x76, 0xb9, 0x9a, 0x5c, // IID1001 + 0xd5, 0xfe, 0x4e, 0x94, 0x9a, 0xd7, 0x3c, 0x27, 0xff, // IID1002 + 0x62, 0x5c, 0x90, 0x10, 0x4f, 0x8c, 0xe6, 0x73, 0x30, 0x56, 0xc8, // IID1003 + 0xd5, 0xc9, 0x4f, 0xa4, 0x58, 0xef, 0x7d, 0xdc, 0x1b, // IID1004 #endif // _LP64 }; @@ -2196,19 +2364,19 @@ 15, // IID265 15, // IID266 11, // IID267 - 11, // IID268 + 9, // IID268 11, // IID269 11, // IID270 11, // IID271 - 11, // IID272 + 9, // IID272 11, // IID273 10, // IID274 11, // IID275 - 11, // IID276 + 9, // IID276 11, // IID277 11, // IID278 11, // IID279 - 11, // IID280 + 9, // IID280 10, // IID281 11, // IID282 10, // IID283 @@ -2216,645 +2384,729 @@ 11, // IID285 10, // IID286 11, // IID287 - 10, // IID288 + 8, // IID288 11, // IID289 11, // IID290 - 10, // IID291 - 10, // IID292 - 8, // IID293 - 7, // IID294 + 11, // IID291 + 9, // IID292 + 11, // IID293 + 11, // IID294 7, // IID295 - 10, // IID296 - 10, // IID297 + 7, // IID296 + 8, // IID297 10, // IID298 - 8, // IID299 + 10, // IID299 10, // IID300 - 10, // IID301 + 4, // IID301 7, // IID302 - 10, // IID303 -#endif // _LP64 - 10, // IID304 -#ifdef _LP64 - 10, // IID305 - 10, // IID306 + 5, // IID303 + 7, // IID304 + 7, // IID305 + 7, // IID306 10, // IID307 - 7, // IID308 + 10, // IID308 10, // IID309 - 10, // IID310 - 8, // IID311 - 7, // IID312 + 7, // IID310 + 7, // IID311 + 10, // IID312 7, // IID313 - 10, // IID314 - 6, // IID315 - 6, // IID316 - 4, // IID317 - 7, // IID318 + 7, // IID314 + 8, // IID315 + 7, // IID316 + 7, // IID317 #endif // _LP64 - 7, // IID319 + 10, // IID318 #ifdef _LP64 - 5, // IID320 - 7, // IID321 + 5, // IID319 + 7, // IID320 + 5, // IID321 7, // IID322 7, // IID323 - 7, // IID324 -#endif // _LP64 + 4, // IID324 7, // IID325 -#ifdef _LP64 - 4, // IID326 - 7, // IID327 + 7, // IID326 + 6, // IID327 7, // IID328 7, // IID329 - 7, // IID330 + 5, // IID330 7, // IID331 - 5, // IID332 + 7, // IID332 7, // IID333 7, // IID334 7, // IID335 - 7, // IID336 + 5, // IID336 7, // IID337 - 4, // IID338 + 7, // IID338 7, // IID339 - 7, // IID340 - 7, // IID341 - 7, // IID342 + 6, // IID340 + 6, // IID341 + 5, // IID342 7, // IID343 - 4, // IID344 + 7, // IID344 7, // IID345 7, // IID346 7, // IID347 - 7, // IID348 + 4, // IID348 7, // IID349 - 5, // IID350 + 7, // IID350 6, // IID351 - 6, // IID352 + 7, // IID352 7, // IID353 - 10, // IID354 - 10, // IID355 - 8, // IID356 - 10, // IID357 + 5, // IID354 + 7, // IID355 + 7, // IID356 + 7, // IID357 10, // IID358 10, // IID359 - 10, // IID360 + 8, // IID360 10, // IID361 -#endif // _LP64 - 6, // IID362 -#ifdef _LP64 - 10, // IID363 + 10, // IID362 + 7, // IID363 10, // IID364 10, // IID365 - 10, // IID366 - 10, // IID367 - 8, // IID368 - 10, // IID369 + 5, // IID366 + 7, // IID367 +#endif // _LP64 + 7, // IID368 +#ifdef _LP64 + 7, // IID369 10, // IID370 10, // IID371 - 10, // IID372 + 8, // IID372 10, // IID373 - 11, // IID374 - 11, // IID375 + 10, // IID374 + 10, // IID375 11, // IID376 - 11, // IID377 + 9, // IID377 11, // IID378 10, // IID379 11, // IID380 - 11, // IID381 + 9, // IID381 11, // IID382 11, // IID383 11, // IID384 - 11, // IID385 + 9, // IID385 11, // IID386 11, // IID387 - 6, // IID388 - 4, // IID389 - 6, // IID390 - 6, // IID391 - 6, // IID392 - 3, // IID393 - 6, // IID394 - 6, // IID395 - 6, // IID396 - 4, // IID397 - 6, // IID398 - 6, // IID399 - 6, // IID400 - 5, // IID401 - 6, // IID402 - 6, // IID403 - 6, // IID404 - 4, // IID405 - 6, // IID406 - 6, // IID407 + 10, // IID388 + 8, // IID389 + 11, // IID390 + 11, // IID391 + 11, // IID392 + 9, // IID393 + 10, // IID394 + 11, // IID395 + 11, // IID396 + 9, // IID397 + 11, // IID398 + 11, // IID399 + 11, // IID400 + 9, // IID401 + 11, // IID402 + 11, // IID403 + 11, // IID404 + 10, // IID405 + 10, // IID406 + 11, // IID407 6, // IID408 4, // IID409 - 6, // IID410 + 4, // IID410 6, // IID411 6, // IID412 - 4, // IID413 + 6, // IID413 6, // IID414 - 6, // IID415 - 6, // IID416 - 4, // IID417 + 4, // IID415 + 3, // IID416 + 6, // IID417 6, // IID418 6, // IID419 6, // IID420 4, // IID421 - 6, // IID422 + 4, // IID422 6, // IID423 - 7, // IID424 - 5, // IID425 - 7, // IID426 - 7, // IID427 - 7, // IID428 - 5, // IID429 - 7, // IID430 - 7, // IID431 + 6, // IID424 + 6, // IID425 + 6, // IID426 + 5, // IID427 + 4, // IID428 + 6, // IID429 + 6, // IID430 +#endif // _LP64 + 6, // IID431 +#ifdef _LP64 6, // IID432 4, // IID433 - 6, // IID434 - 4, // IID435 + 4, // IID434 + 6, // IID435 6, // IID436 - 4, // IID437 + 6, // IID437 6, // IID438 4, // IID439 6, // IID440 - 4, // IID441 + 6, // IID441 6, // IID442 4, // IID443 6, // IID444 - 4, // IID445 + 6, // IID445 6, // IID446 - 4, // IID447 + 3, // IID447 6, // IID448 - 4, // IID449 + 6, // IID449 6, // IID450 4, // IID451 4, // IID452 - 4, // IID453 + 6, // IID453 6, // IID454 - 4, // IID455 - 6, // IID456 - 4, // IID457 - 6, // IID458 - 4, // IID459 - 6, // IID460 - 4, // IID461 - 6, // IID462 - 4, // IID463 - 10, // IID464 - 11, // IID465 - 11, // IID466 - 11, // IID467 - 10, // IID468 - 11, // IID469 - 11, // IID470 - 11, // IID471 - 11, // IID472 - 11, // IID473 - 11, // IID474 - 11, // IID475 - 11, // IID476 - 11, // IID477 - 11, // IID478 - 11, // IID479 - 4, // IID480 + 6, // IID455 + 7, // IID456 + 5, // IID457 + 7, // IID458 + 7, // IID459 + 7, // IID460 + 5, // IID461 + 7, // IID462 + 7, // IID463 + 6, // IID464 + 4, // IID465 + 6, // IID466 + 4, // IID467 + 6, // IID468 + 4, // IID469 + 6, // IID470 + 4, // IID471 + 6, // IID472 + 4, // IID473 + 6, // IID474 + 4, // IID475 + 6, // IID476 + 4, // IID477 + 6, // IID478 + 4, // IID479 + 6, // IID480 4, // IID481 - 4, // IID482 - 5, // IID483 - 4, // IID484 + 6, // IID482 + 4, // IID483 + 6, // IID484 4, // IID485 - 5, // IID486 - 5, // IID487 - 4, // IID488 + 6, // IID486 + 4, // IID487 + 6, // IID488 4, // IID489 - 4, // IID490 - 3, // IID491 - 4, // IID492 + 6, // IID490 + 4, // IID491 + 6, // IID492 4, // IID493 - 4, // IID494 + 6, // IID494 4, // IID495 - 4, // IID496 - 4, // IID497 - 9, // IID498 + 11, // IID496 + 9, // IID497 + 11, // IID498 9, // IID499 - 9, // IID500 + 11, // IID500 9, // IID501 - 9, // IID502 - 9, // IID503 - 9, // IID504 + 11, // IID502 + 8, // IID503 + 11, // IID504 9, // IID505 - 12, // IID506 - 12, // IID507 + 11, // IID506 + 8, // IID507 11, // IID508 - 8, // IID509 - 9, // IID510 - 13, // IID511 - 10, // IID512 - 12, // IID513 - 10, // IID514 - 10, // IID515 - 13, // IID516 - 12, // IID517 - 9, // IID518 + 9, // IID509 + 10, // IID510 + 9, // IID511 + 11, // IID512 + 9, // IID513 + 9, // IID514 + 9, // IID515 + 11, // IID516 + 9, // IID517 + 11, // IID518 9, // IID519 - 9, // IID520 + 11, // IID520 9, // IID521 - 9, // IID522 - 7, // IID523 - 9, // IID524 + 10, // IID522 + 9, // IID523 + 10, // IID524 9, // IID525 - 9, // IID526 + 11, // IID526 9, // IID527 - 10, // IID528 - 9, // IID529 - 9, // IID530 - 7, // IID531 - 10, // IID532 - 9, // IID533 - 9, // IID534 + 4, // IID528 + 3, // IID529 + 4, // IID530 + 5, // IID531 + 3, // IID532 + 4, // IID533 + 5, // IID534 5, // IID535 - 5, // IID536 - 8, // IID537 - 8, // IID538 + 4, // IID536 + 3, // IID537 + 4, // IID538 4, // IID539 4, // IID540 - 5, // IID541 - 5, // IID542 + 4, // IID541 + 4, // IID542 4, // IID543 4, // IID544 - 7, // IID545 - 4, // IID546 - 3, // IID547 - 4, // IID548 + 4, // IID545 + 8, // IID546 + 9, // IID547 + 9, // IID548 8, // IID549 8, // IID550 - 10, // IID551 - 5, // IID552 - 8, // IID553 - 8, // IID554 - 7, // IID555 - 9, // IID556 - 8, // IID557 - 9, // IID558 - 9, // IID559 - 9, // IID560 - 9, // IID561 - 8, // IID562 - 9, // IID563 - 9, // IID564 - 9, // IID565 + 9, // IID551 + 9, // IID552 + 9, // IID553 + 13, // IID554 + 13, // IID555 + 11, // IID556 + 10, // IID557 + 10, // IID558 + 13, // IID559 + 10, // IID560 + 13, // IID561 + 10, // IID562 + 13, // IID563 + 11, // IID564 + 13, // IID565 9, // IID566 9, // IID567 9, // IID568 9, // IID569 9, // IID570 9, // IID571 - 4, // IID572 - 4, // IID573 - 4, // IID574 - 3, // IID575 - 3, // IID576 - 4, // IID577 - 4, // IID578 - 3, // IID579 - 4, // IID580 - 3, // IID581 - 4, // IID582 - 4, // IID583 - 4, // IID584 - 3, // IID585 - 3, // IID586 - 3, // IID587 + 9, // IID572 + 9, // IID573 + 8, // IID574 + 9, // IID575 + 10, // IID576 + 9, // IID577 + 9, // IID578 + 9, // IID579 + 10, // IID580 + 9, // IID581 + 9, // IID582 + 8, // IID583 + 5, // IID584 + 8, // IID585 + 7, // IID586 + 4, // IID587 3, // IID588 - 9, // IID589 - 9, // IID590 - 9, // IID591 - 9, // IID592 - 9, // IID593 - 9, // IID594 - 9, // IID595 - 9, // IID596 - 13, // IID597 + 5, // IID589 + 4, // IID590 + 4, // IID591 + 5, // IID592 + 7, // IID593 + 5, // IID594 + 5, // IID595 + 7, // IID596 + 7, // IID597 8, // IID598 - 5, // IID599 + 11, // IID599 5, // IID600 - 6, // IID601 - 6, // IID602 - 6, // IID603 - 6, // IID604 + 8, // IID601 + 8, // IID602 + 8, // IID603 + 9, // IID604 9, // IID605 9, // IID606 9, // IID607 9, // IID608 - 4, // IID609 - 4, // IID610 - 4, // IID611 - 4, // IID612 + 8, // IID609 + 9, // IID610 + 9, // IID611 + 9, // IID612 9, // IID613 - 6, // IID614 - 6, // IID615 - 6, // IID616 - 6, // IID617 - 6, // IID618 - 6, // IID619 - 6, // IID620 - 6, // IID621 - 11, // IID622 - 11, // IID623 - 6, // IID624 - 4, // IID625 - 6, // IID626 - 6, // IID627 - 6, // IID628 - 6, // IID629 - 6, // IID630 - 6, // IID631 - 6, // IID632 + 9, // IID614 + 9, // IID615 + 9, // IID616 + 8, // IID617 + 9, // IID618 + 9, // IID619 + 4, // IID620 + 3, // IID621 + 4, // IID622 + 3, // IID623 + 4, // IID624 + 3, // IID625 + 3, // IID626 + 4, // IID627 + 4, // IID628 + 3, // IID629 + 4, // IID630 + 4, // IID631 + 4, // IID632 4, // IID633 - 6, // IID634 - 6, // IID635 - 6, // IID636 - 4, // IID637 - 6, // IID638 - 6, // IID639 - 6, // IID640 - 6, // IID641 - 6, // IID642 - 4, // IID643 - 6, // IID644 - 6, // IID645 - 6, // IID646 - 4, // IID647 - 6, // IID648 + 3, // IID634 + 3, // IID635 + 3, // IID636 + 9, // IID637 + 8, // IID638 + 9, // IID639 + 8, // IID640 + 9, // IID641 + 8, // IID642 + 7, // IID643 + 9, // IID644 + 13, // IID645 + 8, // IID646 + 5, // IID647 + 5, // IID648 6, // IID649 6, // IID650 - 3, // IID651 + 6, // IID651 6, // IID652 - 6, // IID653 - 6, // IID654 - 3, // IID655 - 6, // IID656 - 6, // IID657 - 6, // IID658 - 3, // IID659 - 6, // IID660 - 6, // IID661 - 4, // IID662 - 4, // IID663 + 9, // IID653 + 9, // IID654 + 8, // IID655 + 9, // IID656 + 4, // IID657 + 4, // IID658 + 4, // IID659 + 4, // IID660 + 9, // IID661 + 6, // IID662 + 6, // IID663 6, // IID664 6, // IID665 6, // IID666 - 3, // IID667 + 6, // IID667 6, // IID668 6, // IID669 - 6, // IID670 - 4, // IID671 + 11, // IID670 + 11, // IID671 6, // IID672 - 6, // IID673 + 4, // IID673 6, // IID674 6, // IID675 6, // IID676 6, // IID677 - 10, // IID678 - 10, // IID679 - 11, // IID680 - 11, // IID681 - 11, // IID682 - 11, // IID683 - 11, // IID684 - 10, // IID685 - 10, // IID686 - 11, // IID687 - 11, // IID688 - 11, // IID689 - 11, // IID690 - 11, // IID691 - 11, // IID692 - 11, // IID693 - 11, // IID694 - 10, // IID695 - 11, // IID696 - 11, // IID697 - 10, // IID698 - 11, // IID699 - 11, // IID700 - 10, // IID701 - 11, // IID702 - 11, // IID703 - 11, // IID704 - 10, // IID705 - 11, // IID706 - 11, // IID707 - 11, // IID708 - 11, // IID709 - 11, // IID710 - 11, // IID711 - 11, // IID712 - 11, // IID713 - 10, // IID714 - 11, // IID715 - 11, // IID716 - 10, // IID717 - 15, // IID718 - 12, // IID719 - 12, // IID720 - 15, // IID721 - 15, // IID722 - 12, // IID723 - 12, // IID724 - 15, // IID725 - 12, // IID726 - 12, // IID727 - 12, // IID728 - 11, // IID729 - 12, // IID730 - 12, // IID731 - 14, // IID732 - 15, // IID733 - 15, // IID734 - 12, // IID735 - 7, // IID736 - 7, // IID737 - 8, // IID738 - 10, // IID739 - 10, // IID740 - 10, // IID741 - 10, // IID742 - 10, // IID743 - 8, // IID744 - 10, // IID745 - 10, // IID746 - 10, // IID747 - 10, // IID748 - 10, // IID749 - 10, // IID750 - 10, // IID751 - 10, // IID752 - 10, // IID753 - 10, // IID754 - 10, // IID755 - 8, // IID756 - 10, // IID757 - 10, // IID758 + 6, // IID678 + 6, // IID679 + 6, // IID680 + 4, // IID681 + 6, // IID682 + 6, // IID683 + 6, // IID684 + 4, // IID685 + 6, // IID686 + 6, // IID687 + 6, // IID688 + 6, // IID689 + 6, // IID690 + 4, // IID691 + 6, // IID692 + 6, // IID693 + 6, // IID694 + 3, // IID695 + 6, // IID696 + 6, // IID697 + 6, // IID698 + 4, // IID699 + 6, // IID700 + 6, // IID701 + 6, // IID702 + 4, // IID703 + 6, // IID704 + 6, // IID705 + 6, // IID706 + 3, // IID707 + 6, // IID708 + 6, // IID709 + 6, // IID710 + 4, // IID711 + 6, // IID712 + 6, // IID713 + 6, // IID714 + 3, // IID715 + 6, // IID716 + 6, // IID717 + 6, // IID718 + 3, // IID719 + 6, // IID720 + 6, // IID721 + 6, // IID722 + 6, // IID723 + 6, // IID724 + 6, // IID725 + 11, // IID726 + 11, // IID727 + 11, // IID728 + 10, // IID729 + 11, // IID730 + 11, // IID731 + 11, // IID732 + 10, // IID733 + 11, // IID734 + 11, // IID735 + 11, // IID736 + 11, // IID737 + 11, // IID738 + 11, // IID739 + 11, // IID740 + 11, // IID741 + 11, // IID742 + 11, // IID743 + 11, // IID744 + 11, // IID745 + 11, // IID746 + 8, // IID747 + 11, // IID748 + 11, // IID749 + 11, // IID750 + 9, // IID751 + 11, // IID752 + 11, // IID753 + 11, // IID754 + 9, // IID755 + 11, // IID756 + 11, // IID757 + 11, // IID758 10, // IID759 - 7, // IID760 - 7, // IID761 - 4, // IID762 - 7, // IID763 - 7, // IID764 - 4, // IID765 - 6, // IID766 - 6, // IID767 - 7, // IID768 - 7, // IID769 - 7, // IID770 - 5, // IID771 - 6, // IID772 - 6, // IID773 - 6, // IID774 - 7, // IID775 - 7, // IID776 - 5, // IID777 - 7, // IID778 - 7, // IID779 - 6, // IID780 - 7, // IID781 - 7, // IID782 - 4, // IID783 - 6, // IID784 - 6, // IID785 - 7, // IID786 - 7, // IID787 - 7, // IID788 - 4, // IID789 - 7, // IID790 - 7, // IID791 + 11, // IID760 + 11, // IID761 + 11, // IID762 + 9, // IID763 + 11, // IID764 + 11, // IID765 + 14, // IID766 + 15, // IID767 + 12, // IID768 + 12, // IID769 + 15, // IID770 + 15, // IID771 + 15, // IID772 + 15, // IID773 + 11, // IID774 + 12, // IID775 + 11, // IID776 + 12, // IID777 + 12, // IID778 + 12, // IID779 + 11, // IID780 + 15, // IID781 + 15, // IID782 + 12, // IID783 + 10, // IID784 + 10, // IID785 + 8, // IID786 + 10, // IID787 + 10, // IID788 + 7, // IID789 + 10, // IID790 + 10, // IID791 7, // IID792 - 7, // IID793 - 7, // IID794 - 4, // IID795 - 7, // IID796 - 7, // IID797 - 6, // IID798 - 10, // IID799 - 10, // IID800 - 5, // IID801 - 7, // IID802 - 7, // IID803 - 10, // IID804 + 10, // IID793 + 10, // IID794 + 10, // IID795 + 10, // IID796 + 10, // IID797 + 10, // IID798 + 7, // IID799 + 7, // IID800 + 10, // IID801 + 10, // IID802 + 10, // IID803 + 4, // IID804 10, // IID805 10, // IID806 - 8, // IID807 - 10, // IID808 - 10, // IID809 - 10, // IID810 - 7, // IID811 - 10, // IID812 - 8, // IID813 - 10, // IID814 - 10, // IID815 - 8, // IID816 - 10, // IID817 - 10, // IID818 - 8, // IID819 - 10, // IID820 - 10, // IID821 - 10, // IID822 - 11, // IID823 - 11, // IID824 - 10, // IID825 - 11, // IID826 - 11, // IID827 - 11, // IID828 - 11, // IID829 - 11, // IID830 - 10, // IID831 - 11, // IID832 - 11, // IID833 - 10, // IID834 - 6, // IID835 - 3, // IID836 - 6, // IID837 + 7, // IID807 + 7, // IID808 + 7, // IID809 + 4, // IID810 + 6, // IID811 + 6, // IID812 + 5, // IID813 + 7, // IID814 + 7, // IID815 + 7, // IID816 + 7, // IID817 + 7, // IID818 + 5, // IID819 + 7, // IID820 + 7, // IID821 + 7, // IID822 + 7, // IID823 + 7, // IID824 + 4, // IID825 + 6, // IID826 + 6, // IID827 + 7, // IID828 + 6, // IID829 + 6, // IID830 + 5, // IID831 + 7, // IID832 + 7, // IID833 + 7, // IID834 + 7, // IID835 + 7, // IID836 + 4, // IID837 6, // IID838 6, // IID839 - 6, // IID840 - 6, // IID841 - 6, // IID842 - 6, // IID843 - 4, // IID844 - 6, // IID845 - 6, // IID846 - 6, // IID847 - 4, // IID848 - 6, // IID849 - 6, // IID850 - 6, // IID851 - 3, // IID852 - 6, // IID853 - 6, // IID854 - 6, // IID855 - 3, // IID856 - 6, // IID857 - 6, // IID858 - 6, // IID859 - 4, // IID860 - 6, // IID861 - 6, // IID862 - 7, // IID863 - 5, // IID864 - 7, // IID865 - 7, // IID866 - 7, // IID867 - 5, // IID868 - 7, // IID869 - 7, // IID870 - 6, // IID871 - 4, // IID872 - 6, // IID873 - 4, // IID874 - 6, // IID875 - 4, // IID876 - 6, // IID877 - 4, // IID878 - 6, // IID879 - 4, // IID880 - 6, // IID881 - 4, // IID882 - 6, // IID883 - 4, // IID884 - 6, // IID885 - 4, // IID886 - 6, // IID887 - 4, // IID888 - 6, // IID889 - 4, // IID890 - 6, // IID891 - 4, // IID892 - 6, // IID893 - 4, // IID894 + 7, // IID840 + 7, // IID841 + 7, // IID842 + 4, // IID843 + 7, // IID844 + 7, // IID845 + 7, // IID846 + 7, // IID847 + 7, // IID848 + 7, // IID849 + 10, // IID850 + 10, // IID851 + 10, // IID852 + 10, // IID853 + 10, // IID854 + 7, // IID855 + 7, // IID856 + 7, // IID857 + 10, // IID858 + 10, // IID859 + 10, // IID860 + 7, // IID861 + 10, // IID862 + 10, // IID863 + 7, // IID864 + 10, // IID865 + 10, // IID866 + 8, // IID867 + 10, // IID868 + 10, // IID869 + 10, // IID870 + 11, // IID871 + 9, // IID872 + 11, // IID873 + 11, // IID874 + 11, // IID875 + 9, // IID876 + 10, // IID877 + 11, // IID878 + 11, // IID879 + 9, // IID880 + 11, // IID881 + 10, // IID882 + 11, // IID883 + 9, // IID884 + 11, // IID885 + 11, // IID886 + 11, // IID887 + 9, // IID888 + 11, // IID889 + 11, // IID890 + 10, // IID891 + 9, // IID892 + 10, // IID893 + 11, // IID894 6, // IID895 4, // IID896 - 6, // IID897 - 4, // IID898 + 3, // IID897 + 6, // IID898 6, // IID899 - 4, // IID900 + 6, // IID900 6, // IID901 - 4, // IID902 - 11, // IID903 - 11, // IID904 - 11, // IID905 - 10, // IID906 - 11, // IID907 - 9, // IID908 - 11, // IID909 - 11, // IID910 - 11, // IID911 - 10, // IID912 - 11, // IID913 - 11, // IID914 - 11, // IID915 - 11, // IID916 - 11, // IID917 - 11, // IID918 + 6, // IID902 + 6, // IID903 + 6, // IID904 + 6, // IID905 + 4, // IID906 + 4, // IID907 + 6, // IID908 + 6, // IID909 + 6, // IID910 + 6, // IID911 + 4, // IID912 + 4, // IID913 + 6, // IID914 + 6, // IID915 + 6, // IID916 + 6, // IID917 + 4, // IID918 + 4, // IID919 + 6, // IID920 + 6, // IID921 + 6, // IID922 + 6, // IID923 + 4, // IID924 + 6, // IID925 + 6, // IID926 + 4, // IID927 + 3, // IID928 + 4, // IID929 + 6, // IID930 + 6, // IID931 + 6, // IID932 + 7, // IID933 + 5, // IID934 + 7, // IID935 + 7, // IID936 + 5, // IID937 + 5, // IID938 + 7, // IID939 + 7, // IID940 + 6, // IID941 + 4, // IID942 + 6, // IID943 + 4, // IID944 + 6, // IID945 + 4, // IID946 + 6, // IID947 + 4, // IID948 + 6, // IID949 + 4, // IID950 + 6, // IID951 + 4, // IID952 + 6, // IID953 + 4, // IID954 + 6, // IID955 + 4, // IID956 + 6, // IID957 + 4, // IID958 + 6, // IID959 + 4, // IID960 + 6, // IID961 + 4, // IID962 + 6, // IID963 + 4, // IID964 + 6, // IID965 + 4, // IID966 + 6, // IID967 + 4, // IID968 + 6, // IID969 + 4, // IID970 + 6, // IID971 + 4, // IID972 + 11, // IID973 + 9, // IID974 + 10, // IID975 + 9, // IID976 + 11, // IID977 + 9, // IID978 + 11, // IID979 + 9, // IID980 + 11, // IID981 + 9, // IID982 + 11, // IID983 + 9, // IID984 + 11, // IID985 + 9, // IID986 + 10, // IID987 + 9, // IID988 + 11, // IID989 + 9, // IID990 + 11, // IID991 + 9, // IID992 + 11, // IID993 + 9, // IID994 + 11, // IID995 + 9, // IID996 + 11, // IID997 + 9, // IID998 + 10, // IID999 + 9, // IID1000 + 11, // IID1001 + 9, // IID1002 + 11, // IID1003 + 9, // IID1004 #endif // _LP64 }; @@ -3152,662 +3404,746 @@ "__ eaddl(r28, Address(r24, rcx, (Address::ScaleFactor)3, -0x6053edc2), r28, false);", // IID268 "__ eaddl(r17, Address(r18, r24, (Address::ScaleFactor)3, -0x1bf71f78), r29, true);", // IID269 "__ eaddl(rcx, Address(r15, r28, (Address::ScaleFactor)1, +0x15b8216), rcx, true);", // IID270 - "__ eorl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false);", // IID271 - "__ eorl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false);", // IID272 - "__ eorl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true);", // IID273 - "__ eorl(r16, Address(r23, -0x165064ff), r16, true);", // IID274 - "__ eorb(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false);", // IID275 - "__ eorb(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false);", // IID276 - "__ eorb(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true);", // IID277 - "__ eorb(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true);", // IID278 - "__ esubl(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false);", // IID279 - "__ esubl(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false);", // IID280 - "__ esubl(r21, Address(r9, -0x1050127a), r13, true);", // IID281 - "__ esubl(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true);", // IID282 - "__ exorl(r15, Address(r18, +0x5c2bbce5), r12, false);", // IID283 - "__ exorl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false);", // IID284 - "__ exorl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true);", // IID285 - "__ exorl(r9, Address(r15, +0x775acdad), r9, true);", // IID286 - "__ exorb(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false);", // IID287 - "__ exorb(r10, Address(r27, +0xa3150de), r10, false);", // IID288 - "__ exorb(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true);", // IID289 - "__ exorb(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true);", // IID290 - "__ eaddl(r21, r15, 1048576, false);", // IID291 - "__ eaddl(rax, r18, 1048576, false);", // IID292 - "__ eaddl(r18, r18, 256, false);", // IID293 - "__ eaddl(r13, r19, 16, true);", // IID294 - "__ eaddl(rax, r23, 16, true);", // IID295 - "__ eaddl(r25, r25, 16777216, true);", // IID296 - "__ eandl(r29, r18, 1048576, false);", // IID297 - "__ eandl(rax, r14, 1048576, false);", // IID298 - "__ eandl(r19, r19, 65536, false);", // IID299 - "__ eandl(r27, r25, 1048576, true);", // IID300 - "__ eandl(rax, r20, 1048576, true);", // IID301 - "__ eandl(r28, r28, 16, true);", // IID302 - "__ eimull(r31, r22, 4096, false);", // IID303 + "__ eandl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false);", // IID271 + "__ eandl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false);", // IID272 + "__ eandl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true);", // IID273 + "__ eandl(r16, Address(r23, -0x165064ff), r16, true);", // IID274 + "__ eorl(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false);", // IID275 + "__ eorl(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false);", // IID276 + "__ eorl(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true);", // IID277 + "__ eorl(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true);", // IID278 + "__ eorb(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false);", // IID279 + "__ eorb(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false);", // IID280 + "__ eorb(r21, Address(r9, -0x1050127a), r13, true);", // IID281 + "__ eorb(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true);", // IID282 + "__ esubl(r15, Address(r18, +0x5c2bbce5), r12, false);", // IID283 + "__ esubl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false);", // IID284 + "__ esubl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true);", // IID285 + "__ esubl(r9, Address(r15, +0x775acdad), r9, true);", // IID286 + "__ exorl(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false);", // IID287 + "__ exorl(r10, Address(r27, +0xa3150de), r10, false);", // IID288 + "__ exorl(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true);", // IID289 + "__ exorl(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true);", // IID290 + "__ exorb(r16, Address(r21, r15, (Address::ScaleFactor)0, -0x1403b60d), r18, false);", // IID291 + "__ exorb(r13, Address(r19, r23, (Address::ScaleFactor)2, +0x237ef1e1), r13, false);", // IID292 + "__ exorb(r29, Address(r18, r14, (Address::ScaleFactor)2, +0x5cc0095b), r14, true);", // IID293 + "__ exorb(r27, Address(r25, r20, (Address::ScaleFactor)3, +0x1cf7b958), r27, true);", // IID294 + "__ eaddl(r16, r24, 16, false);", // IID295 + "__ eaddl(rax, r24, 16, false);", // IID296 + "__ eaddl(r21, r21, 65536, false);", // IID297 + "__ eaddl(r24, r8, 1048576, true);", // IID298 + "__ eaddl(rax, r13, 1048576, true);", // IID299 + "__ eaddl(r29, r29, 16777216, true);", // IID300 + "__ eandl(r12, r12, 16, false);", // IID301 + "__ eandl(rax, r30, 16, false);", // IID302 + "__ eandl(r24, r24, 16, false);", // IID303 + "__ eandl(r8, r12, 1, true);", // IID304 + "__ eandl(rax, r13, 1, true);", // IID305 + "__ eandl(r25, r25, 16, true);", // IID306 + "__ eimull(r18, r23, 65536, false);", // IID307 + "__ eimull(rax, r9, 65536, false);", // IID308 + "__ eimull(r26, r26, 268435456, false);", // IID309 + "__ eimull(r25, r21, 1, true);", // IID310 + "__ eimull(rax, r24, 1, true);", // IID311 + "__ eimull(r24, r24, 16777216, true);", // IID312 + "__ eorl(r30, r26, 1, false);", // IID313 + "__ eorl(rax, r22, 1, false);", // IID314 + "__ eorl(r17, r17, 1048576, false);", // IID315 + "__ eorl(r24, r8, 1, true);", // IID316 + "__ eorl(rax, r27, 1, true);", // IID317 #endif // _LP64 - "__ eimull(rax, rbx, 4096, false);", // IID304 + "__ eorl(rdx, rdx, 268435456, true);", // IID318 #ifdef _LP64 - "__ eimull(r24, r24, 1048576, false);", // IID305 - "__ eimull(r21, r16, 65536, true);", // IID306 - "__ eimull(rax, r24, 65536, true);", // IID307 - "__ eimull(r13, r13, 16, true);", // IID308 - "__ eorl(r29, r8, 16777216, false);", // IID309 - "__ eorl(rax, r12, 16777216, false);", // IID310 - "__ eorl(r30, r30, 4096, false);", // IID311 - "__ eorl(r24, rdx, 16, true);", // IID312 - "__ eorl(rax, r8, 16, true);", // IID313 - "__ eorl(r13, r13, 4096, true);", // IID314 - "__ ercll(r25, r13, 1);", // IID315 - "__ ercll(rax, r18, 1);", // IID316 - "__ ercll(r9, r9, 16);", // IID317 - "__ eroll(r26, r25, 8, false);", // IID318 + "__ ercll(r22, r22, 8);", // IID319 + "__ ercll(rax, r23, 8);", // IID320 + "__ ercll(r19, r19, 4);", // IID321 + "__ eroll(r30, r24, 2, false);", // IID322 + "__ eroll(rax, r29, 2, false);", // IID323 + "__ eroll(r8, r8, 2, false);", // IID324 + "__ eroll(r18, r24, 16, true);", // IID325 + "__ eroll(rax, r13, 16, true);", // IID326 + "__ eroll(r24, r24, 1, true);", // IID327 + "__ erorl(r28, r17, 16, false);", // IID328 + "__ erorl(rax, r24, 16, false);", // IID329 + "__ erorl(r17, r17, 4, false);", // IID330 + "__ erorl(r24, rcx, 4, true);", // IID331 + "__ erorl(rax, r16, 4, true);", // IID332 + "__ erorl(r15, r15, 2, true);", // IID333 + "__ esall(r14, r27, 4, false);", // IID334 + "__ esall(rax, r23, 4, false);", // IID335 + "__ esall(r30, r30, 4, false);", // IID336 + "__ esall(r27, rdx, 2, true);", // IID337 + "__ esall(rax, r19, 2, true);", // IID338 + "__ esall(r20, r20, 2, true);", // IID339 + "__ esarl(r21, r23, 1, false);", // IID340 + "__ esarl(rax, r30, 1, false);", // IID341 + "__ esarl(r25, r25, 2, false);", // IID342 + "__ esarl(r24, r19, 4, true);", // IID343 + "__ esarl(rax, r14, 4, true);", // IID344 + "__ esarl(r26, r26, 16, true);", // IID345 + "__ eshll(r22, r13, 8, false);", // IID346 + "__ eshll(rax, r24, 8, false);", // IID347 + "__ eshll(r14, r14, 16, false);", // IID348 + "__ eshll(r28, r25, 8, true);", // IID349 + "__ eshll(rax, r10, 8, true);", // IID350 + "__ eshll(r20, r20, 1, true);", // IID351 + "__ eshrl(r12, rbx, 4, false);", // IID352 + "__ eshrl(rax, r23, 4, false);", // IID353 + "__ eshrl(r28, r28, 16, false);", // IID354 + "__ eshrl(r24, r30, 4, true);", // IID355 + "__ eshrl(rax, r31, 4, true);", // IID356 + "__ eshrl(r31, r31, 2, true);", // IID357 + "__ esubl(r20, r10, 256, false);", // IID358 + "__ esubl(rax, r13, 256, false);", // IID359 + "__ esubl(r25, r25, 256, false);", // IID360 + "__ esubl(r23, r12, 268435456, true);", // IID361 + "__ esubl(rax, r16, 268435456, true);", // IID362 + "__ esubl(r31, r31, 1, true);", // IID363 + "__ exorl(r9, r15, 16777216, false);", // IID364 + "__ exorl(rax, r13, 16777216, false);", // IID365 + "__ exorl(r28, r28, 16, false);", // IID366 + "__ exorl(r29, r22, 16, true);", // IID367 #endif // _LP64 - "__ eroll(rax, rdx, 8, false);", // IID319 + "__ exorl(rax, rbx, 16, true);", // IID368 #ifdef _LP64 - "__ eroll(r24, r24, 16, false);", // IID320 - "__ eroll(r24, rcx, 8, true);", // IID321 - "__ eroll(rax, r30, 8, true);", // IID322 - "__ eroll(r28, r28, 16, true);", // IID323 - "__ erorl(r17, r28, 4, false);", // IID324 + "__ exorl(r8, r8, 16, true);", // IID369 + "__ esubl_imm32(r16, r13, 4194304, false);", // IID370 + "__ esubl_imm32(rax, r12, 4194304, false);", // IID371 + "__ esubl_imm32(r17, r17, 67108864, false);", // IID372 + "__ esubl_imm32(r22, r26, 1073741824, true);", // IID373 + "__ esubl_imm32(rax, r10, 1073741824, true);", // IID374 + "__ esubl_imm32(r11, r11, 1073741824, true);", // IID375 + "__ eaddl(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false);", // IID376 + "__ eaddl(r30, r30, Address(r18, r19, (Address::ScaleFactor)2, +0x25f990cf), false);", // IID377 + "__ eaddl(rcx, r25, Address(r19, r16, (Address::ScaleFactor)0, +0x482d5dbc), true);", // IID378 + "__ eaddl(r9, r9, Address(r11, +0x43d5ee01), true);", // IID379 + "__ eandl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false);", // IID380 + "__ eandl(r27, r27, Address(r13, r15, (Address::ScaleFactor)3, -0x1268b895), false);", // IID381 + "__ eandl(r9, r23, Address(r22, r30, (Address::ScaleFactor)0, -0x715acbb), true);", // IID382 + "__ eandl(rbx, rbx, Address(r28, r16, (Address::ScaleFactor)2, +0xb0223ee), true);", // IID383 + "__ eimull(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false);", // IID384 + "__ eimull(r17, r17, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), false);", // IID385 + "__ eimull(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), true);", // IID386 + "__ eimull(rbx, rbx, Address(r28, r22, (Address::ScaleFactor)3, -0x519d9a27), true);", // IID387 + "__ eorl(r17, rcx, Address(r14, +0x10642223), false);", // IID388 + "__ eorl(r26, r26, Address(r31, -0x7a9a83ba), false);", // IID389 + "__ eorl(r15, r22, Address(r12, r12, (Address::ScaleFactor)2, +0x743b6997), true);", // IID390 + "__ eorl(r8, r8, Address(rdx, r22, (Address::ScaleFactor)3, -0x588414dc), true);", // IID391 + "__ esubl(rcx, r28, Address(r30, r13, (Address::ScaleFactor)2, +0xe9310e5), false);", // IID392 + "__ esubl(rcx, rcx, Address(r30, r10, (Address::ScaleFactor)1, -0x1b076ed1), false);", // IID393 + "__ esubl(r9, r21, Address(r30, +0x2f79ffd3), true);", // IID394 + "__ esubl(r16, r16, Address(rdx, r14, (Address::ScaleFactor)2, +0x675d71c1), true);", // IID395 + "__ exorl(r27, r28, Address(rbx, r26, (Address::ScaleFactor)2, -0x78c20b81), false);", // IID396 + "__ exorl(r14, r14, Address(r31, r19, (Address::ScaleFactor)1, -0x4ff251cc), false);", // IID397 + "__ exorl(r20, r18, Address(r13, r16, (Address::ScaleFactor)2, -0x19efc6e2), true);", // IID398 + "__ exorl(r19, r19, Address(r13, r23, (Address::ScaleFactor)1, -0x2d1bd8aa), true);", // IID399 + "__ exorb(r29, r17, Address(rdx, r29, (Address::ScaleFactor)2, +0x66573e84), false);", // IID400 + "__ exorb(r22, r22, Address(r24, r25, (Address::ScaleFactor)3, +0x3a94a93f), false);", // IID401 + "__ exorb(r13, r29, Address(r15, r23, (Address::ScaleFactor)1, +0x76d43532), true);", // IID402 + "__ exorb(r15, r15, Address(r13, r9, (Address::ScaleFactor)0, -0x474e6d1a), true);", // IID403 + "__ exorw(r17, r16, Address(r23, rdx, (Address::ScaleFactor)0, +0x562a291), false);", // IID404 + "__ exorw(r29, r29, Address(r18, r28, (Address::ScaleFactor)3, -0x541967f2), false);", // IID405 + "__ exorw(r27, r11, Address(r10, +0xa911c5a), true);", // IID406 + "__ exorw(r31, r31, Address(r30, r19, (Address::ScaleFactor)2, -0xf6a3da), true);", // IID407 + "__ eaddl(r12, r13, r23, false);", // IID408 + "__ eaddl(r28, r28, r20, false);", // IID409 + "__ eaddl(r20, r24, r20, false);", // IID410 + "__ eaddl(r11, r10, r15, true);", // IID411 + "__ eaddl(r19, r19, r20, true);", // IID412 + "__ eaddl(r23, r15, r23, true);", // IID413 + "__ eandl(r26, r19, r24, false);", // IID414 + "__ eandl(r23, r23, r28, false);", // IID415 + "__ eandl(r11, r13, r11, false);", // IID416 + "__ eandl(r13, rdx, r31, true);", // IID417 + "__ eandl(r23, r23, r23, true);", // IID418 + "__ eandl(r9, r27, r9, true);", // IID419 + "__ eimull(r21, r20, r24, false);", // IID420 + "__ eimull(r21, r21, r29, false);", // IID421 + "__ eimull(rbx, r11, rbx, false);", // IID422 + "__ eimull(r21, rbx, rcx, true);", // IID423 + "__ eimull(r31, r31, r21, true);", // IID424 + "__ eimull(r15, r25, r15, true);", // IID425 + "__ eorw(r30, r23, r25, false);", // IID426 + "__ eorw(r18, r18, rcx, false);", // IID427 + "__ eorw(r10, rcx, r10, false);", // IID428 + "__ eorw(r31, r21, r26, true);", // IID429 + "__ eorw(r21, r21, r19, true);", // IID430 #endif // _LP64 - "__ erorl(rax, rdx, 4, false);", // IID325 + "__ eorw(rdx, rbx, rdx, true);", // IID431 #ifdef _LP64 - "__ erorl(r8, r8, 16, false);", // IID326 - "__ erorl(r19, rdx, 16, true);", // IID327 - "__ erorl(rax, r31, 16, true);", // IID328 - "__ erorl(r22, r22, 8, true);", // IID329 - "__ esall(r23, r25, 16, false);", // IID330 - "__ esall(rax, r14, 16, false);", // IID331 - "__ esall(r31, r31, 8, false);", // IID332 - "__ esall(r30, r24, 2, true);", // IID333 - "__ esall(rax, r29, 2, true);", // IID334 - "__ esall(r8, r8, 2, true);", // IID335 - "__ esarl(r18, r24, 16, false);", // IID336 - "__ esarl(rax, r13, 16, false);", // IID337 - "__ esarl(r24, r24, 1, false);", // IID338 - "__ esarl(r28, r17, 16, true);", // IID339 - "__ esarl(rax, r24, 16, true);", // IID340 - "__ esarl(r17, r17, 4, true);", // IID341 - "__ eshll(r24, rcx, 4, false);", // IID342 - "__ eshll(rax, r16, 4, false);", // IID343 - "__ eshll(r15, r15, 2, false);", // IID344 - "__ eshll(r14, r27, 4, true);", // IID345 - "__ eshll(rax, r23, 4, true);", // IID346 - "__ eshll(r30, r30, 4, true);", // IID347 - "__ eshrl(r27, rdx, 2, false);", // IID348 - "__ eshrl(rax, r19, 2, false);", // IID349 - "__ eshrl(r20, r20, 2, false);", // IID350 - "__ eshrl(r21, r23, 1, true);", // IID351 - "__ eshrl(rax, r30, 1, true);", // IID352 - "__ eshrl(r25, r25, 2, true);", // IID353 - "__ esubl(r24, r19, 1048576, false);", // IID354 - "__ esubl(rax, r14, 1048576, false);", // IID355 - "__ esubl(r22, r22, 268435456, false);", // IID356 - "__ esubl(r24, r24, 65536, true);", // IID357 - "__ esubl(rax, r14, 65536, true);", // IID358 - "__ esubl(r28, r28, 268435456, true);", // IID359 - "__ exorl(rbx, r20, 256, false);", // IID360 - "__ exorl(rax, r15, 256, false);", // IID361 -#endif // _LP64 - "__ exorl(rbx, rbx, 4096, false);", // IID362 -#ifdef _LP64 - "__ exorl(r24, r30, 65536, true);", // IID363 - "__ exorl(rax, r31, 65536, true);", // IID364 - "__ exorl(r31, r31, 4096, true);", // IID365 - "__ esubl_imm32(r20, r10, 1048576, false);", // IID366 - "__ esubl_imm32(rax, r13, 1048576, false);", // IID367 - "__ esubl_imm32(r25, r25, 1048576, false);", // IID368 - "__ esubl_imm32(r23, r12, 1073741824, true);", // IID369 - "__ esubl_imm32(rax, r16, 1073741824, true);", // IID370 - "__ esubl_imm32(r31, r31, 65536, true);", // IID371 - "__ eaddl(r17, r13, Address(r9, +0x7fef2f98), false);", // IID372 - "__ eaddl(r29, r8, Address(r22, -0x4df70aac), true);", // IID373 - "__ eandl(r13, r17, Address(r12, r15, (Address::ScaleFactor)3, +0x50a8a902), false);", // IID374 - "__ eandl(r22, r25, Address(r26, r10, (Address::ScaleFactor)2, +0x70ea2754), true);", // IID375 - "__ eimull(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false);", // IID376 - "__ eimull(r30, r18, Address(r18, r19, (Address::ScaleFactor)2, -0x7fcd28c7), true);", // IID377 - "__ eorl(r16, r31, Address(r25, r11, (Address::ScaleFactor)3, +0x482d5dbc), false);", // IID378 - "__ eorl(r9, r27, Address(r11, +0x43d5ee01), true);", // IID379 - "__ esubl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false);", // IID380 - "__ esubl(r27, r22, Address(r13, r15, (Address::ScaleFactor)1, +0x771f0da7), true);", // IID381 - "__ exorl(r9, r30, Address(r9, r22, (Address::ScaleFactor)3, -0x4ad6c88e), false);", // IID382 - "__ exorl(r11, r16, Address(rbx, r28, (Address::ScaleFactor)2, +0xb0223ee), true);", // IID383 - "__ exorb(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false);", // IID384 - "__ exorb(r17, r30, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), true);", // IID385 - "__ exorw(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), false);", // IID386 - "__ exorw(rbx, r22, Address(r28, r22, (Address::ScaleFactor)0, -0x7d30a0b1), true);", // IID387 - "__ eaddl(r14, r24, rcx, false);", // IID388 - "__ eaddl(r8, r8, r17, false);", // IID389 - "__ eaddl(r26, r24, r12, true);", // IID390 - "__ eaddl(r24, r24, r23, true);", // IID391 - "__ eandl(r13, r26, r31, false);", // IID392 - "__ eandl(r11, r11, r8, false);", // IID393 - "__ eandl(rcx, r19, r15, true);", // IID394 - "__ eandl(r12, r12, r12, true);", // IID395 - "__ eimull(r22, r20, r19, false);", // IID396 - "__ eimull(r8, r8, rdx, false);", // IID397 - "__ eimull(r22, r27, r23, true);", // IID398 - "__ eimull(r9, r9, r18, true);", // IID399 - "__ eorw(rcx, r30, r13, false);", // IID400 - "__ eorw(r28, r28, r19, false);", // IID401 - "__ eorw(r12, r30, r27, true);", // IID402 - "__ eorw(r8, r8, r22, true);", // IID403 - "__ eorl(r16, rcx, r30, false);", // IID404 - "__ eorl(r10, r10, r25, false);", // IID405 - "__ eorl(r15, r17, r17, true);", // IID406 - "__ eorl(r9, r9, r30, true);", // IID407 - "__ eshldl(r20, r21, r8, false);", // IID408 - "__ eshldl(r26, r26, r14, false);", // IID409 - "__ eshldl(r16, rdx, r14, true);", // IID410 - "__ eshldl(r19, r19, r8, true);", // IID411 - "__ eshrdl(r27, rbx, r26, false);", // IID412 - "__ eshrdl(r28, r28, r19, false);", // IID413 - "__ eshrdl(rcx, r11, r14, true);", // IID414 - "__ eshrdl(r31, r31, r19, true);", // IID415 - "__ esubl(r26, r13, r25, false);", // IID416 - "__ esubl(r24, r24, r11, false);", // IID417 - "__ esubl(r18, r20, r13, true);", // IID418 - "__ esubl(r16, r16, r18, true);", // IID419 - "__ exorl(r19, r17, r8, false);", // IID420 - "__ exorl(r19, r19, r13, false);", // IID421 - "__ exorl(r23, r13, r15, true);", // IID422 - "__ exorl(r11, r11, r29, true);", // IID423 - "__ eshldl(r29, r17, r17, 1, false);", // IID424 - "__ eshldl(r22, r22, r24, 4, false);", // IID425 - "__ eshldl(r8, r28, r11, 16, true);", // IID426 - "__ eshldl(r15, r15, r23, 4, true);", // IID427 - "__ eshrdl(r29, r22, r16, 4, false);", // IID428 - "__ eshrdl(r13, r13, r9, 4, false);", // IID429 - "__ eshrdl(r15, r21, r12, 2, true);", // IID430 - "__ eshrdl(r17, r17, r23, 2, true);", // IID431 - "__ ecmovl (Assembler::Condition::overflow, rdx, r16, r29);", // IID432 - "__ ecmovl (Assembler::Condition::overflow, r10, r10, r21);", // IID433 - "__ ecmovl (Assembler::Condition::noOverflow, r17, r29, r18);", // IID434 - "__ ecmovl (Assembler::Condition::noOverflow, r28, r28, r24);", // IID435 - "__ ecmovl (Assembler::Condition::below, r10, r20, r27);", // IID436 - "__ ecmovl (Assembler::Condition::below, r10, r10, r14);", // IID437 - "__ ecmovl (Assembler::Condition::aboveEqual, r11, r27, rcx);", // IID438 - "__ ecmovl (Assembler::Condition::aboveEqual, r22, r22, r15);", // IID439 - "__ ecmovl (Assembler::Condition::zero, r31, r30, r19);", // IID440 - "__ ecmovl (Assembler::Condition::zero, r19, r19, r26);", // IID441 - "__ ecmovl (Assembler::Condition::notZero, r21, r14, r26);", // IID442 - "__ ecmovl (Assembler::Condition::notZero, r20, r20, r15);", // IID443 - "__ ecmovl (Assembler::Condition::belowEqual, r12, r13, r23);", // IID444 - "__ ecmovl (Assembler::Condition::belowEqual, r28, r28, r20);", // IID445 - "__ ecmovl (Assembler::Condition::above, r20, r24, r11);", // IID446 - "__ ecmovl (Assembler::Condition::above, r10, r10, r15);", // IID447 - "__ ecmovl (Assembler::Condition::negative, r19, r20, r23);", // IID448 - "__ ecmovl (Assembler::Condition::negative, r15, r15, r26);", // IID449 - "__ ecmovl (Assembler::Condition::positive, r19, r24, r23);", // IID450 - "__ ecmovl (Assembler::Condition::positive, r28, r28, r11);", // IID451 - "__ ecmovl (Assembler::Condition::parity, r13, r13, rdx);", // IID452 - "__ ecmovl (Assembler::Condition::parity, r31, r31, r23);", // IID453 - "__ ecmovl (Assembler::Condition::noParity, r23, r9, r27);", // IID454 - "__ ecmovl (Assembler::Condition::noParity, r21, r21, r20);", // IID455 - "__ ecmovl (Assembler::Condition::less, r24, r21, r29);", // IID456 - "__ ecmovl (Assembler::Condition::less, rbx, rbx, r11);", // IID457 - "__ ecmovl (Assembler::Condition::greaterEqual, r21, rbx, rcx);", // IID458 - "__ ecmovl (Assembler::Condition::greaterEqual, r31, r31, r21);", // IID459 - "__ ecmovl (Assembler::Condition::lessEqual, r15, r25, r30);", // IID460 - "__ ecmovl (Assembler::Condition::lessEqual, r23, r23, r25);", // IID461 - "__ ecmovl (Assembler::Condition::greater, r18, rcx, r10);", // IID462 - "__ ecmovl (Assembler::Condition::greater, rcx, rcx, r31);", // IID463 - "__ ecmovl (Assembler::Condition::overflow, r21, r19, Address(r26, -0x6e290873));", // IID464 - "__ ecmovl (Assembler::Condition::noOverflow, r24, r19, Address(r22, rcx, (Address::ScaleFactor)0, +0x11f85f9a));", // IID465 - "__ ecmovl (Assembler::Condition::below, r17, r24, Address(r20, +0x534d775e));", // IID466 - "__ ecmovl (Assembler::Condition::aboveEqual, r20, r18, Address(r20, -0x47c94ecd));", // IID467 - "__ ecmovl (Assembler::Condition::zero, r9, r13, Address(r23, -0x4b83c563));", // IID468 - "__ ecmovl (Assembler::Condition::notZero, r11, r25, Address(r24, r14, (Address::ScaleFactor)1, -0x446507af));", // IID469 - "__ ecmovl (Assembler::Condition::belowEqual, r14, r24, Address(r30, r13, (Address::ScaleFactor)2, +0xd0661d));", // IID470 - "__ ecmovl (Assembler::Condition::above, r13, r25, Address(r14, r27, (Address::ScaleFactor)3, +0x47e1403));", // IID471 - "__ ecmovl (Assembler::Condition::negative, r24, r19, Address(rcx, rdx, (Address::ScaleFactor)3, -0x644a5318));", // IID472 - "__ ecmovl (Assembler::Condition::positive, r26, r24, Address(r22, r22, (Address::ScaleFactor)0, +0x70352446));", // IID473 - "__ ecmovl (Assembler::Condition::parity, r19, r26, Address(r8, r30, (Address::ScaleFactor)2, +0x78a12f5c));", // IID474 - "__ ecmovl (Assembler::Condition::noParity, r29, r11, Address(r25, r20, (Address::ScaleFactor)0, +0x27a8303a));", // IID475 - "__ ecmovl (Assembler::Condition::less, r22, r24, Address(r27, r16, (Address::ScaleFactor)1, +0x2541a10));", // IID476 - "__ ecmovl (Assembler::Condition::greaterEqual, r31, r15, Address(r8, r16, (Address::ScaleFactor)3, +0x558e3251));", // IID477 - "__ ecmovl (Assembler::Condition::lessEqual, r27, r18, Address(r8, r10, (Address::ScaleFactor)0, -0x471987b7));", // IID478 - "__ ecmovl (Assembler::Condition::greater, r18, r16, Address(r18, r19, (Address::ScaleFactor)2, -0x120ae81e));", // IID479 - "__ adcq(rbx, r31);", // IID480 - "__ cmpq(r30, r31);", // IID481 - "__ imulq(r29, r28);", // IID482 - "__ popcntq(r25, r10);", // IID483 - "__ sbbq(r24, r20);", // IID484 - "__ subq(r16, rdx);", // IID485 - "__ tzcntq(r26, r28);", // IID486 - "__ lzcntq(r28, r9);", // IID487 - "__ addq(r20, r24);", // IID488 - "__ andq(r24, r29);", // IID489 - "__ orq(r23, r27);", // IID490 - "__ xorq(r15, r12);", // IID491 - "__ movq(r18, r19);", // IID492 - "__ bsfq(r31, rcx);", // IID493 - "__ bsrq(r9, r13);", // IID494 - "__ btq(r20, rcx);", // IID495 - "__ xchgq(r8, r21);", // IID496 - "__ testq(r24, r14);", // IID497 - "__ addq(Address(rcx, r23, (Address::ScaleFactor)2, +0x4ff06c4d), r29);", // IID498 - "__ andq(Address(r24, r10, (Address::ScaleFactor)1, -0x75d9a189), r26);", // IID499 - "__ cmpq(Address(rbx, rbx, (Address::ScaleFactor)0, +0x4033d59c), r17);", // IID500 - "__ orq(Address(r22, r12, (Address::ScaleFactor)3, -0x3893347d), r18);", // IID501 - "__ xorq(Address(r20, r23, (Address::ScaleFactor)3, +0x4b311560), r12);", // IID502 - "__ subq(Address(r10, r28, (Address::ScaleFactor)2, +0x5c3a2657), r29);", // IID503 - "__ movq(Address(r13, r25, (Address::ScaleFactor)3, +0x1a3d6f3f), r22);", // IID504 - "__ xaddq(Address(r17, r24, (Address::ScaleFactor)3, -0x35addbd8), r25);", // IID505 - "__ andq(Address(r25, +0x632184c3), 16777216);", // IID506 - "__ addq(Address(r13, r13, (Address::ScaleFactor)0, -0x3972eac6), 16777216);", // IID507 - "__ cmpq(Address(r9, -0x13b4c806), 4096);", // IID508 - "__ sarq(Address(r31, +0x4fa7f551), 1);", // IID509 - "__ salq(Address(r21, r31, (Address::ScaleFactor)2, +0x31aa8232), 1);", // IID510 - "__ sbbq(Address(r24, r31, (Address::ScaleFactor)2, -0x466538b7), 268435456);", // IID511 - "__ shrq(Address(r28, r22, (Address::ScaleFactor)0, -0x3efe85b1), 2);", // IID512 - "__ subq(Address(r16, -0x1389a3eb), 1048576);", // IID513 - "__ xorq(Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615), 16);", // IID514 - "__ orq(Address(r12, r28, (Address::ScaleFactor)1, -0x34c898e2), 1);", // IID515 - "__ movq(Address(rcx, r24, (Address::ScaleFactor)2, -0x1644eb08), 256);", // IID516 - "__ testq(Address(r29, -0x7d23890b), -65536);", // IID517 - "__ addq(r23, Address(rcx, r19, (Address::ScaleFactor)2, +0x70eac654));", // IID518 - "__ andq(rdx, Address(r24, r15, (Address::ScaleFactor)0, -0x204ddaa9));", // IID519 - "__ cmpq(rdx, Address(r23, r11, (Address::ScaleFactor)3, +0x32c930bd));", // IID520 - "__ lzcntq(r28, Address(rdx, -0x5433c28f));", // IID521 - "__ orq(r22, Address(r19, r14, (Address::ScaleFactor)1, -0x2cc67d38));", // IID522 - "__ adcq(r10, Address(r10, +0x3d7c59f));", // IID523 - "__ imulq(r10, Address(r8, r8, (Address::ScaleFactor)3, -0xe61862d));", // IID524 - "__ popcntq(r23, Address(r29, -0x777ed96d));", // IID525 - "__ sbbq(rcx, Address(rbx, r19, (Address::ScaleFactor)1, +0x53c601cb));", // IID526 - "__ subq(r14, Address(r17, rbx, (Address::ScaleFactor)0, -0x768bf073));", // IID527 - "__ tzcntq(r29, Address(r10, r19, (Address::ScaleFactor)1, +0x30c98d3c));", // IID528 - "__ xorq(r10, Address(r16, r27, (Address::ScaleFactor)0, -0x3d08d602));", // IID529 - "__ movq(r18, Address(r28, r28, (Address::ScaleFactor)3, -0x62fbac91));", // IID530 - "__ leaq(rbx, Address(rcx, +0x450602a5));", // IID531 - "__ cvttsd2siq(r12, Address(r30, r31, (Address::ScaleFactor)0, -0x6798a630));", // IID532 - "__ xchgq(r31, Address(r24, r10, (Address::ScaleFactor)1, -0x706712ed));", // IID533 - "__ testq(r14, Address(r13, r20, (Address::ScaleFactor)3, +0x171081f2));", // IID534 - "__ addq(r31, 16);", // IID535 - "__ andq(r25, 16);", // IID536 - "__ adcq(r23, 256);", // IID537 - "__ cmpq(r19, 268435456);", // IID538 - "__ rclq(r31, 1);", // IID539 - "__ rcrq(r17, 1);", // IID540 - "__ rolq(r25, 2);", // IID541 - "__ rorq(r17, 4);", // IID542 - "__ sarq(r28, 1);", // IID543 - "__ salq(r15, 4);", // IID544 - "__ sbbq(rbx, 65536);", // IID545 - "__ shlq(r21, 1);", // IID546 - "__ shrq(r10, 1);", // IID547 - "__ subq(r14, 16);", // IID548 - "__ xorq(r18, 268435456);", // IID549 - "__ movq(r23, 16);", // IID550 - "__ mov64(r12, 1099511627776);", // IID551 - "__ btq(r14, 4);", // IID552 - "__ testq(r24, -4096);", // IID553 - "__ orq_imm32(r19, 1048576);", // IID554 - "__ subq_imm32(rcx, 268435456);", // IID555 - "__ cmovq(Assembler::Condition::overflow, rdx, Address(r19, rbx, (Address::ScaleFactor)3, +0x211c8c4));", // IID556 - "__ cmovq(Assembler::Condition::noOverflow, rbx, Address(r21, +0x49267743));", // IID557 - "__ cmovq(Assembler::Condition::below, r21, Address(r8, r28, (Address::ScaleFactor)1, -0x4c8c2946));", // IID558 - "__ cmovq(Assembler::Condition::aboveEqual, r12, Address(r26, r20, (Address::ScaleFactor)0, -0x264df89c));", // IID559 - "__ cmovq(Assembler::Condition::zero, r17, Address(r28, r9, (Address::ScaleFactor)2, +0x3497196b));", // IID560 - "__ cmovq(Assembler::Condition::notZero, r13, Address(r15, r23, (Address::ScaleFactor)1, -0x27a30999));", // IID561 - "__ cmovq(Assembler::Condition::belowEqual, r22, Address(r22, +0xf39ab05));", // IID562 - "__ cmovq(Assembler::Condition::above, rcx, Address(r22, r26, (Address::ScaleFactor)3, -0x48c954c));", // IID563 - "__ cmovq(Assembler::Condition::negative, r25, Address(r19, r21, (Address::ScaleFactor)0, +0xe405b0b));", // IID564 - "__ cmovq(Assembler::Condition::positive, r12, Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b));", // IID565 - "__ cmovq(Assembler::Condition::parity, rbx, Address(r30, r10, (Address::ScaleFactor)1, -0x19798323));", // IID566 - "__ cmovq(Assembler::Condition::noParity, r21, Address(r24, r31, (Address::ScaleFactor)0, -0x5731652b));", // IID567 - "__ cmovq(Assembler::Condition::less, r18, Address(r8, r10, (Address::ScaleFactor)1, -0x5613be89));", // IID568 - "__ cmovq(Assembler::Condition::greaterEqual, r28, Address(r21, r21, (Address::ScaleFactor)3, +0x65a0fdc4));", // IID569 - "__ cmovq(Assembler::Condition::lessEqual, r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c));", // IID570 - "__ cmovq(Assembler::Condition::greater, r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38));", // IID571 - "__ call(r23);", // IID572 - "__ divq(r30);", // IID573 - "__ idivq(r19);", // IID574 - "__ imulq(r9);", // IID575 - "__ mulq(r13);", // IID576 - "__ negq(r16);", // IID577 - "__ notq(r29);", // IID578 - "__ rolq(rcx);", // IID579 - "__ rorq(r25);", // IID580 - "__ sarq(r8);", // IID581 - "__ salq(r27);", // IID582 - "__ shlq(r30);", // IID583 - "__ shrq(r23);", // IID584 - "__ incrementq(rbx);", // IID585 - "__ decrementq(r14);", // IID586 - "__ pushp(r21);", // IID587 - "__ popp(r21);", // IID588 - "__ call(Address(r20, r21, (Address::ScaleFactor)1, +0x56c6af2f));", // IID589 - "__ mulq(Address(r31, r19, (Address::ScaleFactor)3, -0x1b4eb23));", // IID590 - "__ negq(Address(r27, r27, (Address::ScaleFactor)0, -0x58dbfc1f));", // IID591 - "__ sarq(Address(rbx, r22, (Address::ScaleFactor)2, -0x606349d1));", // IID592 - "__ salq(Address(r26, r23, (Address::ScaleFactor)3, +0xb95a079));", // IID593 - "__ shrq(Address(r14, r26, (Address::ScaleFactor)0, +0x3544e09));", // IID594 - "__ incrementq(Address(r27, rdx, (Address::ScaleFactor)0, +0x120b3250));", // IID595 - "__ decrementq(Address(r9, r25, (Address::ScaleFactor)2, -0x34aaeccb));", // IID596 - "__ imulq(r20, Address(r16, r28, (Address::ScaleFactor)1, -0x59de05a5), 1048576);", // IID597 - "__ imulq(r17, r23, 256);", // IID598 - "__ shldq(r19, r11, 8);", // IID599 - "__ shrdq(r28, r10, 8);", // IID600 - "__ pop2(r29, r26);", // IID601 - "__ pop2p(r22, r10);", // IID602 - "__ push2(r25, r30);", // IID603 - "__ push2p(r28, r15);", // IID604 - "__ movzbq(r11, Address(r29, r19, (Address::ScaleFactor)2, -0x12368d34));", // IID605 - "__ movzwq(r14, Address(r8, r30, (Address::ScaleFactor)2, -0x4a9392de));", // IID606 - "__ movsbq(r28, Address(r23, r15, (Address::ScaleFactor)0, +0x6189cb54));", // IID607 - "__ movswq(r28, Address(rbx, r23, (Address::ScaleFactor)3, -0x2de86561));", // IID608 - "__ movzbq(r11, rcx);", // IID609 - "__ movzwq(r30, r15);", // IID610 - "__ movsbq(r14, rcx);", // IID611 - "__ movswq(r23, r9);", // IID612 - "__ cmpxchgq(r12, Address(r13, r10, (Address::ScaleFactor)1, -0x7c62c3a));", // IID613 - "__ eidivq(rcx, false);", // IID614 - "__ eidivq(r15, true);", // IID615 - "__ edivq(r23, false);", // IID616 - "__ edivq(r24, true);", // IID617 - "__ eimulq(r27, false);", // IID618 - "__ eimulq(r30, true);", // IID619 - "__ emulq(r12, false);", // IID620 - "__ emulq(rcx, true);", // IID621 - "__ emulq(Address(r13, r9, (Address::ScaleFactor)3, -0x226aab94), false);", // IID622 - "__ emulq(Address(r13, r24, (Address::ScaleFactor)3, -0x286c7605), true);", // IID623 - "__ eimulq(r21, r30, false);", // IID624 - "__ eimulq(r17, r17, false);", // IID625 - "__ eimulq(r29, r12, true);", // IID626 - "__ eimulq(r30, r30, true);", // IID627 - "__ elzcntq(r24, r15, false);", // IID628 - "__ elzcntq(r25, r25, false);", // IID629 - "__ elzcntq(r25, r21, true);", // IID630 - "__ elzcntq(r22, r22, true);", // IID631 - "__ enegq(r17, r30, false);", // IID632 - "__ enegq(r17, r17, false);", // IID633 - "__ enegq(r31, r17, true);", // IID634 - "__ enegq(r29, r29, true);", // IID635 - "__ enotq(r10, r9);", // IID636 - "__ enotq(r24, r24);", // IID637 - "__ epopcntq(r28, r15, false);", // IID638 - "__ epopcntq(r10, r10, false);", // IID639 - "__ epopcntq(r27, r30, true);", // IID640 - "__ epopcntq(r28, r28, true);", // IID641 - "__ erolq(r28, r14, false);", // IID642 - "__ erolq(r23, r23, false);", // IID643 - "__ erolq(r23, r24, true);", // IID644 - "__ erolq(r21, r21, true);", // IID645 - "__ erorq(r31, r22, false);", // IID646 - "__ erorq(r28, r28, false);", // IID647 - "__ erorq(r17, r10, true);", // IID648 - "__ erorq(r9, r9, true);", // IID649 - "__ esalq(r29, r30, false);", // IID650 - "__ esalq(r11, r11, false);", // IID651 - "__ esalq(r26, r11, true);", // IID652 - "__ esalq(r16, r16, true);", // IID653 - "__ esarq(rbx, r15, false);", // IID654 - "__ esarq(r14, r14, false);", // IID655 - "__ esarq(r25, r16, true);", // IID656 - "__ esarq(r8, r8, true);", // IID657 - "__ edecq(r11, r13, false);", // IID658 - "__ edecq(rcx, rcx, false);", // IID659 - "__ edecq(r21, r18, true);", // IID660 - "__ edecq(r28, r28, true);", // IID661 - "__ eincq(r16, r16, false);", // IID662 - "__ eincq(r29, r29, false);", // IID663 - "__ eincq(r18, r9, true);", // IID664 - "__ eincq(r19, r19, true);", // IID665 - "__ eshlq(r19, r18, false);", // IID666 - "__ eshlq(r8, r8, false);", // IID667 - "__ eshlq(r12, r15, true);", // IID668 - "__ eshlq(r29, r29, true);", // IID669 - "__ eshrq(r28, r24, false);", // IID670 - "__ eshrq(r19, r19, false);", // IID671 - "__ eshrq(r8, r28, true);", // IID672 - "__ eshrq(r17, r17, true);", // IID673 - "__ etzcntq(r28, r16, false);", // IID674 - "__ etzcntq(r14, r14, false);", // IID675 - "__ etzcntq(r12, r31, true);", // IID676 - "__ etzcntq(r14, r14, true);", // IID677 - "__ eimulq(r31, Address(r13, -0x69c4b352), false);", // IID678 - "__ eimulq(r17, Address(r18, -0x60ab1105), true);", // IID679 - "__ elzcntq(r27, Address(r14, r25, (Address::ScaleFactor)2, +0x2798bf83), false);", // IID680 - "__ elzcntq(r23, Address(r10, r11, (Address::ScaleFactor)0, -0x378e635d), true);", // IID681 - "__ enegq(rcx, Address(r19, r9, (Address::ScaleFactor)3, -0x6847d440), false);", // IID682 - "__ enegq(rcx, Address(rbx, rcx, (Address::ScaleFactor)0, +0x6f92d38d), true);", // IID683 - "__ epopcntq(r20, Address(r12, -0x2a8b27d6), false);", // IID684 - "__ epopcntq(r31, Address(r30, +0x4603f6d0), true);", // IID685 - "__ esalq(rbx, Address(r24, +0x567d06f9), false);", // IID686 - "__ esalq(r12, Address(r24, r28, (Address::ScaleFactor)0, -0x1c4c584e), true);", // IID687 - "__ esarq(r12, Address(r23, r24, (Address::ScaleFactor)2, -0x3157bcba), false);", // IID688 - "__ esarq(r8, Address(r14, r24, (Address::ScaleFactor)2, -0x714290a5), true);", // IID689 - "__ edecq(r23, Address(r8, r15, (Address::ScaleFactor)1, -0x5ae272dd), false);", // IID690 - "__ edecq(r13, Address(r29, r9, (Address::ScaleFactor)3, -0x5b5174a9), true);", // IID691 - "__ eincq(r11, Address(r21, r31, (Address::ScaleFactor)3, -0x2176b4dc), false);", // IID692 - "__ eincq(r13, Address(rcx, r16, (Address::ScaleFactor)0, -0x36b448c9), true);", // IID693 - "__ eshrq(r26, Address(r25, rcx, (Address::ScaleFactor)2, -0x5f894993), false);", // IID694 - "__ eshrq(r25, Address(r9, +0x51798d21), true);", // IID695 - "__ etzcntq(r28, Address(r13, r26, (Address::ScaleFactor)2, +0x207196f6), false);", // IID696 - "__ etzcntq(rbx, Address(r19, r13, (Address::ScaleFactor)0, -0x24d937d5), true);", // IID697 - "__ eaddq(r17, Address(r30, +0x3935ccff), r31, false);", // IID698 - "__ eaddq(r14, Address(r27, r10, (Address::ScaleFactor)2, -0x34ad9bab), r14, false);", // IID699 - "__ eaddq(r18, Address(r20, r23, (Address::ScaleFactor)0, +0x5ad3ed4b), r30, true);", // IID700 - "__ eaddq(r20, Address(rdx, -0x322a99e5), r20, true);", // IID701 - "__ eandq(r31, Address(rbx, r27, (Address::ScaleFactor)3, +0x4ce247d2), r17, false);", // IID702 - "__ eandq(r30, Address(r18, r19, (Address::ScaleFactor)1, -0x4ee3d14), r30, false);", // IID703 - "__ eandq(r28, Address(r11, rbx, (Address::ScaleFactor)3, -0x28994bbf), r24, true);", // IID704 - "__ eandq(r30, Address(r22, +0x7d21c24), r30, true);", // IID705 - "__ eorq(r26, Address(r15, r19, (Address::ScaleFactor)3, +0x58c21792), r20, false);", // IID706 - "__ eorq(r13, Address(r10, r27, (Address::ScaleFactor)2, -0x2c70d333), r13, false);", // IID707 - "__ eorq(rbx, Address(r12, rbx, (Address::ScaleFactor)0, -0x1fb0f1bc), r26, true);", // IID708 - "__ eorq(r31, Address(r27, r31, (Address::ScaleFactor)1, +0x28d1756), r31, true);", // IID709 - "__ esubq(r24, Address(r28, r23, (Address::ScaleFactor)1, +0x6980f610), r27, false);", // IID710 - "__ esubq(r15, Address(r11, r30, (Address::ScaleFactor)3, -0x49777e7), r15, false);", // IID711 - "__ esubq(r17, Address(r25, r13, (Address::ScaleFactor)2, +0x31619e46), r31, true);", // IID712 - "__ esubq(r18, Address(r11, r10, (Address::ScaleFactor)2, +0x1922861a), r18, true);", // IID713 - "__ exorq(rbx, Address(r11, -0x4716d420), r21, false);", // IID714 - "__ exorq(r8, Address(rdx, r9, (Address::ScaleFactor)2, -0x4cfe39c), r8, false);", // IID715 - "__ exorq(r16, Address(r14, r27, (Address::ScaleFactor)0, +0x7c6654d9), r25, true);", // IID716 - "__ exorq(r29, Address(r15, -0x5efab479), r29, true);", // IID717 - "__ eaddq(r19, Address(r13, r22, (Address::ScaleFactor)2, +0x68b64559), 16777216, false);", // IID718 - "__ eaddq(r16, Address(r13, r31, (Address::ScaleFactor)3, -0x65143af5), 1, true);", // IID719 - "__ eandq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), 1, false);", // IID720 - "__ eandq(r11, Address(r28, -0xf6d4b26), 65536, true);", // IID721 - "__ eimulq(rcx, Address(r18, r10, (Address::ScaleFactor)0, +0x46ec6da1), 16777216, false);", // IID722 - "__ eimulq(r15, Address(r9, r10, (Address::ScaleFactor)3, -0x7fc36af3), 16, true);", // IID723 - "__ eorq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), 1, false);", // IID724 - "__ eorq(rdx, Address(r25, r14, (Address::ScaleFactor)2, -0x59aa6b85), 4096, true);", // IID725 - "__ esalq(r17, Address(r26, r21, (Address::ScaleFactor)1, -0x6ab1f15f), 8, false);", // IID726 - "__ esalq(r12, Address(r22, r17, (Address::ScaleFactor)0, -0x43ac14ab), 2, true);", // IID727 - "__ esarq(r29, Address(r18, r16, (Address::ScaleFactor)0, -0x59dc0c61), 4, false);", // IID728 - "__ esarq(r16, Address(r11, -0x7bdd314), 4, true);", // IID729 - "__ eshrq(r26, Address(r23, r27, (Address::ScaleFactor)3, -0x55b92314), 16, false);", // IID730 - "__ eshrq(r23, Address(r16, r29, (Address::ScaleFactor)1, +0x71311a1d), 2, true);", // IID731 - "__ esubq(r25, Address(r9, -0x9532bac), 1048576, false);", // IID732 - "__ esubq(r17, Address(r8, r23, (Address::ScaleFactor)0, +0x55d06ca2), 1048576, true);", // IID733 - "__ exorq(r29, Address(r9, r24, (Address::ScaleFactor)0, -0x2c141c1), 1048576, false);", // IID734 - "__ exorq(r28, Address(r22, r19, (Address::ScaleFactor)1, -0x2d9d9abd), 16, true);", // IID735 - "__ eaddq(r22, r14, 16, false);", // IID736 - "__ eaddq(rax, r12, 16, false);", // IID737 - "__ eaddq(r24, r24, 65536, false);", // IID738 - "__ eaddq(r21, rbx, 65536, true);", // IID739 - "__ eaddq(rax, rbx, 65536, true);", // IID740 - "__ eaddq(r24, r24, 65536, true);", // IID741 - "__ eandq(r21, r27, 16777216, false);", // IID742 - "__ eandq(rax, r27, 16777216, false);", // IID743 - "__ eandq(r24, r24, 65536, false);", // IID744 - "__ eandq(r13, r31, 1048576, true);", // IID745 - "__ eandq(rax, r21, 1048576, true);", // IID746 - "__ eandq(r30, r30, 1048576, true);", // IID747 - "__ eimulq(r8, r13, 268435456, false);", // IID748 - "__ eimulq(rax, r31, 268435456, false);", // IID749 - "__ eimulq(r13, r13, 65536, false);", // IID750 - "__ eimulq(r14, r29, 1048576, true);", // IID751 - "__ eimulq(rax, r22, 1048576, true);", // IID752 - "__ eimulq(r8, r8, 268435456, true);", // IID753 - "__ eorq(r30, r15, 4096, false);", // IID754 - "__ eorq(rax, r28, 4096, false);", // IID755 - "__ eorq(r26, r26, 1048576, false);", // IID756 - "__ eorq(r16, r12, 268435456, true);", // IID757 - "__ eorq(rax, r9, 268435456, true);", // IID758 - "__ eorq(r23, r23, 256, true);", // IID759 - "__ erclq(r15, r9, 16);", // IID760 - "__ erclq(rax, r8, 16);", // IID761 - "__ erclq(r25, r25, 1);", // IID762 - "__ erolq(r9, r17, 16, false);", // IID763 - "__ erolq(rax, r20, 16, false);", // IID764 - "__ erolq(r27, r27, 1, false);", // IID765 - "__ erolq(r20, r31, 1, true);", // IID766 - "__ erolq(rax, r18, 1, true);", // IID767 - "__ erolq(r28, r28, 16, true);", // IID768 - "__ erorq(r26, r18, 16, false);", // IID769 - "__ erorq(rax, r24, 16, false);", // IID770 - "__ erorq(r22, r22, 16, false);", // IID771 - "__ erorq(r27, r29, 1, true);", // IID772 - "__ erorq(rax, r18, 1, true);", // IID773 - "__ erorq(r21, r21, 1, true);", // IID774 - "__ esalq(r12, rcx, 2, false);", // IID775 - "__ esalq(rax, r24, 2, false);", // IID776 - "__ esalq(r22, r22, 8, false);", // IID777 - "__ esalq(r17, r23, 8, true);", // IID778 - "__ esalq(rax, r27, 8, true);", // IID779 - "__ esalq(r23, r23, 1, true);", // IID780 - "__ esarq(r8, r25, 16, false);", // IID781 - "__ esarq(rax, r23, 16, false);", // IID782 - "__ esarq(r9, r9, 4, false);", // IID783 - "__ esarq(r22, r13, 1, true);", // IID784 - "__ esarq(rax, r11, 1, true);", // IID785 - "__ esarq(r12, r12, 2, true);", // IID786 - "__ eshlq(rcx, r30, 8, false);", // IID787 - "__ eshlq(rax, r19, 8, false);", // IID788 - "__ eshlq(r13, r13, 2, false);", // IID789 - "__ eshlq(r18, r11, 8, true);", // IID790 - "__ eshlq(rax, r9, 8, true);", // IID791 - "__ eshlq(rcx, rcx, 16, true);", // IID792 - "__ eshrq(r10, r22, 4, false);", // IID793 - "__ eshrq(rax, r9, 4, false);", // IID794 - "__ eshrq(r12, r12, 2, false);", // IID795 - "__ eshrq(r26, r31, 8, true);", // IID796 - "__ eshrq(rax, r12, 8, true);", // IID797 - "__ eshrq(r28, r28, 1, true);", // IID798 - "__ esubq(r15, r30, 65536, false);", // IID799 - "__ esubq(rax, rcx, 65536, false);", // IID800 - "__ esubq(r26, r26, 16, false);", // IID801 - "__ esubq(r12, r14, 1, true);", // IID802 - "__ esubq(rax, r21, 1, true);", // IID803 - "__ esubq(r20, r20, 1048576, true);", // IID804 - "__ exorq(r11, rbx, 16777216, false);", // IID805 - "__ exorq(rax, r23, 16777216, false);", // IID806 - "__ exorq(r31, r31, 268435456, false);", // IID807 - "__ exorq(r29, r28, 4096, true);", // IID808 - "__ exorq(rax, r19, 4096, true);", // IID809 - "__ exorq(rdx, rdx, 268435456, true);", // IID810 - "__ eorq_imm32(rdx, rdx, 1048576, false);", // IID811 - "__ eorq_imm32(rax, r22, 1048576, false);", // IID812 - "__ eorq_imm32(r29, r29, 1048576, false);", // IID813 - "__ eorq_imm32(r17, rcx, 4194304, false);", // IID814 - "__ eorq_imm32(rax, r25, 4194304, false);", // IID815 - "__ eorq_imm32(r27, r27, 1073741824, false);", // IID816 - "__ esubq_imm32(r16, r19, 4194304, false);", // IID817 - "__ esubq_imm32(rax, r31, 4194304, false);", // IID818 - "__ esubq_imm32(r26, r26, 262144, false);", // IID819 - "__ esubq_imm32(r17, r22, 1073741824, true);", // IID820 - "__ esubq_imm32(rax, r18, 1073741824, true);", // IID821 - "__ esubq_imm32(r23, r23, 268435456, true);", // IID822 - "__ eaddq(r13, r30, Address(r24, r19, (Address::ScaleFactor)1, +0x56ea3a3b), false);", // IID823 - "__ eaddq(r29, r15, Address(r26, r27, (Address::ScaleFactor)3, -0x4b113958), true);", // IID824 - "__ eandq(r12, r30, Address(r31, -0x46103c74), false);", // IID825 - "__ eandq(r27, r10, Address(r22, r25, (Address::ScaleFactor)1, +0x6a1ebee5), true);", // IID826 - "__ eorq(r30, r26, Address(r11, r18, (Address::ScaleFactor)2, -0x2b9fff29), false);", // IID827 - "__ eorq(r9, r12, Address(r18, r17, (Address::ScaleFactor)0, +0xb4859f6), true);", // IID828 - "__ eimulq(rdx, r17, Address(r24, rdx, (Address::ScaleFactor)2, +0x3d284cd8), false);", // IID829 - "__ eimulq(r29, r26, Address(r30, r12, (Address::ScaleFactor)1, +0x6e813124), true);", // IID830 - "__ esubq(rbx, r13, Address(r22, -0x702a289e), false);", // IID831 - "__ esubq(r23, r29, Address(r25, rdx, (Address::ScaleFactor)0, -0x6252a7ed), true);", // IID832 - "__ exorq(r8, r18, Address(r19, r14, (Address::ScaleFactor)2, -0xebfa697), false);", // IID833 - "__ exorq(r10, r28, Address(r26, +0x168381ca), true);", // IID834 - "__ eaddq(rcx, r18, r8, false);", // IID835 - "__ eaddq(rcx, rcx, r14, false);", // IID836 - "__ eaddq(r23, r10, r16, true);", // IID837 - "__ eaddq(r11, r11, r24, true);", // IID838 - "__ eadcxq(r9, r18, rdx);", // IID839 - "__ eadcxq(r8, r8, r15);", // IID840 - "__ eadoxq(r15, r22, r26);", // IID841 - "__ eadoxq(r11, r11, rdx);", // IID842 - "__ eandq(r19, rdx, r22, false);", // IID843 - "__ eandq(r29, r29, r17, false);", // IID844 - "__ eandq(r23, r27, r15, true);", // IID845 - "__ eandq(r9, r9, r13, true);", // IID846 - "__ eimulq(r18, r15, r16, false);", // IID847 - "__ eimulq(rcx, rcx, r17, false);", // IID848 - "__ eimulq(r23, r12, r20, true);", // IID849 - "__ eimulq(r10, r10, r9, true);", // IID850 - "__ eorq(rdx, r19, r14, false);", // IID851 - "__ eorq(rcx, rcx, r13, false);", // IID852 - "__ eorq(r9, r25, r29, true);", // IID853 - "__ eorq(rdx, rdx, r25, true);", // IID854 - "__ esubq(r23, r8, r16, false);", // IID855 - "__ esubq(r13, r13, r13, false);", // IID856 - "__ esubq(r19, r12, r15, true);", // IID857 - "__ esubq(r9, r9, rdx, true);", // IID858 - "__ exorq(r13, r16, r31, false);", // IID859 - "__ exorq(r17, r17, r30, false);", // IID860 - "__ exorq(r19, r30, r20, true);", // IID861 - "__ exorq(r31, r31, r13, true);", // IID862 - "__ eshldq(r22, r10, r13, 4, false);", // IID863 - "__ eshldq(r24, r24, r21, 16, false);", // IID864 - "__ eshldq(r20, r13, r27, 16, true);", // IID865 - "__ eshldq(r31, r31, r19, 2, true);", // IID866 - "__ eshrdq(r30, r20, r11, 8, false);", // IID867 - "__ eshrdq(rdx, rdx, r15, 1, false);", // IID868 - "__ eshrdq(r28, r30, r14, 2, true);", // IID869 - "__ eshrdq(r20, r20, r16, 1, true);", // IID870 - "__ ecmovq (Assembler::Condition::overflow, r21, r17, r28);", // IID871 - "__ ecmovq (Assembler::Condition::overflow, r15, r15, r30);", // IID872 - "__ ecmovq (Assembler::Condition::noOverflow, rcx, r15, r15);", // IID873 - "__ ecmovq (Assembler::Condition::noOverflow, rcx, rcx, r13);", // IID874 - "__ ecmovq (Assembler::Condition::below, rdx, r26, r26);", // IID875 - "__ ecmovq (Assembler::Condition::below, r28, r28, r15);", // IID876 - "__ ecmovq (Assembler::Condition::aboveEqual, r8, rdx, rcx);", // IID877 - "__ ecmovq (Assembler::Condition::aboveEqual, rcx, rcx, rcx);", // IID878 - "__ ecmovq (Assembler::Condition::zero, r10, r13, r9);", // IID879 - "__ ecmovq (Assembler::Condition::zero, r14, r14, r27);", // IID880 - "__ ecmovq (Assembler::Condition::notZero, r11, r23, r9);", // IID881 - "__ ecmovq (Assembler::Condition::notZero, r11, r11, rdx);", // IID882 - "__ ecmovq (Assembler::Condition::belowEqual, r31, r14, r25);", // IID883 - "__ ecmovq (Assembler::Condition::belowEqual, r20, r20, r12);", // IID884 - "__ ecmovq (Assembler::Condition::above, rdx, r10, r28);", // IID885 - "__ ecmovq (Assembler::Condition::above, r8, r8, r17);", // IID886 - "__ ecmovq (Assembler::Condition::negative, rcx, r30, r23);", // IID887 - "__ ecmovq (Assembler::Condition::negative, r26, r26, r18);", // IID888 - "__ ecmovq (Assembler::Condition::positive, rdx, rbx, r18);", // IID889 - "__ ecmovq (Assembler::Condition::positive, r21, r21, r13);", // IID890 - "__ ecmovq (Assembler::Condition::parity, r27, r28, r27);", // IID891 - "__ ecmovq (Assembler::Condition::parity, r11, r11, r30);", // IID892 - "__ ecmovq (Assembler::Condition::noParity, rcx, r21, r18);", // IID893 - "__ ecmovq (Assembler::Condition::noParity, rcx, rcx, r29);", // IID894 - "__ ecmovq (Assembler::Condition::less, rdx, r21, r12);", // IID895 - "__ ecmovq (Assembler::Condition::less, rdx, rdx, r26);", // IID896 - "__ ecmovq (Assembler::Condition::greaterEqual, r17, rbx, r22);", // IID897 - "__ ecmovq (Assembler::Condition::greaterEqual, rdx, rdx, r11);", // IID898 - "__ ecmovq (Assembler::Condition::lessEqual, rdx, r14, r8);", // IID899 - "__ ecmovq (Assembler::Condition::lessEqual, r14, r14, r8);", // IID900 - "__ ecmovq (Assembler::Condition::greater, r25, r29, r21);", // IID901 - "__ ecmovq (Assembler::Condition::greater, r26, r26, r30);", // IID902 - "__ ecmovq (Assembler::Condition::overflow, r24, r21, Address(r13, r11, (Address::ScaleFactor)1, +0x439c521e));", // IID903 - "__ ecmovq (Assembler::Condition::noOverflow, r11, r18, Address(r29, r16, (Address::ScaleFactor)0, +0x632127f));", // IID904 - "__ ecmovq (Assembler::Condition::below, r16, r8, Address(r8, r26, (Address::ScaleFactor)1, +0x10633def));", // IID905 - "__ ecmovq (Assembler::Condition::aboveEqual, r13, r14, Address(r18, -0x54f69e38));", // IID906 - "__ ecmovq (Assembler::Condition::zero, r12, r8, Address(r31, r26, (Address::ScaleFactor)1, -0x7a1e447a));", // IID907 - "__ ecmovq (Assembler::Condition::notZero, r29, r29, Address(r19, r11, (Address::ScaleFactor)2, -0x35d82dd2));", // IID908 - "__ ecmovq (Assembler::Condition::belowEqual, rcx, r18, Address(r25, r28, (Address::ScaleFactor)0, +0x30be64a0));", // IID909 - "__ ecmovq (Assembler::Condition::above, r28, r12, Address(r10, r16, (Address::ScaleFactor)1, -0x22b8fefa));", // IID910 - "__ ecmovq (Assembler::Condition::negative, r11, r8, Address(rbx, r11, (Address::ScaleFactor)3, +0x25cc9e96));", // IID911 - "__ ecmovq (Assembler::Condition::positive, r12, r27, Address(r11, -0xc2d70fe));", // IID912 - "__ ecmovq (Assembler::Condition::parity, r8, r26, Address(r19, rbx, (Address::ScaleFactor)1, -0x486db7ea));", // IID913 - "__ ecmovq (Assembler::Condition::noParity, r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884));", // IID914 - "__ ecmovq (Assembler::Condition::less, r27, r8, Address(r29, r14, (Address::ScaleFactor)2, +0x92b7a8));", // IID915 - "__ ecmovq (Assembler::Condition::greaterEqual, r14, r28, Address(r19, rdx, (Address::ScaleFactor)0, +0x9c2d45));", // IID916 - "__ ecmovq (Assembler::Condition::lessEqual, r25, r8, Address(rcx, r18, (Address::ScaleFactor)2, +0x6655c86b));", // IID917 - "__ ecmovq (Assembler::Condition::greater, r19, r21, Address(r10, r25, (Address::ScaleFactor)0, -0x1005430b));", // IID918 + "__ eorl(rcx, r24, r22, false);", // IID432 + "__ eorl(rcx, rcx, r19, false);", // IID433 + "__ eorl(r27, r27, r27, false);", // IID434 + "__ eorl(r31, r9, r13, true);", // IID435 + "__ eorl(r31, r31, r23, true);", // IID436 + "__ eorl(r19, r17, r19, true);", // IID437 + "__ eshldl(r20, r16, r24, false);", // IID438 + "__ eshldl(rdx, rdx, r12, false);", // IID439 + "__ eshldl(r29, r9, r31, true);", // IID440 + "__ eshldl(r17, r17, r20, true);", // IID441 + "__ eshrdl(r20, r15, r18, false);", // IID442 + "__ eshrdl(rcx, rcx, r12, false);", // IID443 + "__ eshrdl(r14, r9, r23, true);", // IID444 + "__ eshrdl(r19, r19, r13, true);", // IID445 + "__ esubl(r30, r27, r27, false);", // IID446 + "__ esubl(rdx, rdx, r11, false);", // IID447 + "__ esubl(r15, r11, r24, true);", // IID448 + "__ esubl(r14, r14, r25, true);", // IID449 + "__ exorl(r31, r16, r12, false);", // IID450 + "__ exorl(r20, r20, r14, false);", // IID451 + "__ exorl(r30, r13, r30, false);", // IID452 + "__ exorl(r24, r17, r17, true);", // IID453 + "__ exorl(r26, r26, r21, true);", // IID454 + "__ exorl(r11, r13, r11, true);", // IID455 + "__ eshldl(r27, r25, r21, 4, false);", // IID456 + "__ eshldl(r22, r22, r10, 4, false);", // IID457 + "__ eshldl(r21, r15, r24, 16, true);", // IID458 + "__ eshldl(rdx, rdx, r19, 1, true);", // IID459 + "__ eshrdl(r23, r13, r8, 16, false);", // IID460 + "__ eshrdl(r26, r26, r22, 1, false);", // IID461 + "__ eshrdl(r24, r9, r30, 16, true);", // IID462 + "__ eshrdl(r19, r19, r8, 4, true);", // IID463 + "__ ecmovl (Assembler::Condition::overflow, r30, r26, r17);", // IID464 + "__ ecmovl (Assembler::Condition::overflow, r14, r14, r26);", // IID465 + "__ ecmovl (Assembler::Condition::noOverflow, r24, r19, r29);", // IID466 + "__ ecmovl (Assembler::Condition::noOverflow, r25, r25, r20);", // IID467 + "__ ecmovl (Assembler::Condition::below, r11, r10, r14);", // IID468 + "__ ecmovl (Assembler::Condition::below, r30, r30, r25);", // IID469 + "__ ecmovl (Assembler::Condition::aboveEqual, r13, r22, r27);", // IID470 + "__ ecmovl (Assembler::Condition::aboveEqual, r16, r16, r24);", // IID471 + "__ ecmovl (Assembler::Condition::zero, r28, r13, r30);", // IID472 + "__ ecmovl (Assembler::Condition::zero, r30, r30, r24);", // IID473 + "__ ecmovl (Assembler::Condition::notZero, r21, r20, r31);", // IID474 + "__ ecmovl (Assembler::Condition::notZero, r8, r8, r16);", // IID475 + "__ ecmovl (Assembler::Condition::belowEqual, r15, r26, r22);", // IID476 + "__ ecmovl (Assembler::Condition::belowEqual, r31, r31, rdx);", // IID477 + "__ ecmovl (Assembler::Condition::above, r27, r8, r10);", // IID478 + "__ ecmovl (Assembler::Condition::above, r18, r18, r11);", // IID479 + "__ ecmovl (Assembler::Condition::negative, r27, rbx, r21);", // IID480 + "__ ecmovl (Assembler::Condition::negative, r12, r12, r31);", // IID481 + "__ ecmovl (Assembler::Condition::positive, r12, rdx, r18);", // IID482 + "__ ecmovl (Assembler::Condition::positive, r18, r18, r19);", // IID483 + "__ ecmovl (Assembler::Condition::parity, r16, r20, r23);", // IID484 + "__ ecmovl (Assembler::Condition::parity, r18, r18, r16);", // IID485 + "__ ecmovl (Assembler::Condition::noParity, rbx, r31, r30);", // IID486 + "__ ecmovl (Assembler::Condition::noParity, r31, r31, r29);", // IID487 + "__ ecmovl (Assembler::Condition::less, r28, r25, r10);", // IID488 + "__ ecmovl (Assembler::Condition::less, r24, r24, r20);", // IID489 + "__ ecmovl (Assembler::Condition::greaterEqual, r16, rdx, r26);", // IID490 + "__ ecmovl (Assembler::Condition::greaterEqual, r28, r28, r28);", // IID491 + "__ ecmovl (Assembler::Condition::lessEqual, r9, r20, r24);", // IID492 + "__ ecmovl (Assembler::Condition::lessEqual, r24, r24, r29);", // IID493 + "__ ecmovl (Assembler::Condition::greater, r23, r27, r15);", // IID494 + "__ ecmovl (Assembler::Condition::greater, r12, r12, r18);", // IID495 + "__ ecmovl (Assembler::Condition::overflow, r19, r9, Address(r31, rcx, (Address::ScaleFactor)1, -0x2be98bd));", // IID496 + "__ ecmovl (Assembler::Condition::overflow, r8, r8, Address(r21, r24, (Address::ScaleFactor)1, +0x41e6a0cb));", // IID497 + "__ ecmovl (Assembler::Condition::noOverflow, r23, r15, Address(r19, r30, (Address::ScaleFactor)3, -0x55adfe2d));", // IID498 + "__ ecmovl (Assembler::Condition::noOverflow, rdx, rdx, Address(r27, rdx, (Address::ScaleFactor)0, -0x1aa12735));", // IID499 + "__ ecmovl (Assembler::Condition::below, rbx, r29, Address(r31, r12, (Address::ScaleFactor)0, +0xbd42246));", // IID500 + "__ ecmovl (Assembler::Condition::below, r21, r21, Address(r19, r21, (Address::ScaleFactor)1, -0x41518818));", // IID501 + "__ ecmovl (Assembler::Condition::aboveEqual, r23, r29, Address(r22, r9, (Address::ScaleFactor)2, -0x35addbd8));", // IID502 + "__ ecmovl (Assembler::Condition::aboveEqual, r18, r18, Address(r25, +0x632184c3));", // IID503 + "__ ecmovl (Assembler::Condition::zero, r29, r13, Address(r18, r13, (Address::ScaleFactor)0, -0x3972eac6));", // IID504 + "__ ecmovl (Assembler::Condition::zero, r29, r29, Address(r12, r9, (Address::ScaleFactor)3, -0x668cdfd2));", // IID505 + "__ ecmovl (Assembler::Condition::notZero, r25, r18, Address(r9, r22, (Address::ScaleFactor)2, +0x7f6ac91f));", // IID506 + "__ ecmovl (Assembler::Condition::notZero, r28, r28, Address(r30, +0x562e6594));", // IID507 + "__ ecmovl (Assembler::Condition::belowEqual, r27, r24, Address(r15, r20, (Address::ScaleFactor)2, -0x466538b7));", // IID508 + "__ ecmovl (Assembler::Condition::belowEqual, r25, r25, Address(r26, r11, (Address::ScaleFactor)3, -0x593812a9));", // IID509 + "__ ecmovl (Assembler::Condition::above, rcx, r20, Address(r16, -0x1389a3eb));", // IID510 + "__ ecmovl (Assembler::Condition::above, rbx, rbx, Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615));", // IID511 + "__ ecmovl (Assembler::Condition::negative, rdx, r14, Address(r12, r28, (Address::ScaleFactor)1, -0x51725a91));", // IID512 + "__ ecmovl (Assembler::Condition::negative, r24, r24, Address(r17, r18, (Address::ScaleFactor)1, -0x1725c4e4));", // IID513 + "__ ecmovl (Assembler::Condition::positive, rcx, rcx, Address(r15, r23, (Address::ScaleFactor)2, -0x6bd22ccf));", // IID514 + "__ ecmovl (Assembler::Condition::positive, r24, r24, Address(r15, r10, (Address::ScaleFactor)1, -0x7ffb3d09));", // IID515 + "__ ecmovl (Assembler::Condition::parity, r23, rcx, Address(r11, r23, (Address::ScaleFactor)0, +0x3738c585));", // IID516 + "__ ecmovl (Assembler::Condition::parity, r24, r24, Address(r30, r10, (Address::ScaleFactor)0, +0xfcc15a8));", // IID517 + "__ ecmovl (Assembler::Condition::noParity, r14, r26, Address(r14, r21, (Address::ScaleFactor)1, -0x4430ce9f));", // IID518 + "__ ecmovl (Assembler::Condition::noParity, r10, r10, Address(r28, +0x3d7c59f));", // IID519 + "__ ecmovl (Assembler::Condition::less, r10, r21, Address(r8, r8, (Address::ScaleFactor)3, +0x4a6584b4));", // IID520 + "__ ecmovl (Assembler::Condition::less, r26, r26, Address(r19, r20, (Address::ScaleFactor)3, +0x47c660ef));", // IID521 + "__ ecmovl (Assembler::Condition::greaterEqual, r26, r10, Address(rcx, +0x61977a97));", // IID522 + "__ ecmovl (Assembler::Condition::greaterEqual, r30, r30, Address(r15, r19, (Address::ScaleFactor)3, +0x53c601cb));", // IID523 + "__ ecmovl (Assembler::Condition::lessEqual, r14, r9, Address(r17, -0x566ceee2));", // IID524 + "__ ecmovl (Assembler::Condition::lessEqual, r15, r15, Address(r27, r20, (Address::ScaleFactor)0, +0x76164792));", // IID525 + "__ ecmovl (Assembler::Condition::greater, r27, r14, Address(r9, r13, (Address::ScaleFactor)2, +0xf5752d7));", // IID526 + "__ ecmovl (Assembler::Condition::greater, r12, r12, Address(rbx, rcx, (Address::ScaleFactor)3, -0x5501b4c6));", // IID527 + "__ adcq(r30, r31);", // IID528 + "__ cmpq(r12, rdx);", // IID529 + "__ imulq(r21, r24);", // IID530 + "__ popcntq(r9, r25);", // IID531 + "__ sbbq(r8, r12);", // IID532 + "__ subq(r31, r24);", // IID533 + "__ tzcntq(r10, r16);", // IID534 + "__ lzcntq(r20, r21);", // IID535 + "__ addq(rdx, r17);", // IID536 + "__ andq(r14, r13);", // IID537 + "__ orq(r20, r24);", // IID538 + "__ xorq(r21, r22);", // IID539 + "__ movq(r12, r27);", // IID540 + "__ bsfq(r23, rdx);", // IID541 + "__ bsrq(r31, r28);", // IID542 + "__ btq(r8, r25);", // IID543 + "__ xchgq(r21, rbx);", // IID544 + "__ testq(r23, r23);", // IID545 + "__ addq(Address(r19, -0x180d3ea1), r10);", // IID546 + "__ andq(Address(r11, r17, (Address::ScaleFactor)1, -0x78976be8), r25);", // IID547 + "__ cmpq(Address(rbx, r28, (Address::ScaleFactor)3, +0x35f72102), r13);", // IID548 + "__ orq(Address(r8, -0x34465011), r21);", // IID549 + "__ xorq(Address(r19, -0x404b22dd), r18);", // IID550 + "__ subq(Address(r23, r27, (Address::ScaleFactor)3, -0x428d2646), r14);", // IID551 + "__ movq(Address(r9, rcx, (Address::ScaleFactor)2, -0x72611661), r28);", // IID552 + "__ xaddq(Address(r24, r21, (Address::ScaleFactor)2, +0x3a6be990), rbx);", // IID553 + "__ andq(Address(r22, r10, (Address::ScaleFactor)0, +0x7ef8bdd), 1048576);", // IID554 + "__ addq(Address(r13, r28, (Address::ScaleFactor)0, -0x754789b1), 65536);", // IID555 + "__ cmpq(Address(r10, -0xbd2a8da), 268435456);", // IID556 + "__ sarq(Address(r23, r14, (Address::ScaleFactor)1, +0x6a16d9f5), 4);", // IID557 + "__ salq(Address(rcx, r21, (Address::ScaleFactor)1, +0x5f66ac1e), 8);", // IID558 + "__ sbbq(Address(rcx, r22, (Address::ScaleFactor)3, -0x48c954c), 268435456);", // IID559 + "__ shrq(Address(r21, r30, (Address::ScaleFactor)0, +0xe405b0b), 8);", // IID560 + "__ subq(Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b), 4096);", // IID561 + "__ xorq(Address(r30, r10, (Address::ScaleFactor)1, -0x19798323), 16);", // IID562 + "__ orq(Address(rdx, r24, (Address::ScaleFactor)3, +0x18d9b316), 4096);", // IID563 + "__ movq(Address(rbx, -0x3058074d), 256);", // IID564 + "__ testq(Address(r28, r21, (Address::ScaleFactor)3, +0x65a0fdc4), -268435456);", // IID565 + "__ addq(r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c));", // IID566 + "__ andq(r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38));", // IID567 + "__ cmpq(r23, Address(r30, r19, (Address::ScaleFactor)0, -0x3e912f7f));", // IID568 + "__ lzcntq(r29, Address(rcx, +0x12e3fbe4));", // IID569 + "__ orq(r14, Address(r21, r21, (Address::ScaleFactor)2, +0xd73042));", // IID570 + "__ adcq(r31, Address(r17, r31, (Address::ScaleFactor)2, +0xabde912));", // IID571 + "__ imulq(r20, Address(r13, r27, (Address::ScaleFactor)0, -0x58dbfc1f));", // IID572 + "__ popcntq(rbx, Address(r22, -0x72c66c23));", // IID573 + "__ sbbq(r26, Address(r9, +0x334aba09));", // IID574 + "__ subq(r9, Address(r9, r30, (Address::ScaleFactor)3, -0x219a6102));", // IID575 + "__ tzcntq(r25, Address(r20, -0x2131bab1));", // IID576 + "__ xorq(r16, Address(r28, r16, (Address::ScaleFactor)1, +0x48c483b9));", // IID577 + "__ movq(r30, Address(r9, r16, (Address::ScaleFactor)0, -0x88ce84f));", // IID578 + "__ leaq(r11, Address(r30, r29, (Address::ScaleFactor)2, +0x3eeb8fd0));", // IID579 + "__ cvttsd2siq(r26, Address(r29, r10, (Address::ScaleFactor)3, +0x3ef4822e));", // IID580 + "__ xchgq(r29, Address(r19, r20, (Address::ScaleFactor)2, -0x3f0f3db9));", // IID581 + "__ testq(r8, Address(r30, r20, (Address::ScaleFactor)0, +0x15b56a17));", // IID582 + "__ addq(r26, 4096);", // IID583 + "__ andq(r20, 16);", // IID584 + "__ adcq(r23, 1048576);", // IID585 + "__ cmpq(r12, 4096);", // IID586 + "__ rclq(rcx, 4);", // IID587 + "__ rcrq(r14, 1);", // IID588 + "__ rolq(r23, 2);", // IID589 + "__ rorq(r12, 4);", // IID590 + "__ sarq(r10, 4);", // IID591 + "__ salq(r20, 4);", // IID592 + "__ sbbq(rcx, 1048576);", // IID593 + "__ shlq(r23, 16);", // IID594 + "__ shrq(r27, 2);", // IID595 + "__ subq(rcx, 65536);", // IID596 + "__ xorq(r9, 1048576);", // IID597 + "__ movq(r16, 65536);", // IID598 + "__ mov64(r24, 4503599627370496);", // IID599 + "__ btq(r18, 64);", // IID600 + "__ testq(r29, -4096);", // IID601 + "__ orq_imm32(r30, 67108864);", // IID602 + "__ subq_imm32(r25, 268435456);", // IID603 + "__ cmovq(Assembler::Condition::overflow, r30, Address(r17, r31, (Address::ScaleFactor)2, +0x47ff92f0));", // IID604 + "__ cmovq(Assembler::Condition::noOverflow, r9, Address(r24, r28, (Address::ScaleFactor)1, +0x384904c0));", // IID605 + "__ cmovq(Assembler::Condition::below, r23, Address(r23, r24, (Address::ScaleFactor)3, -0x197f1266));", // IID606 + "__ cmovq(Assembler::Condition::aboveEqual, r9, Address(r29, r30, (Address::ScaleFactor)0, +0x2b5d49c8));", // IID607 + "__ cmovq(Assembler::Condition::zero, r16, Address(rbx, r15, (Address::ScaleFactor)1, +0x22379381));", // IID608 + "__ cmovq(Assembler::Condition::notZero, r8, Address(r11, +0x49d67a0));", // IID609 + "__ cmovq(Assembler::Condition::belowEqual, r28, Address(r16, r16, (Address::ScaleFactor)2, -0x5e941da9));", // IID610 + "__ cmovq(Assembler::Condition::above, r19, Address(r18, r8, (Address::ScaleFactor)0, -0xa5e55ec));", // IID611 + "__ cmovq(Assembler::Condition::negative, r28, Address(r17, r28, (Address::ScaleFactor)1, -0x3264220c));", // IID612 + "__ cmovq(Assembler::Condition::positive, r31, Address(r14, r31, (Address::ScaleFactor)1, +0x5001bc5a));", // IID613 + "__ cmovq(Assembler::Condition::parity, rbx, Address(r18, r17, (Address::ScaleFactor)2, -0x286f2379));", // IID614 + "__ cmovq(Assembler::Condition::noParity, r17, Address(r20, -0x5549f838));", // IID615 + "__ cmovq(Assembler::Condition::less, r30, Address(r9, r28, (Address::ScaleFactor)1, -0x25b00cf3));", // IID616 + "__ cmovq(Assembler::Condition::greaterEqual, r19, Address(r9, -0x2aabf22c));", // IID617 + "__ cmovq(Assembler::Condition::lessEqual, rbx, Address(rcx, r12, (Address::ScaleFactor)1, -0x432d68cc));", // IID618 + "__ cmovq(Assembler::Condition::greater, rbx, Address(r15, r17, (Address::ScaleFactor)3, -0x2b97565e));", // IID619 + "__ call(r24);", // IID620 + "__ divq(r9);", // IID621 + "__ idivq(r28);", // IID622 + "__ imulq(rdx);", // IID623 + "__ mulq(r31);", // IID624 + "__ negq(r12);", // IID625 + "__ notq(r12);", // IID626 + "__ rolq(r24);", // IID627 + "__ rorq(r28);", // IID628 + "__ sarq(r11);", // IID629 + "__ salq(r27);", // IID630 + "__ shlq(r23);", // IID631 + "__ shrq(r17);", // IID632 + "__ incrementq(r16);", // IID633 + "__ decrementq(r12);", // IID634 + "__ pushp(r23);", // IID635 + "__ popp(r24);", // IID636 + "__ call(Address(r18, r14, (Address::ScaleFactor)0, -0x66639d32));", // IID637 + "__ mulq(Address(r24, -0x660a2421));", // IID638 + "__ negq(Address(r14, r18, (Address::ScaleFactor)0, +0x40f3936e));", // IID639 + "__ sarq(Address(r10, r13, (Address::ScaleFactor)0, +0x7d04cb72));", // IID640 + "__ salq(Address(r18, r11, (Address::ScaleFactor)3, -0x2176b4dc));", // IID641 + "__ shrq(Address(r13, rcx, (Address::ScaleFactor)1, +0x7996aa80));", // IID642 + "__ incrementq(Address(r14, +0x67c2d02a));", // IID643 + "__ decrementq(Address(r22, r26, (Address::ScaleFactor)0, +0x224f62c0));", // IID644 + "__ imulq(rdx, Address(r31, rbx, (Address::ScaleFactor)1, +0x2b00bb10), 16777216);", // IID645 + "__ imulq(r21, r31, 4096);", // IID646 + "__ shldq(rbx, r19, 1);", // IID647 + "__ shrdq(r11, r23, 4);", // IID648 + "__ pop2(r16, r30);", // IID649 + "__ pop2p(r17, rbx);", // IID650 + "__ push2(r20, r30);", // IID651 + "__ push2p(r8, r31);", // IID652 + "__ movzbq(r28, Address(r8, r14, (Address::ScaleFactor)0, +0x469ae67a));", // IID653 + "__ movzwq(r14, Address(r8, r18, (Address::ScaleFactor)2, -0x48699e02));", // IID654 + "__ movsbq(r21, Address(rbx, -0x64dae06b));", // IID655 + "__ movswq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x60318819));", // IID656 + "__ movzbq(r30, r13);", // IID657 + "__ movzwq(r30, r18);", // IID658 + "__ movsbq(r19, r15);", // IID659 + "__ movswq(r20, r16);", // IID660 + "__ cmpxchgq(r28, Address(r11, rbx, (Address::ScaleFactor)3, +0xfc3479d));", // IID661 + "__ eidivq(r20, false);", // IID662 + "__ eidivq(r30, true);", // IID663 + "__ edivq(r22, false);", // IID664 + "__ edivq(r11, true);", // IID665 + "__ eimulq(rcx, false);", // IID666 + "__ eimulq(r28, true);", // IID667 + "__ emulq(r21, false);", // IID668 + "__ emulq(r13, true);", // IID669 + "__ emulq(Address(r26, r15, (Address::ScaleFactor)2, +0x70a1ce6e), false);", // IID670 + "__ emulq(Address(r24, r19, (Address::ScaleFactor)1, -0x1670855c), true);", // IID671 + "__ eimulq(r10, r27, false);", // IID672 + "__ eimulq(r17, r17, false);", // IID673 + "__ eimulq(rdx, r22, true);", // IID674 + "__ eimulq(rbx, rbx, true);", // IID675 + "__ elzcntq(r28, r15, false);", // IID676 + "__ elzcntq(r15, r15, false);", // IID677 + "__ elzcntq(rbx, r12, true);", // IID678 + "__ elzcntq(rbx, rbx, true);", // IID679 + "__ enegq(r26, r11, false);", // IID680 + "__ enegq(r17, r17, false);", // IID681 + "__ enegq(rdx, r31, true);", // IID682 + "__ enegq(r27, r27, true);", // IID683 + "__ enotq(r31, r15);", // IID684 + "__ enotq(r21, r21);", // IID685 + "__ epopcntq(rbx, r24, false);", // IID686 + "__ epopcntq(r28, r28, false);", // IID687 + "__ epopcntq(r23, r27, true);", // IID688 + "__ epopcntq(r13, r13, true);", // IID689 + "__ erolq(r25, r28, false);", // IID690 + "__ erolq(r31, r31, false);", // IID691 + "__ erolq(r25, r23, true);", // IID692 + "__ erolq(rcx, rcx, true);", // IID693 + "__ erorq(r22, r14, false);", // IID694 + "__ erorq(r15, r15, false);", // IID695 + "__ erorq(r11, r30, true);", // IID696 + "__ erorq(r24, r24, true);", // IID697 + "__ esalq(r10, r20, false);", // IID698 + "__ esalq(r19, r19, false);", // IID699 + "__ esalq(r17, r25, true);", // IID700 + "__ esalq(r13, r13, true);", // IID701 + "__ esarq(r31, r30, false);", // IID702 + "__ esarq(r18, r18, false);", // IID703 + "__ esarq(r25, r25, true);", // IID704 + "__ esarq(r28, r28, true);", // IID705 + "__ edecq(r22, r27, false);", // IID706 + "__ edecq(r12, r12, false);", // IID707 + "__ edecq(r18, r11, true);", // IID708 + "__ edecq(r10, r10, true);", // IID709 + "__ eincq(r20, r24, false);", // IID710 + "__ eincq(r18, r18, false);", // IID711 + "__ eincq(rbx, r11, true);", // IID712 + "__ eincq(r26, r26, true);", // IID713 + "__ eshlq(r21, r8, false);", // IID714 + "__ eshlq(rbx, rbx, false);", // IID715 + "__ eshlq(r22, r21, true);", // IID716 + "__ eshlq(r27, r27, true);", // IID717 + "__ eshrq(r12, r16, false);", // IID718 + "__ eshrq(r8, r8, false);", // IID719 + "__ eshrq(rdx, r9, true);", // IID720 + "__ eshrq(r20, r20, true);", // IID721 + "__ etzcntq(r31, r21, false);", // IID722 + "__ etzcntq(r20, r20, false);", // IID723 + "__ etzcntq(rcx, r16, true);", // IID724 + "__ etzcntq(r14, r14, true);", // IID725 + "__ eimulq(r27, Address(r25, r9, (Address::ScaleFactor)1, +0x445a2393), false);", // IID726 + "__ eimulq(r23, Address(rcx, r9, (Address::ScaleFactor)1, -0x1480ef0c), true);", // IID727 + "__ elzcntq(r13, Address(r22, r17, (Address::ScaleFactor)1, -0x750c1996), false);", // IID728 + "__ elzcntq(r13, Address(r31, -0x342b6259), true);", // IID729 + "__ enegq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), false);", // IID730 + "__ enegq(r13, Address(r11, r28, (Address::ScaleFactor)3, +0x5c0013ab), true);", // IID731 + "__ epopcntq(rdx, Address(r18, rcx, (Address::ScaleFactor)2, -0x6113eaaf), false);", // IID732 + "__ epopcntq(r9, Address(r10, -0x5ca7d588), true);", // IID733 + "__ esalq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), false);", // IID734 + "__ esalq(r25, Address(r12, rdx, (Address::ScaleFactor)1, +0x62823bce), true);", // IID735 + "__ esarq(r9, Address(r10, r18, (Address::ScaleFactor)2, -0x264a7a48), false);", // IID736 + "__ esarq(rbx, Address(r14, r27, (Address::ScaleFactor)0, +0x20291e00), true);", // IID737 + "__ edecq(r12, Address(r15, r14, (Address::ScaleFactor)2, -0x20f7dabb), false);", // IID738 + "__ edecq(r9, Address(r10, r25, (Address::ScaleFactor)1, +0x21411d84), true);", // IID739 + "__ eincq(r20, Address(rbx, r25, (Address::ScaleFactor)3, +0x2f0329e), false);", // IID740 + "__ eincq(r10, Address(r12, r31, (Address::ScaleFactor)0, -0x37505c8c), true);", // IID741 + "__ eshrq(r24, Address(r23, r14, (Address::ScaleFactor)3, -0x71e75ab0), false);", // IID742 + "__ eshrq(r25, Address(r19, r10, (Address::ScaleFactor)1, +0x507b0a88), true);", // IID743 + "__ etzcntq(r31, Address(rbx, r16, (Address::ScaleFactor)0, +0x19d5192a), false);", // IID744 + "__ etzcntq(r9, Address(r22, r28, (Address::ScaleFactor)2, +0x211007cd), true);", // IID745 + "__ eaddq(r16, Address(r21, rbx, (Address::ScaleFactor)3, -0x823fa1e), r28, false);", // IID746 + "__ eaddq(r15, Address(rdx, r8, (Address::ScaleFactor)3, -0x34b9a058), r15, false);", // IID747 + "__ eaddq(r24, Address(r14, r24, (Address::ScaleFactor)3, +0x6cdc59d2), r13, true);", // IID748 + "__ eaddq(rbx, Address(r27, r14, (Address::ScaleFactor)3, +0x36c5e8de), rbx, true);", // IID749 + "__ eandq(r21, Address(r27, r27, (Address::ScaleFactor)1, -0x2c023b13), r27, false);", // IID750 + "__ eandq(r31, Address(r21, r15, (Address::ScaleFactor)2, +0x6ef2c74a), r31, false);", // IID751 + "__ eandq(r13, Address(r31, r25, (Address::ScaleFactor)1, +0x734fe9ab), r27, true);", // IID752 + "__ eandq(r15, Address(r14, r29, (Address::ScaleFactor)3, -0x6e68556), r15, true);", // IID753 + "__ eorq(r12, Address(r30, r15, (Address::ScaleFactor)3, +0x3ba33f9e), r28, false);", // IID754 + "__ eorq(r16, Address(r12, r9, (Address::ScaleFactor)0, -0x28e03b33), r16, false);", // IID755 + "__ eorq(r8, Address(r8, r25, (Address::ScaleFactor)3, -0x1e42bd95), r27, true);", // IID756 + "__ eorq(rcx, Address(r27, rbx, (Address::ScaleFactor)2, +0x7be4bcad), rcx, true);", // IID757 + "__ esubq(r24, Address(r23, r22, (Address::ScaleFactor)2, +0x6f8827d7), rdx, false);", // IID758 + "__ esubq(r21, Address(r10, -0x635b8c8), r21, false);", // IID759 + "__ esubq(r23, Address(r27, r26, (Address::ScaleFactor)3, +0x922bcc0), rbx, true);", // IID760 + "__ esubq(r25, Address(r23, r15, (Address::ScaleFactor)0, -0x38f494ac), r25, true);", // IID761 + "__ exorq(r11, Address(r12, r19, (Address::ScaleFactor)2, -0x5b71ec17), rcx, false);", // IID762 + "__ exorq(r28, Address(r19, r18, (Address::ScaleFactor)0, +0x716b9b7e), r28, false);", // IID763 + "__ exorq(r21, Address(rcx, r29, (Address::ScaleFactor)0, -0x5af0441e), r16, true);", // IID764 + "__ exorq(r12, Address(r20, r26, (Address::ScaleFactor)0, +0xe0b7fb1), r12, true);", // IID765 + "__ eaddq(r30, Address(rcx, +0x2d3b7b4f), 1048576, false);", // IID766 + "__ eaddq(r14, Address(r21, r15, (Address::ScaleFactor)2, -0x1222aee8), 4096, true);", // IID767 + "__ eandq(r23, Address(r20, r31, (Address::ScaleFactor)0, -0x96e4d6a), 16, false);", // IID768 + "__ eandq(r10, Address(rdx, rdx, (Address::ScaleFactor)3, +0x3875f17c), 1, true);", // IID769 + "__ eimulq(r17, Address(rcx, r25, (Address::ScaleFactor)2, +0x32c71076), 4096, false);", // IID770 + "__ eimulq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x7bada60d), 1048576, true);", // IID771 + "__ eorq(r25, Address(r18, r23, (Address::ScaleFactor)1, +0x48147444), 16777216, false);", // IID772 + "__ eorq(r29, Address(r26, r27, (Address::ScaleFactor)1, -0x4b113958), 1048576, true);", // IID773 + "__ esalq(r31, Address(r18, -0x46103c74), 2, false);", // IID774 + "__ esalq(r25, Address(r10, r15, (Address::ScaleFactor)0, +0x48925da4), 16, true);", // IID775 + "__ esarq(r26, Address(r18, -0x5ea1c542), 8, false);", // IID776 + "__ esarq(r12, Address(r10, r22, (Address::ScaleFactor)2, +0x5d958264), 8, true);", // IID777 + "__ eshrq(rdx, Address(r17, r20, (Address::ScaleFactor)2, +0x295add23), 16, false);", // IID778 + "__ eshrq(rbx, Address(r22, r28, (Address::ScaleFactor)1, +0x782929cb), 2, true);", // IID779 + "__ esubq(r19, Address(r23, -0x49811d72), 1, false);", // IID780 + "__ esubq(r8, Address(r19, r14, (Address::ScaleFactor)2, -0x1b2bae9a), 1048576, true);", // IID781 + "__ exorq(r19, Address(rcx, r10, (Address::ScaleFactor)0, +0x45a66ee9), 1048576, false);", // IID782 + "__ exorq(r28, Address(r9, r29, (Address::ScaleFactor)0, -0x28a19314), 16, true);", // IID783 + "__ eaddq(r8, rcx, 16777216, false);", // IID784 + "__ eaddq(rax, r14, 16777216, false);", // IID785 + "__ eaddq(r16, r16, 256, false);", // IID786 + "__ eaddq(r24, r9, 4096, true);", // IID787 + "__ eaddq(rax, r18, 4096, true);", // IID788 + "__ eaddq(r8, r8, 1, true);", // IID789 + "__ eandq(r15, r22, 1048576, false);", // IID790 + "__ eandq(rax, r26, 1048576, false);", // IID791 + "__ eandq(rdx, rdx, 4096, false);", // IID792 + "__ eandq(rdx, r22, 268435456, true);", // IID793 + "__ eandq(rax, r29, 268435456, true);", // IID794 + "__ eandq(r23, r23, 16777216, true);", // IID795 + "__ eimulq(r9, r13, 1048576, false);", // IID796 + "__ eimulq(rax, r18, 1048576, false);", // IID797 + "__ eimulq(r16, r16, 1048576, false);", // IID798 + "__ eimulq(r17, r23, 1, true);", // IID799 + "__ eimulq(rax, r12, 1, true);", // IID800 + "__ eimulq(r10, r10, 268435456, true);", // IID801 + "__ eorq(rdx, r19, 256, false);", // IID802 + "__ eorq(rax, r14, 256, false);", // IID803 + "__ eorq(r13, r13, 1, false);", // IID804 + "__ eorq(r25, r29, 256, true);", // IID805 + "__ eorq(rax, rdx, 256, true);", // IID806 + "__ eorq(r16, r16, 16, true);", // IID807 + "__ erclq(r13, r19, 4);", // IID808 + "__ erclq(rax, r12, 4);", // IID809 + "__ erclq(r9, r9, 4);", // IID810 + "__ erolq(r13, r16, 1, false);", // IID811 + "__ erolq(rax, r31, 1, false);", // IID812 + "__ erolq(r30, r30, 8, false);", // IID813 + "__ erolq(r30, r20, 8, true);", // IID814 + "__ erolq(rax, r31, 8, true);", // IID815 + "__ erolq(r31, r31, 4, true);", // IID816 + "__ erorq(r22, r10, 4, false);", // IID817 + "__ erorq(rax, r13, 4, false);", // IID818 + "__ erorq(r24, r24, 16, false);", // IID819 + "__ erorq(r29, r22, 16, true);", // IID820 + "__ erorq(rax, r20, 16, true);", // IID821 + "__ erorq(r27, r27, 4, true);", // IID822 + "__ esalq(r31, r19, 2, false);", // IID823 + "__ esalq(rax, r20, 2, false);", // IID824 + "__ esalq(r11, r11, 8, false);", // IID825 + "__ esalq(rdx, r15, 1, true);", // IID826 + "__ esalq(rax, r10, 1, true);", // IID827 + "__ esalq(r29, r29, 4, true);", // IID828 + "__ esarq(r20, r16, 1, false);", // IID829 + "__ esarq(rax, r21, 1, false);", // IID830 + "__ esarq(r28, r28, 8, false);", // IID831 + "__ esarq(r30, rcx, 4, true);", // IID832 + "__ esarq(rax, r15, 4, true);", // IID833 + "__ esarq(rcx, rcx, 4, true);", // IID834 + "__ eshlq(rdx, r26, 4, false);", // IID835 + "__ eshlq(rax, r26, 4, false);", // IID836 + "__ eshlq(r8, r8, 4, false);", // IID837 + "__ eshlq(rcx, rcx, 1, true);", // IID838 + "__ eshlq(rax, rcx, 1, true);", // IID839 + "__ eshlq(r13, r13, 2, true);", // IID840 + "__ eshrq(r14, r27, 2, false);", // IID841 + "__ eshrq(rax, r11, 2, false);", // IID842 + "__ eshrq(r9, r9, 16, false);", // IID843 + "__ eshrq(rdx, r31, 2, true);", // IID844 + "__ eshrq(rax, r14, 2, true);", // IID845 + "__ eshrq(r12, r12, 8, true);", // IID846 + "__ esubq(r10, r28, 1, false);", // IID847 + "__ esubq(rax, r8, 1, false);", // IID848 + "__ esubq(rcx, rcx, 16777216, false);", // IID849 + "__ esubq(rdx, rbx, 16777216, true);", // IID850 + "__ esubq(rax, r18, 16777216, true);", // IID851 + "__ esubq(r27, r27, 65536, true);", // IID852 + "__ exorq(r30, rcx, 4096, false);", // IID853 + "__ exorq(rax, r21, 4096, false);", // IID854 + "__ exorq(rcx, rcx, 16777216, false);", // IID855 + "__ exorq(r21, r12, 1, true);", // IID856 + "__ exorq(rax, rdx, 1, true);", // IID857 + "__ exorq(rbx, rbx, 16777216, true);", // IID858 + "__ eorq_imm32(r11, rdx, 65536, false);", // IID859 + "__ eorq_imm32(rax, r14, 65536, false);", // IID860 + "__ eorq_imm32(r14, r14, 262144, false);", // IID861 + "__ eorq_imm32(r25, r29, 262144, false);", // IID862 + "__ eorq_imm32(rax, r21, 262144, false);", // IID863 + "__ eorq_imm32(r11, r11, 16777216, false);", // IID864 + "__ esubq_imm32(r29, r19, 67108864, false);", // IID865 + "__ esubq_imm32(rax, r11, 67108864, false);", // IID866 + "__ esubq_imm32(r18, r18, 67108864, false);", // IID867 + "__ esubq_imm32(r28, r23, 4194304, true);", // IID868 + "__ esubq_imm32(rax, r21, 4194304, true);", // IID869 + "__ esubq_imm32(r16, r16, 16777216, true);", // IID870 + "__ eaddq(r8, r25, Address(r26, r8, (Address::ScaleFactor)1, +0x10633def), false);", // IID871 + "__ eaddq(r13, r13, Address(r18, r16, (Address::ScaleFactor)1, -0x74204508), false);", // IID872 + "__ eaddq(r17, r26, Address(r12, +0x23a80abf), true);", // IID873 + "__ eaddq(r9, r9, Address(r29, r19, (Address::ScaleFactor)0, -0x29e9e52), true);", // IID874 + "__ eandq(r9, r28, Address(rcx, r25, (Address::ScaleFactor)2, +0x4261ffaa), false);", // IID875 + "__ eandq(r27, r27, Address(rdx, r28, (Address::ScaleFactor)0, -0x26bdc9c1), false);", // IID876 + "__ eandq(r14, r11, Address(r16, +0x63ba0ddf), true);", // IID877 + "__ eandq(r8, r8, Address(r22, r25, (Address::ScaleFactor)1, -0x43b6ab44), true);", // IID878 + "__ eorq(r19, rcx, Address(r27, rcx, (Address::ScaleFactor)2, -0x7f687fc6), false);", // IID879 + "__ eorq(r19, r19, Address(rbx, r26, (Address::ScaleFactor)1, -0x486db7ea), false);", // IID880 + "__ eorq(r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884), true);", // IID881 + "__ eorq(r27, r27, Address(r29, +0x20337180), true);", // IID882 + "__ eimulq(rcx, r21, Address(r21, rbx, (Address::ScaleFactor)0, -0x3303888e), false);", // IID883 + "__ eimulq(rdx, rdx, Address(r28, r9, (Address::ScaleFactor)3, -0x7ad8f741), false);", // IID884 + "__ eimulq(r8, r29, Address(r17, r12, (Address::ScaleFactor)0, +0x6e85396a), true);", // IID885 + "__ eimulq(r16, r16, Address(r19, r10, (Address::ScaleFactor)3, -0x49599300), true);", // IID886 + "__ esubq(r20, r17, Address(r13, r22, (Address::ScaleFactor)0, +0x1d219a4f), false);", // IID887 + "__ esubq(r25, r25, Address(r21, r21, (Address::ScaleFactor)3, -0x6868a8c7), false);", // IID888 + "__ esubq(r20, r24, Address(rbx, r20, (Address::ScaleFactor)2, +0x32c59da6), true);", // IID889 + "__ esubq(r8, r8, Address(r12, r17, (Address::ScaleFactor)0, -0x26be2dcf), true);", // IID890 + "__ exorq(rdx, r19, Address(r9, +0x7d903b91), false);", // IID891 + "__ exorq(r28, r28, Address(r29, r27, (Address::ScaleFactor)2, +0x53091f6f), false);", // IID892 + "__ exorq(r17, r16, Address(r27, +0x7c6e9207), true);", // IID893 + "__ exorq(r15, r15, Address(r13, r24, (Address::ScaleFactor)3, -0x75c87960), true);", // IID894 + "__ eaddq(r16, rbx, r18, false);", // IID895 + "__ eaddq(r24, r24, r18, false);", // IID896 + "__ eaddq(r9, r15, r9, false);", // IID897 + "__ eaddq(r19, r26, r13, true);", // IID898 + "__ eaddq(r28, r28, r22, true);", // IID899 + "__ eaddq(r22, r11, r22, true);", // IID900 + "__ eadcxq(rcx, r12, r13);", // IID901 + "__ eadcxq(r30, r30, r12);", // IID902 + "__ eadoxq(r28, r14, r18);", // IID903 + "__ eadoxq(r30, r30, r19);", // IID904 + "__ eandq(r20, r14, r14, false);", // IID905 + "__ eandq(r17, r17, r23, false);", // IID906 + "__ eandq(r17, r14, r17, false);", // IID907 + "__ eandq(r19, r20, r15, true);", // IID908 + "__ eandq(rbx, rbx, r13, true);", // IID909 + "__ eandq(r22, r30, r22, true);", // IID910 + "__ eimulq(r17, r24, rcx, false);", // IID911 + "__ eimulq(r21, r21, r8, false);", // IID912 + "__ eimulq(r29, r21, r29, false);", // IID913 + "__ eimulq(r27, r13, r23, true);", // IID914 + "__ eimulq(r26, r26, r8, true);", // IID915 + "__ eimulq(r22, r13, r22, true);", // IID916 + "__ eorq(r11, rdx, r29, false);", // IID917 + "__ eorq(rdx, rdx, r31, false);", // IID918 + "__ eorq(r10, r29, r10, false);", // IID919 + "__ eorq(r27, r28, rcx, true);", // IID920 + "__ eorq(r25, r25, r9, true);", // IID921 + "__ eorq(rcx, r8, rcx, true);", // IID922 + "__ esubq(rcx, r10, r16, false);", // IID923 + "__ esubq(r17, r17, rcx, false);", // IID924 + "__ esubq(r13, r21, r24, true);", // IID925 + "__ esubq(r31, r31, r28, true);", // IID926 + "__ exorq(r23, r28, r23, false);", // IID927 + "__ exorq(r10, r10, r11, false);", // IID928 + "__ exorq(r19, r18, r19, false);", // IID929 + "__ exorq(r31, r9, rdx, true);", // IID930 + "__ exorq(r13, r13, r9, true);", // IID931 + "__ exorq(rcx, r10, rcx, true);", // IID932 + "__ eshldq(r12, r24, r22, 8, false);", // IID933 + "__ eshldq(r25, r25, r25, 8, false);", // IID934 + "__ eshldq(r21, r20, r15, 8, true);", // IID935 + "__ eshldq(r21, r21, r10, 8, true);", // IID936 + "__ eshrdq(r18, r18, r8, 2, false);", // IID937 + "__ eshrdq(r26, r26, r29, 8, false);", // IID938 + "__ eshrdq(r29, r26, r19, 2, true);", // IID939 + "__ eshrdq(r12, r12, rcx, 4, true);", // IID940 + "__ ecmovq (Assembler::Condition::overflow, r21, r22, r23);", // IID941 + "__ ecmovq (Assembler::Condition::overflow, r9, r9, r13);", // IID942 + "__ ecmovq (Assembler::Condition::noOverflow, rcx, r23, r24);", // IID943 + "__ ecmovq (Assembler::Condition::noOverflow, r28, r28, rdx);", // IID944 + "__ ecmovq (Assembler::Condition::below, r14, r31, r23);", // IID945 + "__ ecmovq (Assembler::Condition::below, r30, r30, r23);", // IID946 + "__ ecmovq (Assembler::Condition::aboveEqual, r10, r29, r22);", // IID947 + "__ ecmovq (Assembler::Condition::aboveEqual, rbx, rbx, r26);", // IID948 + "__ ecmovq (Assembler::Condition::zero, r23, r21, r13);", // IID949 + "__ ecmovq (Assembler::Condition::zero, r10, r10, r20);", // IID950 + "__ ecmovq (Assembler::Condition::notZero, rbx, r9, r29);", // IID951 + "__ ecmovq (Assembler::Condition::notZero, r16, r16, r30);", // IID952 + "__ ecmovq (Assembler::Condition::belowEqual, r13, rcx, r29);", // IID953 + "__ ecmovq (Assembler::Condition::belowEqual, r31, r31, r13);", // IID954 + "__ ecmovq (Assembler::Condition::above, r27, r9, r30);", // IID955 + "__ ecmovq (Assembler::Condition::above, r26, r26, r20);", // IID956 + "__ ecmovq (Assembler::Condition::negative, r8, r12, r22);", // IID957 + "__ ecmovq (Assembler::Condition::negative, r31, r31, r17);", // IID958 + "__ ecmovq (Assembler::Condition::positive, r29, rcx, r25);", // IID959 + "__ ecmovq (Assembler::Condition::positive, r22, r22, r14);", // IID960 + "__ ecmovq (Assembler::Condition::parity, rcx, r27, r9);", // IID961 + "__ ecmovq (Assembler::Condition::parity, r22, r22, r11);", // IID962 + "__ ecmovq (Assembler::Condition::noParity, r14, r19, r24);", // IID963 + "__ ecmovq (Assembler::Condition::noParity, r24, r24, r17);", // IID964 + "__ ecmovq (Assembler::Condition::less, r17, r19, r30);", // IID965 + "__ ecmovq (Assembler::Condition::less, r19, r19, r14);", // IID966 + "__ ecmovq (Assembler::Condition::greaterEqual, r25, r11, r29);", // IID967 + "__ ecmovq (Assembler::Condition::greaterEqual, r12, r12, r26);", // IID968 + "__ ecmovq (Assembler::Condition::lessEqual, r11, rbx, r10);", // IID969 + "__ ecmovq (Assembler::Condition::lessEqual, rdx, rdx, r22);", // IID970 + "__ ecmovq (Assembler::Condition::greater, r14, r15, r23);", // IID971 + "__ ecmovq (Assembler::Condition::greater, r8, r8, r24);", // IID972 + "__ ecmovq (Assembler::Condition::overflow, rbx, r31, Address(r10, r8, (Address::ScaleFactor)3, -0x313f60e0));", // IID973 + "__ ecmovq (Assembler::Condition::overflow, r23, r23, Address(rcx, r24, (Address::ScaleFactor)2, +0x17f41d9c));", // IID974 + "__ ecmovq (Assembler::Condition::noOverflow, r31, r11, Address(r16, +0x2c018942));", // IID975 + "__ ecmovq (Assembler::Condition::noOverflow, r11, r11, Address(r16, r20, (Address::ScaleFactor)3, +0x674b6a55));", // IID976 + "__ ecmovq (Assembler::Condition::below, r9, r13, Address(r9, rcx, (Address::ScaleFactor)0, +0x394a11df));", // IID977 + "__ ecmovq (Assembler::Condition::below, r30, r30, Address(rdx, r22, (Address::ScaleFactor)1, -0x6c362b88));", // IID978 + "__ ecmovq (Assembler::Condition::aboveEqual, r13, rcx, Address(r24, rcx, (Address::ScaleFactor)3, +0x46500b66));", // IID979 + "__ ecmovq (Assembler::Condition::aboveEqual, r24, r24, Address(r18, r25, (Address::ScaleFactor)1, +0x53283b7c));", // IID980 + "__ ecmovq (Assembler::Condition::zero, r23, r25, Address(r15, r9, (Address::ScaleFactor)0, -0x5f03031e));", // IID981 + "__ ecmovq (Assembler::Condition::zero, r25, r25, Address(r28, r16, (Address::ScaleFactor)1, -0x53cef514));", // IID982 + "__ ecmovq (Assembler::Condition::notZero, rbx, r25, Address(r24, r25, (Address::ScaleFactor)2, -0x66caac87));", // IID983 + "__ ecmovq (Assembler::Condition::notZero, r16, r16, Address(r27, r30, (Address::ScaleFactor)3, +0x797f455d));", // IID984 + "__ ecmovq (Assembler::Condition::belowEqual, r25, r30, Address(r18, r18, (Address::ScaleFactor)1, +0x1c9daacd));", // IID985 + "__ ecmovq (Assembler::Condition::belowEqual, r22, r22, Address(rcx, r25, (Address::ScaleFactor)1, -0x3dcbfaa9));", // IID986 + "__ ecmovq (Assembler::Condition::above, r24, r26, Address(r25, +0x747060b5));", // IID987 + "__ ecmovq (Assembler::Condition::above, r8, r8, Address(r24, r20, (Address::ScaleFactor)3, +0x47d285f6));", // IID988 + "__ ecmovq (Assembler::Condition::negative, r12, r16, Address(r13, r10, (Address::ScaleFactor)2, +0x34e5b214));", // IID989 + "__ ecmovq (Assembler::Condition::negative, rdx, rdx, Address(r15, r19, (Address::ScaleFactor)0, -0x405138b1));", // IID990 + "__ ecmovq (Assembler::Condition::positive, r18, r21, Address(rbx, r13, (Address::ScaleFactor)2, +0x51b19197));", // IID991 + "__ ecmovq (Assembler::Condition::positive, r24, r24, Address(r11, r31, (Address::ScaleFactor)3, +0x3e01520a));", // IID992 + "__ ecmovq (Assembler::Condition::parity, r29, r26, Address(r10, r25, (Address::ScaleFactor)3, -0x5f7c3872));", // IID993 + "__ ecmovq (Assembler::Condition::parity, r11, r11, Address(r22, r10, (Address::ScaleFactor)3, -0x68731453));", // IID994 + "__ ecmovq (Assembler::Condition::noParity, r20, r15, Address(r9, r25, (Address::ScaleFactor)0, +0x4a37edaa));", // IID995 + "__ ecmovq (Assembler::Condition::noParity, r31, r31, Address(r9, r20, (Address::ScaleFactor)0, +0x4f999f86));", // IID996 + "__ ecmovq (Assembler::Condition::less, r18, r23, Address(r9, r27, (Address::ScaleFactor)0, -0x3410441d));", // IID997 + "__ ecmovq (Assembler::Condition::less, r16, r16, Address(r24, r10, (Address::ScaleFactor)3, +0x52ed66ee));", // IID998 + "__ ecmovq (Assembler::Condition::greaterEqual, r11, r18, Address(rcx, +0x1de09163));", // IID999 + "__ ecmovq (Assembler::Condition::greaterEqual, r14, r14, Address(r24, r23, (Address::ScaleFactor)1, +0x5df3b4da));", // IID1000 + "__ ecmovq (Assembler::Condition::lessEqual, r15, r14, Address(r30, r20, (Address::ScaleFactor)1, +0x5c9ab976));", // IID1001 + "__ ecmovq (Assembler::Condition::lessEqual, r26, r26, Address(r18, r27, (Address::ScaleFactor)2, -0xd8c329));", // IID1002 + "__ ecmovq (Assembler::Condition::greater, r29, r9, Address(r30, r20, (Address::ScaleFactor)3, -0x37a9cf8d));", // IID1003 + "__ ecmovq (Assembler::Condition::greater, r20, r20, Address(r8, rbx, (Address::ScaleFactor)1, +0x1bdc7def));", // IID1004 #endif // _LP64 }; // END Generated code -- do not edit diff --git a/test/hotspot/gtest/x86/x86-asmtest.py b/test/hotspot/gtest/x86/x86-asmtest.py index 7081c64e604..714b7aaea40 100644 --- a/test/hotspot/gtest/x86/x86-asmtest.py +++ b/test/hotspot/gtest/x86/x86-asmtest.py @@ -92,6 +92,8 @@ registers_mapping = { 'r31': {64: 'r31', 32: 'r31d', 16: 'r31w', 8: 'r31b'}, } +commutative_instrs = ['imul', 'add', 'and', 'xor', 'or'] + class Operand(object): def generate(self): return self @@ -400,6 +402,15 @@ class RegMemRegNddInstruction(NFInstruction): self.mem = Address().generate(mem_base, mem_idx, width) self.reg2 = Register().generate(reg2, width) self.generate_operands(self.reg1, self.mem, self.reg2) + self.demote = True + + def astr(self): + if self.demote: + ops = [op.cstr() for op in self.operands] + # imul does not support RegMemReg + if self._aname in commutative_instrs[1:] and ops[0] == ops[2] and (not self.no_flag): + return f'{self._aname} ' + ', '.join([op.astr() for op in self.operands[:2]]) + return super().astr() class RegRegImmNddInstruction(NFInstruction): def __init__(self, name, aname, width, no_flag, reg1, reg2, imm): @@ -448,6 +459,9 @@ class RegRegRegNddInstruction(NFInstruction): ops = [op.cstr() for op in self.operands] if ops[0] == ops[1] and (not self.no_flag): return hdr + f'{self._aname} ' + ', '.join([op.astr() for op in self.operands[1:]]) + if self._aname in commutative_instrs and ops[0] == ops[2] and (not self.no_flag): + return hdr + f'{self._aname} ' + ', '.join([op.astr() for op in self.operands[:2]]) + return hdr + super().astr() class RegRegRegImmNddInstruction(NFInstruction): @@ -574,6 +588,18 @@ def generate(RegOp, ops, print_lp64_flag=True, full_set=False): lp64_flag = handle_lp64_flag(lp64_flag, print_lp64_flag, test_reg1, test_reg2, test_reg3) instr = RegOp(*op, reg1=test_reg1, reg2=test_reg2, reg3=test_reg3) print_instruction(instr, lp64_flag, print_lp64_flag) + + demote = True if TEST_DEMOTION else False + commute = True if op[1] in commutative_instrs else False + if RegOp in [RegRegRegNddInstruction] and demote and commute : + for i in range(len(test_regs) if full_set else 1): + test_reg1 = test_regs[i] if full_set else random.choice(test_regs) + test_reg2 = test_regs[(i + 2) % len(test_regs)] if full_set else random.choice(test_regs) + test_reg3 = test_reg1 + + lp64_flag = handle_lp64_flag(lp64_flag, print_lp64_flag, test_reg1, test_reg2, test_reg3) + instr = RegOp(*op, reg1=test_reg1, reg2=test_reg2, reg3=test_reg3) + print_instruction(instr, lp64_flag, print_lp64_flag) elif RegOp in [MemRegInstruction, RegMemInstruction, MoveRegMemInstruction, CmpxchgInstruction, CondRegMemInstruction, RegMemNddInstruction]: if full_set: @@ -699,7 +725,7 @@ def generate(RegOp, ops, print_lp64_flag=True, full_set=False): print_instruction(instr, lp64_flag, print_lp64_flag) elif RegOp in [RegMemRegNddInstruction, RegRegMemNddInstruction, CondRegRegMemInstruction]: - demote_options = [False] if TEST_DEMOTION and RegOp not in [RegMemRegNddInstruction] else [False, True] + demote_options = [False, True] for demote in demote_options: for i in range(len(test_regs) if full_set else 1): test_reg1 = test_regs[i] if full_set else random.choice(test_regs) @@ -1023,6 +1049,8 @@ instruction_set = { RegMemRegNddInstruction: [ ('eaddl', 'add', 32, False), ('eaddl', 'add', 32, True), + ('eandl', 'and', 32, False), + ('eandl', 'and', 32, True), ('eorl', 'or', 32, False), ('eorl', 'or', 32, True), ('eorb', 'or', 8, False), diff --git a/test/hotspot/jtreg/ProblemList-Xcomp.txt b/test/hotspot/jtreg/ProblemList-Xcomp.txt index b30ab329495..03f8ce4352c 100644 --- a/test/hotspot/jtreg/ProblemList-Xcomp.txt +++ b/test/hotspot/jtreg/ProblemList-Xcomp.txt @@ -43,9 +43,6 @@ vmTestbase/vm/mlvm/indy/func/jvmti/mergeCP_indy2none_b/TestDescription.java 8308 vmTestbase/vm/mlvm/indy/func/jvmti/mergeCP_none2indy_b/TestDescription.java 8308367 generic-all vmTestbase/vm/mlvm/indy/func/jvmti/redefineClassInTarget/TestDescription.java 8308367 generic-all -vmTestbase/nsk/jdi/StepRequest/addClassFilter_rt/filter_rt001/TestDescription.java 8043571 generic-all -vmTestbase/nsk/jdi/StepRequest/addClassFilter_rt/filter_rt003/TestDescription.java 8043571 generic-all - vmTestbase/nsk/jvmti/scenarios/capability/CM03/cm03t001/TestDescription.java 8299493 macosx-x64,windows-x64 vmTestbase/nsk/stress/thread/thread006.java 8321476 linux-all diff --git a/test/hotspot/jtreg/ProblemList-zgc.txt b/test/hotspot/jtreg/ProblemList-zgc.txt index 9571c717641..911e3adc9ca 100644 --- a/test/hotspot/jtreg/ProblemList-zgc.txt +++ b/test/hotspot/jtreg/ProblemList-zgc.txt @@ -40,7 +40,6 @@ serviceability/sa/ClhsdbClasses.java 8307393 generic- serviceability/sa/ClhsdbDumpclass.java 8307393 generic-all serviceability/sa/ClhsdbDumpheap.java 8307393 generic-all serviceability/sa/ClhsdbField.java 8307393 generic-all -serviceability/sa/ClhsdbFindPC.java#apa 8307393 generic-all serviceability/sa/ClhsdbFindPC.java#no-xcomp-core 8307393 generic-all serviceability/sa/ClhsdbFindPC.java#no-xcomp-process 8307393 generic-all serviceability/sa/ClhsdbFindPC.java#xcomp-core 8307393 generic-all @@ -54,9 +53,7 @@ serviceability/sa/ClhsdbJstack.java#id0 8307393 generic- serviceability/sa/ClhsdbJstack.java#id1 8307393 generic-all serviceability/sa/ClhsdbJstackWithConcurrentLock.java 8307393 generic-all serviceability/sa/ClhsdbJstackXcompStress.java 8307393 generic-all -serviceability/sa/ClhsdbLauncher.java 8307393 generic-all serviceability/sa/ClhsdbLongConstant.java 8307393 generic-all -serviceability/sa/ClhsdbPmap.java 8307393 generic-all serviceability/sa/ClhsdbPmap.java#core 8307393 generic-all serviceability/sa/ClhsdbPmap.java#process 8307393 generic-all serviceability/sa/ClhsdbPrintAll.java 8307393 generic-all @@ -64,7 +61,8 @@ serviceability/sa/ClhsdbPrintAs.java 8307393 generic- serviceability/sa/ClhsdbPrintStatics.java 8307393 generic-all serviceability/sa/ClhsdbPstack.java#core 8307393 generic-all serviceability/sa/ClhsdbPstack.java#process 8307393 generic-all -serviceability/sa/ClhsdbScanOops.java 8307393 generic-all +serviceability/sa/ClhsdbScanOops.java#parallel 8307393 generic-all +serviceability/sa/ClhsdbScanOops.java#serial 8307393 generic-all serviceability/sa/ClhsdbSource.java 8307393 generic-all serviceability/sa/ClhsdbSymbol.java 8307393 generic-all serviceability/sa/ClhsdbThread.java 8307393 generic-all @@ -73,14 +71,6 @@ serviceability/sa/ClhsdbVmStructsDump.java 8307393 generic- serviceability/sa/ClhsdbWhere.java 8307393 generic-all serviceability/sa/DeadlockDetectionTest.java 8307393 generic-all serviceability/sa/JhsdbThreadInfoTest.java 8307393 generic-all -serviceability/sa/LingeredAppSysProps.java 8307393 generic-all -serviceability/sa/LingeredAppWithDefaultMethods.java 8307393 generic-all -serviceability/sa/LingeredAppWithEnum.java 8307393 generic-all -serviceability/sa/LingeredAppWithInterface.java 8307393 generic-all -serviceability/sa/LingeredAppWithInvokeDynamic.java 8307393 generic-all -serviceability/sa/LingeredAppWithLock.java 8307393 generic-all -serviceability/sa/LingeredAppWithNativeMethod.java 8307393 generic-all -serviceability/sa/LingeredAppWithRecComputation.java 8307393 generic-all serviceability/sa/TestClassDump.java 8307393 generic-all serviceability/sa/TestClhsdbJstackLock.java 8307393 generic-all serviceability/sa/TestCpoolForInvokeDynamic.java 8307393 generic-all @@ -104,13 +94,11 @@ serviceability/sa/TestRevPtrsForInvokeDynamic.java 8307393 generic- serviceability/sa/TestSysProps.java 8307393 generic-all serviceability/sa/TestType.java 8307393 generic-all serviceability/sa/UniqueVtableTest.java 8307393 generic-all -serviceability/sa/jmap-hprof/JMapHProfLargeHeapProc.java 8307393 generic-all serviceability/sa/jmap-hprof/JMapHProfLargeHeapTest.java 8307393 generic-all serviceability/sa/sadebugd/ClhsdbAttachToDebugServer.java 8307393 generic-all serviceability/sa/sadebugd/ClhsdbTestConnectArgument.java 8307393 generic-all serviceability/sa/ClhsdbTestAllocationMerge.java 8307393 generic-all serviceability/sa/sadebugd/DebugdConnectTest.java 8307393 generic-all -serviceability/sa/sadebugd/DebugdUtils.java 8307393 generic-all serviceability/sa/sadebugd/DisableRegistryTest.java 8307393 generic-all serviceability/sa/sadebugd/PmapOnDebugdTest.java 8307393 generic-all serviceability/sa/sadebugd/RunCommandOnServerTest.java 8307393 generic-all diff --git a/test/hotspot/jtreg/ProblemList.txt b/test/hotspot/jtreg/ProblemList.txt index 37986c67dd8..d061236c957 100644 --- a/test/hotspot/jtreg/ProblemList.txt +++ b/test/hotspot/jtreg/ProblemList.txt @@ -121,7 +121,8 @@ containers/docker/TestJcmdWithSideCar.java 8341518 linux-x64 # :hotspot_serviceability -serviceability/sa/sadebugd/DebugdConnectTest.java 8239062,8270326 macosx-x64,macosx-aarch64 +# 8239062 and 8270326 only affects macosx-x64,macosx-aarch64 +serviceability/sa/sadebugd/DebugdConnectTest.java 8239062,8270326,8344261 generic-all serviceability/sa/TestRevPtrsForInvokeDynamic.java 8241235 generic-all serviceability/jvmti/vthread/GetThreadStateMountedTest/GetThreadStateMountedTest.java 8318090,8318729 generic-all @@ -143,7 +144,6 @@ serviceability/jvmti/stress/StackTrace/NotSuspended/GetStackTraceNotSuspendedStr serviceability/sa/JhsdbThreadInfoTest.java 8344261 generic-all serviceability/sa/TestJhsdbJstackLock.java 8344261 generic-all -serviceability/sa/sadebugd/DebugdConnectTest.java 8344261 generic-all serviceability/attach/RemovingUnixDomainSocketTest.java 8344261 generic-all ############################################################################# diff --git a/test/hotspot/jtreg/TEST.ROOT b/test/hotspot/jtreg/TEST.ROOT index 2d0d972744c..f5b6922e7e1 100644 --- a/test/hotspot/jtreg/TEST.ROOT +++ b/test/hotspot/jtreg/TEST.ROOT @@ -104,7 +104,7 @@ requires.properties= \ jdk.static # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Path to libraries in the topmost test directory. This is needed so @library # does not need ../../../ notation to reach them diff --git a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups index 3af6548fe33..8169ca87f4e 100644 --- a/test/hotspot/jtreg/TEST.groups +++ b/test/hotspot/jtreg/TEST.groups @@ -427,9 +427,6 @@ hotspot_appcds_dynamic = \ -runtime/cds/appcds/jigsaw/modulepath/OptimizeModuleHandlingTest.java \ -runtime/cds/appcds/loaderConstraints/DynamicLoaderConstraintsTest.java \ -runtime/cds/appcds/javaldr/ArrayTest.java \ - -runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java \ - -runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java \ - -runtime/cds/appcds/javaldr/LockDuringDump.java \ -runtime/cds/appcds/jcmd/JCmdTestStaticDump.java \ -runtime/cds/appcds/jcmd/JCmdTestDynamicDump.java \ -runtime/cds/appcds/jcmd/JCmdTestFileSafety.java \ @@ -451,7 +448,6 @@ hotspot_appcds_dynamic = \ -runtime/cds/appcds/LambdaInvokeVirtual.java \ -runtime/cds/appcds/LambdaProxyClasslist.java \ -runtime/cds/appcds/LambdaVerificationFailedDuringDump.java \ - -runtime/cds/appcds/LambdaWithJavaAgent.java \ -runtime/cds/appcds/LambdaWithUseImplMethodHandle.java \ -runtime/cds/appcds/LambdaWithOldClass.java \ -runtime/cds/appcds/LongClassListPath.java \ @@ -495,7 +491,6 @@ hotspot_cds_verify_shared_spaces = \ runtime/cds/appcds/customLoader/LoaderSegregationTest.java \ runtime/cds/appcds/javaldr/ArrayTest.java \ runtime/cds/appcds/jigsaw/modulepath/ExportModule.java \ - runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java \ runtime/cds/appcds/sharedStrings/SharedStringsBasic.java # No need to run every test with EpsilonGC. A small subset will provide enough @@ -534,21 +529,8 @@ hotspot_aot_classlinking = \ -runtime/cds/appcds/customLoader/ParallelTestSingleFP.java \ -runtime/cds/appcds/customLoader/SameNameInTwoLoadersTest.java \ -runtime/cds/appcds/DumpClassListWithLF.java \ - -runtime/cds/appcds/dynamicArchive/LambdaContainsOldInf.java \ - -runtime/cds/appcds/dynamicArchive/LambdaCustomLoader.java \ - -runtime/cds/appcds/dynamicArchive/LambdaForOldInfInBaseArchive.java \ - -runtime/cds/appcds/dynamicArchive/LambdaInBaseArchive.java \ - -runtime/cds/appcds/dynamicArchive/LambdasInTwoArchives.java \ - -runtime/cds/appcds/dynamicArchive/ModulePath.java \ - -runtime/cds/appcds/dynamicArchive/NestHostOldInf.java \ - -runtime/cds/appcds/dynamicArchive/OldClassAndInf.java \ - -runtime/cds/appcds/dynamicArchive/OldClassInBaseArchive.java \ - -runtime/cds/appcds/dynamicArchive/OldClassVerifierTrouble.java \ - -runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java \ + -runtime/cds/appcds/dynamicArchive \ -runtime/cds/appcds/HelloExtTest.java \ - -runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java \ - -runtime/cds/appcds/javaldr/GCDuringDump.java \ - -runtime/cds/appcds/javaldr/LockDuringDump.java \ -runtime/cds/appcds/jigsaw/classpathtests/EmptyClassInBootClassPath.java \ -runtime/cds/appcds/jigsaw/ExactOptionMatch.java \ -runtime/cds/appcds/jigsaw/JigsawOptionsCombo.java \ @@ -564,7 +546,6 @@ hotspot_aot_classlinking = \ -runtime/cds/appcds/JvmtiAddPath.java \ -runtime/cds/appcds/jvmti \ -runtime/cds/appcds/LambdaProxyClasslist.java \ - -runtime/cds/appcds/LambdaWithJavaAgent.java \ -runtime/cds/appcds/loaderConstraints/LoaderConstraintsTest.java \ -runtime/cds/appcds/methodHandles \ -runtime/cds/appcds/NestHostOldInf.java \ diff --git a/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java b/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java index cb52e5a24a0..9ec3beb0d89 100644 --- a/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java +++ b/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java @@ -25,7 +25,7 @@ package compiler.arguments; /* * @test TestCompileTaskTimeout - * @bug 8308094 8365909 + * @bug 8308094 8365909 8366875 * @requires vm.debug & vm.flagless & os.name == "Linux" * @summary Check functionality of CompileTaskTimeout * @library /test/lib @@ -37,6 +37,12 @@ import jdk.test.lib.process.ProcessTools; public class TestCompileTaskTimeout { public static void main(String[] args) throws Throwable { + double timeoutFactor = 1.0; + try { + timeoutFactor = Double.parseDouble(System.getProperty("test.timeout.factor", "1.0")); + } catch (NumberFormatException ignored) {} + + // Short timeouts crash the VM. ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=1", "--version") .shouldHaveExitValue(134) .shouldContain("timed out after"); @@ -49,7 +55,17 @@ public class TestCompileTaskTimeout { .shouldHaveExitValue(134) .shouldContain("timed out after"); - ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=2000", "--version") + // A long enough timeout succeeds. + int timeout = (int)(500.0 * timeoutFactor); + ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=" + timeout, "--version") + .shouldHaveExitValue(0); + + // Each repeated compilation has a new timeout. + ProcessTools.executeTestJava("-Xcomp", + "-XX:CompileTaskTimeout=" + timeout, + "-XX:RepeatCompilation=100", + "-XX:CompileCommand=compileonly,java/util/concurrent/ConcurrentHashMap.*", + "--version") .shouldHaveExitValue(0); } } diff --git a/test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java b/test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java new file mode 100644 index 00000000000..7870ce10910 --- /dev/null +++ b/test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java @@ -0,0 +1,292 @@ +/* + * 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 compiler.c2.gvn; + +import compiler.lib.generators.Generator; +import compiler.lib.generators.Generators; +import compiler.lib.generators.RestrictableGenerator; +import compiler.lib.ir_framework.DontCompile; +import compiler.lib.ir_framework.ForceInline; +import compiler.lib.ir_framework.IR; +import compiler.lib.ir_framework.IRNode; +import compiler.lib.ir_framework.Run; +import compiler.lib.ir_framework.Test; +import compiler.lib.ir_framework.TestFramework; +import jdk.test.lib.Asserts; + +/* + * @test + * @bug 8356813 + * @summary Test that Value method of ModINode is working as expected. + * @key randomness + * @library /test/lib / + * @run driver compiler.c2.gvn.ModINodeValueTests + */ +public class ModINodeValueTests { + private static final RestrictableGenerator INT_GEN = Generators.G.ints(); + private static final int POS_INT = INT_GEN.restricted(1, Integer.MAX_VALUE).next(); + private static final int NEG_INT = INT_GEN.restricted(Integer.MIN_VALUE, -1).next(); + + public static void main(String[] args) { + TestFramework.run(); + } + + @Run(test = { + "nonNegativeDividend", "nonNegativeDividendInRange", + "negativeDividend", "negativeDividendInRange", + "modByKnownBoundsUpper", "modByKnownBoundsUpperInRange", + "modByKnownBoundsLower", "modByKnownBoundsLowerInRange", + "modByKnownBoundsLimitedByDividendUpper", "modByKnownBoundsLimitedByDividendUpperInRange", + "modByKnownBoundsLimitedByDividendLower", "modByKnownBoundsLimitedByDividendLowerInRange", + "testRandomLimits" + }) + public void runMethod() { + int a = INT_GEN.next(); + int b = INT_GEN.next(); + + int min = Integer.MIN_VALUE; + int max = Integer.MAX_VALUE; + + assertResult(0, 0); + assertResult(a, b); + assertResult(min, min); + assertResult(max, max); + } + + @DontCompile + public void assertResult(int x, int y) { + Asserts.assertEQ(x != 0 && POS_INT % x < 0, nonNegativeDividend(x)); + Asserts.assertEQ(x != 0 && POS_INT % x <= 0, nonNegativeDividendInRange(x)); + Asserts.assertEQ(x != 0 && NEG_INT % x > 0, negativeDividend(x)); + Asserts.assertEQ(x != 0 && NEG_INT % x >= 0, negativeDividendInRange(x)); + Asserts.assertEQ(x % (((byte) y) + 129) > 255, modByKnownBoundsUpper(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129) >= 255, modByKnownBoundsUpperInRange(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129) < -255, modByKnownBoundsLower(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129) <= -255, modByKnownBoundsLowerInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) > 127, modByKnownBoundsLimitedByDividendUpper(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) >= 127, modByKnownBoundsLimitedByDividendUpperInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) < -128, modByKnownBoundsLimitedByDividendLower(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) <= -128, modByKnownBoundsLimitedByDividendLowerInRange(x, y)); + + int res; + try { + res = testRandomLimitsInterpreted(x, y); + } catch (ArithmeticException _) { + try { + testRandomLimits(x, y); + Asserts.fail("Expected ArithmeticException"); + return; // unreachable + } catch (ArithmeticException _) { + return; // test succeeded, no result to assert + } + } + Asserts.assertEQ(res, testRandomLimits(x, y)); + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., POS_INT % x < 0 => false. + public boolean nonNegativeDividend(int x) { + return x != 0 && POS_INT % x < 0; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., POS_INT % x < 0 => false. + // This uses <= to verify the % is not optimized away + public boolean nonNegativeDividendInRange(int x) { + return x != 0 && POS_INT % x <= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., NEG_INT % x > 0 => false. + public boolean negativeDividend(int x) { + return x != 0 && NEG_INT % x > 0; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., NEG_INT % x > 0 => false. + // This uses >= to verify the % is not optimized away + public boolean negativeDividendInRange(int x) { + return x != 0 && NEG_INT % x >= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpper(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129) > 255; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpperInRange(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129) >= 255; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLower(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129) < -255; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLowerInRange(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129) <= -255; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpper(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1) > 127; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpperInRange(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1) >= 127; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLower(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1) < -128; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLowerInRange(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1) <= -128; + } + + private static final int LIMIT_1 = INT_GEN.next(); + private static final int LIMIT_2 = INT_GEN.next(); + private static final int LIMIT_3 = INT_GEN.next(); + private static final int LIMIT_4 = INT_GEN.next(); + private static final int LIMIT_5 = INT_GEN.next(); + private static final int LIMIT_6 = INT_GEN.next(); + private static final int LIMIT_7 = INT_GEN.next(); + private static final int LIMIT_8 = INT_GEN.next(); + private static final Range RANGE_1 = Range.generate(INT_GEN); + private static final Range RANGE_2 = Range.generate(INT_GEN); + + @Test + public int testRandomLimits(int x, int y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + int z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + @DontCompile + public int testRandomLimitsInterpreted(int x, int y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + int z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + record Range(int lo, int hi) { + Range { + if (lo > hi) { + throw new IllegalArgumentException("lo > hi"); + } + } + + @ForceInline + int clamp(int v) { + return Math.min(hi, Math.max(v, lo)); + } + + static Range generate(Generator g) { + var a = g.next(); + var b = g.next(); + if (a > b) { + var tmp = a; + a = b; + b = tmp; + } + return new Range(a, b); + } + } +} diff --git a/test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java b/test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java new file mode 100644 index 00000000000..a6eef6d3326 --- /dev/null +++ b/test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java @@ -0,0 +1,292 @@ +/* + * 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 compiler.c2.gvn; + +import compiler.lib.generators.Generator; +import compiler.lib.generators.Generators; +import compiler.lib.ir_framework.DontCompile; +import compiler.lib.ir_framework.ForceInline; +import compiler.lib.ir_framework.IR; +import compiler.lib.ir_framework.IRNode; +import compiler.lib.ir_framework.Run; +import compiler.lib.ir_framework.Test; +import compiler.lib.ir_framework.TestFramework; +import jdk.test.lib.Asserts; + +/* + * @test + * @bug 8356813 + * @summary Test that Value method of ModLNode is working as expected. + * @key randomness + * @library /test/lib / + * @run driver compiler.c2.gvn.ModLNodeValueTests + */ +public class ModLNodeValueTests { + private static final Generator LONG_GEN = Generators.G.longs(); + private static final long POS_LONG = Generators.G.longs().restricted(1L, Long.MAX_VALUE).next(); + private static final long NEG_LONG = Generators.G.longs().restricted(Long.MIN_VALUE, -1L).next(); + + public static void main(String[] args) { + TestFramework.run(); + } + + @Run(test = { + "nonNegativeDividend", "nonNegativeDividendInRange", + "negativeDividend", "negativeDividendInRange", + "modByKnownBoundsUpper", "modByKnownBoundsUpperInRange", + "modByKnownBoundsLower", "modByKnownBoundsLowerInRange", + "modByKnownBoundsLimitedByDividendUpper", "modByKnownBoundsLimitedByDividendUpperInRange", + "modByKnownBoundsLimitedByDividendLower", "modByKnownBoundsLimitedByDividendLowerInRange", + "testRandomLimits" + }) + public void runMethod() { + long a = LONG_GEN.next(); + long b = LONG_GEN.next(); + + long min = Long.MIN_VALUE; + long max = Long.MAX_VALUE; + + assertResult(0, 0); + assertResult(a, b); + assertResult(min, min); + assertResult(max, max); + } + + @DontCompile + public void assertResult(long x, long y) { + Asserts.assertEQ(x != 0 && POS_LONG % x < 0, nonNegativeDividend(x)); + Asserts.assertEQ(x != 0 && POS_LONG % x <= 0, nonNegativeDividendInRange(x)); + Asserts.assertEQ(x != 0 && NEG_LONG % x > 0, negativeDividend(x)); + Asserts.assertEQ(x != 0 && NEG_LONG % x >= 0, negativeDividendInRange(x)); + Asserts.assertEQ(x % (((byte) y) + 129L) > 255, modByKnownBoundsUpper(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129L) >= 255, modByKnownBoundsUpperInRange(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129L) < -255, modByKnownBoundsLower(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129L) <= -255, modByKnownBoundsLowerInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) > 127, modByKnownBoundsLimitedByDividendUpper(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) >= 127, modByKnownBoundsLimitedByDividendUpperInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) < -128, modByKnownBoundsLimitedByDividendLower(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) <= -128, modByKnownBoundsLimitedByDividendLowerInRange(x, y)); + + int res; + try { + res = testRandomLimitsInterpreted(x, y); + } catch (ArithmeticException _) { + try { + testRandomLimits(x, y); + Asserts.fail("Expected ArithmeticException"); + return; // unreachable + } catch (ArithmeticException _) { + return; // test succeeded, no result to assert + } + } + Asserts.assertEQ(res, testRandomLimits(x, y)); + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., posVal % x < 0 => false. + public boolean nonNegativeDividend(long x) { + return x != 0 && POS_LONG % x < 0; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., posVal % x < 0 => false. + // This uses <= to verify the % is not optimized away + public boolean nonNegativeDividendInRange(long x) { + return x != 0 && POS_LONG % x <= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., negValue % x > 0 => false. + public boolean negativeDividend(long x) { + return x != 0 && NEG_LONG % x > 0; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., negValue % x > 0 => false. + // This uses >= to verify the % is not optimized away + public boolean negativeDividendInRange(long x) { + return x != 0 && NEG_LONG % x >= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpper(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129L) > 255; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpperInRange(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129L) >= 255; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLower(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129L) < -255; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLowerInRange(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129L) <= -255; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpper(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1L) > 127; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpperInRange(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1L) >= 127; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLower(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1L) < -128; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLowerInRange(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1L) <= -128; + } + + + private static final long LIMIT_1 = LONG_GEN.next(); + private static final long LIMIT_2 = LONG_GEN.next(); + private static final long LIMIT_3 = LONG_GEN.next(); + private static final long LIMIT_4 = LONG_GEN.next(); + private static final long LIMIT_5 = LONG_GEN.next(); + private static final long LIMIT_6 = LONG_GEN.next(); + private static final long LIMIT_7 = LONG_GEN.next(); + private static final long LIMIT_8 = LONG_GEN.next(); + private static final Range RANGE_1 = Range.generate(LONG_GEN); + private static final Range RANGE_2 = Range.generate(LONG_GEN); + + @Test + public int testRandomLimits(long x, long y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + long z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + @DontCompile + public int testRandomLimitsInterpreted(long x, long y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + long z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + record Range(long lo, long hi) { + Range { + if (lo > hi) { + throw new IllegalArgumentException("lo > hi"); + } + } + + @ForceInline + long clamp(long v) { + return Math.min(hi, Math.max(v, lo)); + } + + static Range generate(Generator g) { + var a = g.next(); + var b = g.next(); + if (a > b) { + var tmp = a; + a = b; + b = tmp; + } + return new Range(a, b); + } + } +} diff --git a/test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java b/test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java new file mode 100644 index 00000000000..16bdd68c93c --- /dev/null +++ b/test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java @@ -0,0 +1,63 @@ +/* + * 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 8361699 + * @summary Check that NSR state propagate correctly to initially reducible Phis + * and turns them into non-reducible Phis. + * @run main/othervm -XX:CompileCommand=compileonly,*TestReduceAllocationNotReducibleAnymore*::* + * -XX:CompileCommand=dontinline,*TestReduceAllocationNotReducibleAnymore*::* + * -Xcomp compiler.escapeAnalysis.TestReduceAllocationNotReducibleAnymore + * @run main compiler.escapeAnalysis.TestReduceAllocationNotReducibleAnymore + */ + +package compiler.escapeAnalysis; + +public class TestReduceAllocationNotReducibleAnymore { + public static void main(String[] args) { + for (int i = 0; i < 100; i++) { + test(4, null); + } + } + + static void test(int x, A a) { + Object[] objects = { new Object() }; + Object object = new Object(); + for (int i = 0; i < 150; i++) { + try { + objects[x] = object; + object = new byte[10]; + } catch (ArrayIndexOutOfBoundsException e) { + } + try { + a.foo(); + } catch (NullPointerException e) { + } + } + } + + class A { + void foo() {} + } +} diff --git a/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java b/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java index a9df0019ab1..01e015d50cb 100644 --- a/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java +++ b/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java @@ -506,10 +506,10 @@ public class TestG1BarrierGeneration { @Test @IR(failOn = IRNode.SAFEPOINT) @IR(applyIfAnd = {"UseCompressedOops", "false", "ReduceInitialCardMarks", "false"}, - counts = {IRNode.G1_STORE_P_WITH_BARRIER_FLAG, POST_ONLY, "1"}, + counts = {IRNode.G1_STORE_P_WITH_BARRIER_FLAG, POST_ONLY, ">1"}, phase = CompilePhase.FINAL_CODE) @IR(applyIfAnd = {"UseCompressedOops", "true", "ReduceInitialCardMarks", "false"}, - counts = {IRNode.G1_ENCODE_P_AND_STORE_N_WITH_BARRIER_FLAG, POST_ONLY, "1"}, + counts = {IRNode.G1_ENCODE_P_AND_STORE_N_WITH_BARRIER_FLAG, POST_ONLY, ">1"}, phase = CompilePhase.FINAL_CODE) @IR(applyIfAnd = {"UseCompressedOops", "false", "ReduceInitialCardMarks", "true"}, failOn = {IRNode.G1_STORE_P_WITH_BARRIER_FLAG, ANY}, diff --git a/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java b/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java index 6501d082075..2ea54f2b4ef 100644 --- a/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java +++ b/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java @@ -949,9 +949,6 @@ public class TestResolvedJavaType extends TypeUniverse { * Replicates the semantics of jdk.internal.reflect.Reflection#fieldFilterMap. */ private static boolean isHiddenFromReflection(ResolvedJavaField f) { - if (f.getDeclaringClass().equals(metaAccess.lookupJavaType(ConstantPool.class)) && f.getName().equals("constantPoolOop")) { - return true; - } if (f.getDeclaringClass().equals(metaAccess.lookupJavaType(Class.class))) { String name = f.getName(); return name.equals("classLoader") || diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java index 52fc9a05f98..88b34841e57 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java @@ -2295,6 +2295,11 @@ public class IRNode { vectorNode(VECTOR_MASK_CMP_D, "VectorMaskCmp", TYPE_DOUBLE); } + public static final String VECTOR_MASK_CMP = PREFIX + "VECTOR_MASK_CMP" + POSTFIX; + static { + beforeMatchingNameRegex(VECTOR_MASK_CMP, "VectorMaskCmp"); + } + public static final String VECTOR_CAST_B2S = VECTOR_PREFIX + "VECTOR_CAST_B2S" + POSTFIX; static { vectorNode(VECTOR_CAST_B2S, "VectorCastB2X", TYPE_SHORT); @@ -2705,6 +2710,11 @@ public class IRNode { vectorNode(XOR_VL, "XorV", TYPE_LONG); } + public static final String XOR_V = PREFIX + "XOR_V" + POSTFIX; + static { + beforeMatchingNameRegex(XOR_V, "XorV"); + } + public static final String XOR_V_MASK = PREFIX + "XOR_V_MASK" + POSTFIX; static { beforeMatchingNameRegex(XOR_V_MASK, "XorVMask"); @@ -2745,6 +2755,36 @@ public class IRNode { vectorNode(EXPAND_BITS_VL, "ExpandBitsV", TYPE_LONG); } + public static final String EXPAND_VB = VECTOR_PREFIX + "EXPAND_VB" + POSTFIX; + static { + vectorNode(EXPAND_VB, "ExpandV", TYPE_BYTE); + } + + public static final String EXPAND_VS = VECTOR_PREFIX + "EXPAND_VS" + POSTFIX; + static { + vectorNode(EXPAND_VS, "ExpandV", TYPE_SHORT); + } + + public static final String EXPAND_VI = VECTOR_PREFIX + "EXPAND_VI" + POSTFIX; + static { + vectorNode(EXPAND_VI, "ExpandV", TYPE_INT); + } + + public static final String EXPAND_VL = VECTOR_PREFIX + "EXPAND_VL" + POSTFIX; + static { + vectorNode(EXPAND_VL, "ExpandV", TYPE_LONG); + } + + public static final String EXPAND_VF = VECTOR_PREFIX + "EXPAND_VF" + POSTFIX; + static { + vectorNode(EXPAND_VF, "ExpandV", TYPE_FLOAT); + } + + public static final String EXPAND_VD = VECTOR_PREFIX + "EXPAND_VD" + POSTFIX; + static { + vectorNode(EXPAND_VD, "ExpandV", TYPE_DOUBLE); + } + public static final String Z_LOAD_P_WITH_BARRIER_FLAG = COMPOSITE_PREFIX + "Z_LOAD_P_WITH_BARRIER_FLAG" + POSTFIX; static { String regex = START + "zLoadP\\S*" + MID + "barrier\\(\\s*" + IS_REPLACED + "\\s*\\)" + END; diff --git a/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java index d75db965ea3..019dad55b65 100644 --- a/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -30,7 +30,19 @@ * @key randomness * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* + * compiler.loopopts.superword.TestAlignVectorFuzzer + */ + +/* + * @test id=CompileOnly + * @bug 8253191 + * @summary Fuzzing loops with different (random) init, limit, stride, scale etc. Do not force alignment. + * @modules java.base/jdk.internal.misc + * @library /test/lib + * @key randomness + * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions + * -XX:LoopUnrollLimit=250 + * -XX:CompileCommand=compileonly,compiler.loopopts.superword.TestAlignVectorFuzzer::* * compiler.loopopts.superword.TestAlignVectorFuzzer */ @@ -44,7 +56,6 @@ * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:+AlignVector -XX:+VerifyAlignVector * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* * compiler.loopopts.superword.TestAlignVectorFuzzer */ @@ -58,7 +69,6 @@ * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:+AlignVector -XX:+VerifyAlignVector * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* * -XX:ObjectAlignmentInBytes=16 * compiler.loopopts.superword.TestAlignVectorFuzzer */ @@ -73,7 +83,6 @@ * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:+AlignVector -XX:+VerifyAlignVector * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* * -XX:-TieredCompilation -Xbatch * compiler.loopopts.superword.TestAlignVectorFuzzer */ diff --git a/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java b/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java index c5e035edbd0..bd367e7ed5b 100644 --- a/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java +++ b/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java @@ -23,7 +23,7 @@ /** * @test - * @bug 8366118 + * @bug 8366118 8367613 * @summary Check that a huge method is not compiled under -XX:+DontCompileHugeMethods. * @library /test/lib * @run main compiler.runtime.TestDontCompileHugeMethods @@ -99,6 +99,10 @@ public class TestDontCompileHugeMethods { private static void runTest(Path workDir, List jvmArgs) throws Exception { ArrayList command = new ArrayList<>(); + // Disable inlining for shortMethod(). + // Otherwise under "-Xcomp -XX:TieredStopAtLevel=1", shortMethod() is inlined into main(), + // and is not directly executed or compiled. + command.add("-XX:CompileCommand=dontinline,HugeSwitch::shortMethod"); command.add("-XX:+PrintCompilation"); command.add("-Xbatch"); command.addAll(jvmArgs); diff --git a/test/hotspot/jtreg/compiler/startup/StartupOutput.java b/test/hotspot/jtreg/compiler/startup/StartupOutput.java index 14897f7ab87..bb08a2c0a2d 100644 --- a/test/hotspot/jtreg/compiler/startup/StartupOutput.java +++ b/test/hotspot/jtreg/compiler/startup/StartupOutput.java @@ -64,7 +64,7 @@ public class StartupOutput { // On s390x, generated code is ~6x larger in fastdebug and ~1.4x in release builds vs. other archs, // hence we require slightly more minimum space. int minInitialSize = 800 + (Platform.isS390x() ? 800 : 0); - for (int i = 0; i < 200; i++) { + for (int i = 0; i < 50; i++) { int initialCodeCacheSizeInKb = minInitialSize + rand.nextInt(400); int reservedCodeCacheSizeInKb = initialCodeCacheSizeInKb + rand.nextInt(200); pb = ProcessTools.createLimitedTestJavaProcessBuilder("-XX:InitialCodeCacheSize=" + initialCodeCacheSizeInKb + "K", "-XX:ReservedCodeCacheSize=" + reservedCodeCacheSizeInKb + "k", "-version"); diff --git a/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java b/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java new file mode 100644 index 00000000000..af9e7c051f8 --- /dev/null +++ b/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java @@ -0,0 +1,131 @@ +/* + * 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 compiler.vectorapi; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +/* + * @test + * @bug 8367333 + * @requires vm.compiler2.enabled + * @modules jdk.incubator.vector + * @library /test/lib + * + * @run main/othervm -Xbatch -XX:-TieredCompilation + * -XX:+UnlockDiagnosticVMOptions -XX:+StressIncrementalInlining + * -XX:CompileCommand=quiet + * -XX:CompileCommand=compileonly,compiler.vectorapi.TestVectorMathLib::test* + * compiler.vectorapi.TestVectorMathLib + */ + +public class TestVectorMathLib { + private static final VectorSpecies SPECIES = FloatVector.SPECIES_PREFERRED; + + static FloatVector testTAN(FloatVector fv) { + return fv.lanewise(VectorOperators.TAN); + } + static FloatVector testTANH(FloatVector fv) { + return fv.lanewise(VectorOperators.TANH); + } + static FloatVector testSIN(FloatVector fv) { + return fv.lanewise(VectorOperators.SIN); + } + static FloatVector testSINH(FloatVector fv) { + return fv.lanewise(VectorOperators.SINH); + } + static FloatVector testCOS(FloatVector fv) { + return fv.lanewise(VectorOperators.COS); + } + static FloatVector testCOSH(FloatVector fv) { + return fv.lanewise(VectorOperators.COSH); + } + static FloatVector testASIN(FloatVector fv) { + return fv.lanewise(VectorOperators.ASIN); + } + static FloatVector testACOS(FloatVector fv) { + return fv.lanewise(VectorOperators.ACOS); + } + static FloatVector testATAN(FloatVector fv) { + return fv.lanewise(VectorOperators.ATAN); + } + static FloatVector testATAN2(FloatVector fv) { + return fv.lanewise(VectorOperators.ATAN2, fv); + } + static FloatVector testCBRT(FloatVector fv) { + return fv.lanewise(VectorOperators.CBRT); + } + static FloatVector testLOG(FloatVector fv) { + return fv.lanewise(VectorOperators.LOG); + } + static FloatVector testLOG10(FloatVector fv) { + return fv.lanewise(VectorOperators.LOG10); + } + static FloatVector testLOG1P(FloatVector fv) { + return fv.lanewise(VectorOperators.LOG1P); + } + static FloatVector testPOW(FloatVector fv) { + return fv.lanewise(VectorOperators.POW, fv); + } + static FloatVector testEXP(FloatVector fv) { + return fv.lanewise(VectorOperators.EXP); + } + static FloatVector testEXPM1(FloatVector fv) { + return fv.lanewise(VectorOperators.EXPM1); + } + static FloatVector testHYPOT(FloatVector fv) { + return fv.lanewise(VectorOperators.HYPOT, fv); + } + + public static void main(String[] args) { + FloatVector z = FloatVector.zero(SPECIES); + for (int i = 0; i < 20_000; i++) { + z.neg(); // unary + z.add(z); // binary + + testTAN(z); + testTANH(z); + testSIN(z); + testSINH(z); + testCOS(z); + testCOSH(z); + testASIN(z); + testACOS(z); + testATAN(z); + testATAN2(z); + testCBRT(z); + testLOG(z); + testLOG10(z); + testLOG1P(z); + testPOW(z); + testEXP(z); + testEXPM1(z); + testHYPOT(z); + } + + System.out.println("TEST PASSED"); + } +} + diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java new file mode 100644 index 00000000000..ce8fc0fb7b0 --- /dev/null +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java @@ -0,0 +1,198 @@ +/* + * 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 compiler.vectorapi; + +import compiler.lib.generators.*; +import compiler.lib.ir_framework.*; +import jdk.incubator.vector.*; +import jdk.test.lib.Asserts; + +/** + * @test + * @bug 8363989 + * @key randomness + * @library /test/lib / + * @summary AArch64: Add missing backend support of VectorAPI expand operation + * @modules jdk.incubator.vector + * + * @run driver compiler.vectorapi.VectorExpandTest + */ + +public class VectorExpandTest { + static final VectorSpecies B_SPECIES = ByteVector.SPECIES_MAX; + static final VectorSpecies S_SPECIES = ShortVector.SPECIES_MAX; + static final VectorSpecies I_SPECIES = IntVector.SPECIES_MAX; + static final VectorSpecies F_SPECIES = FloatVector.SPECIES_MAX; + static final VectorSpecies L_SPECIES = LongVector.SPECIES_MAX; + static final VectorSpecies D_SPECIES = DoubleVector.SPECIES_MAX; + static final int LENGTH = 512; + static final Generators RD = Generators.G; + static byte[] ba, bb; + static short[] sa, sb; + static int[] ia, ib; + static long[] la, lb; + static float[] fa, fb; + static double[] da, db; + static boolean[] ma; + + static { + ba = new byte[LENGTH]; + bb = new byte[LENGTH]; + sa = new short[LENGTH]; + sb = new short[LENGTH]; + ia = new int[LENGTH]; + ib = new int[LENGTH]; + la = new long[LENGTH]; + lb = new long[LENGTH]; + fa = new float[LENGTH]; + fb = new float[LENGTH]; + da = new double[LENGTH]; + db = new double[LENGTH]; + ma = new boolean[LENGTH]; + + Generator iGen = RD.ints(); + Generator lGen = RD.longs(); + Generator fGen = RD.floats(); + Generator dGen = RD.doubles(); + + for (int i = 0; i < LENGTH; i++) { + ba[i] = iGen.next().byteValue(); + sa[i] = iGen.next().shortValue(); + ma[i] = iGen.next() % 2 == 0; + } + RD.fill(iGen, ia); + RD.fill(lGen, la); + RD.fill(fGen, fa); + RD.fill(dGen, da); + } + + @Test + @IR(counts = { IRNode.EXPAND_VB, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandByte(ByteVector av, VectorMask m) { + av.expand(m).intoArray(bb, 0); + } + + @Run(test = "testVectorExpandByte") + public static void testVectorExpandByte_runner() { + ByteVector av = ByteVector.fromArray(B_SPECIES, ba, 0); + VectorMask m = VectorMask.fromArray(B_SPECIES, ma, 0); + testVectorExpandByte(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? ba[index++] : (byte)0, bb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VS, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandShort(ShortVector av, VectorMask m) { + av.expand(m).intoArray(sb, 0); + } + + @Run(test = "testVectorExpandShort") + public static void testVectorExpandShort_runner() { + ShortVector av = ShortVector.fromArray(S_SPECIES, sa, 0); + VectorMask m = VectorMask.fromArray(S_SPECIES, ma, 0); + testVectorExpandShort(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? sa[index++] : (short)0, sb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VI, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandInt(IntVector av, VectorMask m) { + av.expand(m).intoArray(ib, 0); + } + + @Run(test = "testVectorExpandInt") + public static void testVectorExpandInt_runner() { + IntVector av = IntVector.fromArray(I_SPECIES, ia, 0); + VectorMask m = VectorMask.fromArray(I_SPECIES, ma, 0); + testVectorExpandInt(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? ia[index++] : (int)0, ib[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VL, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandLong(LongVector av, VectorMask m) { + av.expand(m).intoArray(lb, 0); + } + + @Run(test = "testVectorExpandLong") + public static void testVectorExpandLong_runner() { + LongVector av = LongVector.fromArray(L_SPECIES, la, 0); + VectorMask m = VectorMask.fromArray(L_SPECIES, ma, 0); + testVectorExpandLong(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? la[index++] : (long)0, lb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VF, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandFloat(FloatVector av, VectorMask m) { + av.expand(m).intoArray(fb, 0); + } + + @Run(test = "testVectorExpandFloat") + public static void testVectorExpandFloat_runner() { + FloatVector av = FloatVector.fromArray(F_SPECIES, fa, 0); + VectorMask m = VectorMask.fromArray(F_SPECIES, ma, 0); + testVectorExpandFloat(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? fa[index++] : (float)0, fb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VD, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandDouble(DoubleVector av, VectorMask m) { + av.expand(m).intoArray(db, 0); + } + + @Run(test = "testVectorExpandDouble") + public static void testVectorExpandDouble_runner() { + DoubleVector av = DoubleVector.fromArray(D_SPECIES, da, 0); + VectorMask m = VectorMask.fromArray(D_SPECIES, ma, 0); + testVectorExpandDouble(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? da[index++] : (double)0, db[i]); + } + } + + public static void main(String[] args) { + TestFramework testFramework = new TestFramework(); + testFramework.setDefaultWarmup(10000) + .addFlags("--add-modules=jdk.incubator.vector") + .start(); + } +} diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java new file mode 100644 index 00000000000..851113ea4de --- /dev/null +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java @@ -0,0 +1,1299 @@ +/* + * 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 compiler.vectorapi; + +import compiler.lib.generators.*; +import compiler.lib.ir_framework.*; +import jdk.incubator.vector.*; +import jdk.test.lib.Asserts; + +/* + * @test + * @bug 8354242 + * @key randomness + * @library /test/lib / + * @summary test combining vector not operation with compare + * @modules jdk.incubator.vector + * + * @run driver compiler.vectorapi.VectorMaskCompareNotTest + */ + +public class VectorMaskCompareNotTest { + private static int LENGTH = 128; + + private static final VectorSpecies B_SPECIES = VectorSpecies.ofLargestShape(byte.class); + private static final VectorSpecies S_SPECIES = VectorSpecies.ofLargestShape(short.class); + private static final VectorSpecies I_SPECIES = VectorSpecies.ofLargestShape(int.class); + private static final VectorSpecies L_SPECIES = VectorSpecies.ofLargestShape(long.class); + private static final VectorSpecies F_SPECIES = VectorSpecies.ofLargestShape(float.class); + private static final VectorSpecies D_SPECIES = VectorSpecies.ofLargestShape(double.class); + + // Vector species for vector mask cast operation between int and long types, + // they must have the same number of elements. + // For other types, use a vector species of the specified width. + private static final VectorSpecies L_SPECIES_FOR_CAST = VectorSpecies.ofLargestShape(long.class); + private static final VectorSpecies I_SPECIES_FOR_CAST = VectorSpecies.of(int.class, VectorShape.forBitSize(L_SPECIES_FOR_CAST.vectorBitSize() / 2)); + + private static final Generators RD = Generators.G; + + private static byte[] ba; + private static byte[] bb; + private static short[] sa; + private static short[] sb; + private static int[] ia; + private static int[] ib; + private static int[] ic; + private static long[] la; + private static long[] lb; + private static float[] fa; + private static float[] fb; + private static float[] fnan; + private static float[] fpinf; + private static float[] fninf; + private static double[] da; + private static double[] db; + private static double[] dnan; + private static double[] dpinf; + private static double[] dninf; + private static boolean[] mr; + + static { + ba = new byte[LENGTH]; + bb = new byte[LENGTH]; + sa = new short[LENGTH]; + sb = new short[LENGTH]; + ia = new int[LENGTH]; + ib = new int[LENGTH]; + ic = new int[LENGTH]; + la = new long[LENGTH]; + lb = new long[LENGTH]; + fa = new float[LENGTH]; + fb = new float[LENGTH]; + fnan = new float[LENGTH]; + fpinf = new float[LENGTH]; + fninf = new float[LENGTH]; + da = new double[LENGTH]; + db = new double[LENGTH]; + dnan = new double[LENGTH]; + dpinf = new double[LENGTH]; + dninf = new double[LENGTH]; + mr = new boolean[LENGTH]; + + Generator iGen = RD.ints(); + Generator lGen = RD.longs(); + // Use uniform generators for floating point numbers not to generate NaN values. + Generator fGen = RD.uniformFloats(Float.MIN_VALUE, Float.MAX_VALUE); + Generator dGen = RD.uniformDoubles(Double.MIN_VALUE, Double.MAX_VALUE); + for (int i = 0; i < LENGTH; i++) { + ba[i] = iGen.next().byteValue(); + bb[i] = iGen.next().byteValue(); + sa[i] = iGen.next().shortValue(); + sb[i] = iGen.next().shortValue(); + ia[i] = iGen.next(); + ib[i] = iGen.next(); + la[i] = lGen.next(); + lb[i] = lGen.next(); + fa[i] = fGen.next(); + fb[i] = fGen.next(); + fnan[i] = Float.NaN; + fpinf[i] = Float.POSITIVE_INFINITY; + fninf[i] = Float.NEGATIVE_INFINITY; + da[i] = dGen.next(); + db[i] = dGen.next(); + dnan[i] = Double.NaN; + dpinf[i] = Double.POSITIVE_INFINITY; + dninf[i] = Double.NEGATIVE_INFINITY; + } + } + + public static int compareUnsigned(Number a, Number b) { + if (a instanceof Byte) { + return Integer.compareUnsigned(Byte.toUnsignedInt(a.byteValue()), Byte.toUnsignedInt(b.byteValue())); + } else if (a instanceof Short) { + return Integer.compareUnsigned(Short.toUnsignedInt(a.shortValue()), Short.toUnsignedInt(b.shortValue())); + } else if (a instanceof Integer) { + return Integer.compareUnsigned(a.intValue(), b.intValue()); + } else if (a instanceof Long) { + return Long.compareUnsigned(a.longValue(), b.longValue()); + } else { + throw new IllegalArgumentException("Unsupported type for unsigned comparison: " + a.getClass() + ", " + b.getClass()); + } + } + + public static > void compareResults(T a, T b, boolean r, VectorOperators.Comparison op) { + if (op == VectorOperators.EQ) { + // For floating point numbers, a is not NaN, b may be NaN. If b is NaN, + // a.compareTo(b) will return 1, 1 != 0 is true, r is expected to be true. + Asserts.assertEquals(a.compareTo(b) != 0, r); + } else if (op == VectorOperators.NE) { + // For floating point numbers, a is not NaN, b may be NaN. If b is NaN, + // a.compareTo(b) will return 1, 1 == 0 is false, r is expected to be false. + Asserts.assertEquals(a.compareTo(b) == 0, r); + } else if (op == VectorOperators.LE) { + Asserts.assertEquals(a.compareTo(b) > 0, r); + } else if (op == VectorOperators.GE) { + Asserts.assertEquals(a.compareTo(b) < 0, r); + } else if (op == VectorOperators.LT) { + Asserts.assertEquals(a.compareTo(b) >= 0, r); + } else if (op == VectorOperators.GT) { + Asserts.assertEquals(a.compareTo(b) <= 0, r); + } else if (op == VectorOperators.ULE) { + Asserts.assertEquals(compareUnsigned(a, b) > 0, r); + } else if (op == VectorOperators.UGE) { + Asserts.assertEquals(compareUnsigned(a, b) < 0, r); + } else if (op == VectorOperators.ULT) { + Asserts.assertEquals(compareUnsigned(a, b) >= 0, r); + } else if (op == VectorOperators.UGT) { + Asserts.assertEquals(compareUnsigned(a, b) <= 0, r); + } else { + throw new IllegalArgumentException("Unknown comparison operator: " + op); + } + } + + @DontInline + public static void verifyResultsByte(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(ba[i], bb[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsShort(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(sa[i], sb[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsInt(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(ia[i], ib[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsLong(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(la[i], lb[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsFloat(VectorSpecies vs, VectorOperators.Comparison op, float[] a, float[] b) { + for (int i = 0; i < vs.length(); i++) { + compareResults(a[i], b[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsDouble(VectorSpecies vs, VectorOperators.Comparison op, double[] a, double[] b) { + for (int i = 0; i < vs.length(); i++) { + compareResults(a[i], b[i], mr[i], op); + } + } + + interface VectorMaskOperator { + public VectorMask apply(VectorMask m); + } + + @ForceInline + public static void testCompareMaskNotByte(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + ByteVector av = ByteVector.fromArray(vs, ba, 0); + ByteVector bv = ByteVector.fromArray(vs, bb, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotShort(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + ShortVector av = ShortVector.fromArray(vs, sa, 0); + ShortVector bv = ShortVector.fromArray(vs, sb, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotInt(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + IntVector av = IntVector.fromArray(vs, ia, 0); + IntVector bv = IntVector.fromArray(vs, ib, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotLong(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + LongVector av = LongVector.fromArray(vs, la, 0); + LongVector bv = LongVector.fromArray(vs, lb, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotFloat(VectorSpecies vs, VectorOperators.Comparison op, float[] a, float[] b, VectorMaskOperator func) { + FloatVector av = FloatVector.fromArray(vs, a, 0); + FloatVector bv = FloatVector.fromArray(vs, b, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotDouble(VectorSpecies vs, VectorOperators.Comparison op, double[] a, double[] b, VectorMaskOperator func) { + DoubleVector av = DoubleVector.fromArray(vs, a, 0); + DoubleVector bv = DoubleVector.fromArray(vs, b, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + // Byte tests + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.EQ); + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.EQ); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.EQ, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.NE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.NE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.NE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.NE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.LT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.LT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.LT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.LT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.GT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.GT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.GT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.GT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.LE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.LE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.LE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.LE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.GE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.GE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.GE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.GE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.ULT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.UGT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.ULE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.UGE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.UGE); + } + + // Short tests + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.EQ); + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.EQ); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.EQ, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.EQ); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.EQ, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.NE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.NE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.NE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.NE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.NE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.NE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.LT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.LT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.LT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.LT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.LT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.LT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.GT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.GT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.GT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.GT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.GT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.GT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.LE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.LE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.LE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.LE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.LE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.LE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.GE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.GE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.GE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.GE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.GE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.GE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.ULT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.ULT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.ULT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.UGT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.UGT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.UGT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.ULE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.ULE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.ULE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.UGE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.UGE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.UGE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.UGE); + } + + // Int tests + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.EQ); + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.EQ); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.EQ, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.EQ); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.EQ, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.NE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.NE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.NE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.NE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.NE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.NE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.LT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.LT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.LT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.LT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.LT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.LT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.GT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.GT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.GT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.GT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.GT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.GT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.LE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.LE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.LE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.LE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.LE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.LE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.GE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.GE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.GE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.GE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.GE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.GE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.ULT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.ULT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.ULT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.UGT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.UGT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.UGT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.ULE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.ULE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.ULE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.UGE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.UGE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.UGE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.UGE); + } + + // Long tests + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.EQ); + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.EQ); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.EQ, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.NE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.NE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.NE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.NE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.LT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.LT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.LT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.LT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.GT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.GT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.GT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.GT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.LE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.LE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.LE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.LE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.GE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.GE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.GE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.GE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.ULT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.UGT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.ULE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.UGE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.UGE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloat() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fb); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fb); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloat() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fb, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fb); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fb, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fb); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloatNaN() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fnan, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fnan); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fnan, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloatNaN() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fnan, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fnan); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fnan, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloatPositiveInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloatPositiveInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fpinf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fpinf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fpinf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloatNegativeInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fninf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fninf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fninf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fninf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloatNegativeInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fninf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fninf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fninf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fninf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDouble() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, db); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, db); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDouble() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, db, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, db); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, db, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, db); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDoubleNaN() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dnan, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dnan); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dnan, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDoubleNaN() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dnan, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dnan); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dnan, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDoublePositiveInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dpinf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dpinf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dpinf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDoublePositiveInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dpinf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dpinf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dpinf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDoubleNegativeInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dninf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dninf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dninf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dninf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDoubleNegativeInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dninf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dninf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dninf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dninf); + } + + // negative tests + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotByteNegative() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsByte(B_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { return B_SPECIES.maskAll(false).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotShortNegative() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsShort(S_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { return S_SPECIES.maskAll(false).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotIntNegative() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsInt(I_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { return I_SPECIES.maskAll(false).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotLongNegative() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsLong(L_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { return L_SPECIES.maskAll(false).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotFloatNegative() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fb); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { return F_SPECIES.maskAll(false).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fb); + + // Float vectors use the LT comparison. + testCompareMaskNotFloat(F_SPECIES, VectorOperators.LT, fa, fb, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.LT, fa, fb); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotDoubleNegative() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, db); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { return D_SPECIES.maskAll(false).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, db); + + // Double vectors use the LT comparison. + testCompareMaskNotDouble(D_SPECIES, VectorOperators.LT, da, db, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.LT, da, db); + } + + public static void main(String[] args) { + TestFramework testFramework = new TestFramework(); + testFramework.setDefaultWarmup(5000) + .addFlags("--add-modules=jdk.incubator.vector") + .start(); + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java b/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java index 17ae437358d..d28c0888579 100644 --- a/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java +++ b/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java @@ -108,8 +108,7 @@ public class TestGCLogMessages { new LogMessageWithLevel("Other:", Level.INFO), // Pre Evacuate Collection Set - new LogMessageWithLevel("JT Retire TLABs And Flush Logs \\(ms\\):", Level.DEBUG), - new LogMessageWithLevel("Non-JT Flush Logs \\(ms\\):", Level.DEBUG), + new LogMessageWithLevel("JavaThread Retire TLABs \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Choose Collection Set:", Level.DEBUG), new LogMessageWithLevel("Region Register:", Level.DEBUG), new LogMessageWithLevel("Prepare Heap Roots:", Level.DEBUG), @@ -126,10 +125,11 @@ public class TestGCLogMessages { new LogMessageWithLevel("Merged Howl ArrayOfCards:", Level.DEBUG), new LogMessageWithLevel("Merged Howl BitMap:", Level.DEBUG), new LogMessageWithLevel("Merged Howl Full:", Level.DEBUG), - new LogMessageWithLevel("Log Buffers \\(ms\\):", Level.DEBUG), - new LogMessageWithLevel("Dirty Cards:", Level.DEBUG), - new LogMessageWithLevel("Merged Cards:", Level.DEBUG), - new LogMessageWithLevel("Skipped Cards:", Level.DEBUG), + new LogMessageWithLevel("Merged From RS Cards:", Level.DEBUG), + new LogMessageWithLevel("Total Cards:", Level.DEBUG), + new LogMessageWithLevel("Merge Refinement Table:", Level.DEBUG), + new LogMessageWithLevel("Sweep \\(ms\\):", Level.DEBUG), + // Evacuate Collection Set new LogMessageWithLevel("Ext Root Scanning \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Thread Roots \\(ms\\):", Level.TRACE), @@ -173,15 +173,16 @@ public class TestGCLogMessages { new LogMessageWithLevel("Merge Per-Thread State \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("LAB Waste:", Level.DEBUG), new LogMessageWithLevel("LAB Undo Waste:", Level.DEBUG), - new LogMessageWithLevel("Evac Fail Extra Cards:", Level.DEBUG), - new LogMessageWithLevel("Clear Logged Cards \\(ms\\):", Level.DEBUG), + new LogMessageWithLevel("Pending Cards:", Level.DEBUG), + new LogMessageWithLevel("To-Young-Gen Cards:", Level.DEBUG), + new LogMessageWithLevel("Evac-Fail Cards:", Level.DEBUG), + new LogMessageWithLevel("Marked Cards:", Level.DEBUG), + new LogMessageWithLevel("Clear Pending Cards \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Recalculate Used Memory \\(ms\\):", Level.DEBUG), // Post Evacuate Cleanup 2 new LogMessageWithLevel("Post Evacuate Cleanup 2:", Level.DEBUG), new LogMessageWithLevelC2OrJVMCIOnly("Update Derived Pointers", Level.DEBUG), - new LogMessageWithLevel("Redirty Logged Cards \\(ms\\):", Level.DEBUG), - new LogMessageWithLevel("Redirtied Cards:", Level.DEBUG), new LogMessageWithLevel("Resize TLABs \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Free Collection Set \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Serial Free Collection Set:", Level.TRACE), @@ -243,9 +244,7 @@ public class TestGCLogMessages { } LogMessageWithLevel concRefineMessages[] = new LogMessageWithLevel[] { - new LogMessageWithLevel("Mutator refinement: ", Level.DEBUG), - new LogMessageWithLevel("Concurrent refinement: ", Level.DEBUG), - new LogMessageWithLevel("Total refinement: ", Level.DEBUG), + new LogMessageWithLevel("Refinement: sweep: ", Level.DEBUG), // "Concurrent refinement rate" optionally printed if any. // "Generate dirty cards rate" optionally printed if any. }; diff --git a/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java b/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java index 033b74f7eb1..d4b47422c38 100644 --- a/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java +++ b/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java @@ -235,7 +235,6 @@ public class TestOptionsWithRanges { */ excludeTestMaxRange("ConcGCThreads"); excludeTestMaxRange("G1ConcRefinementThreads"); - excludeTestMaxRange("G1UpdateBufferSize"); excludeTestMaxRange("InitialHeapSize"); excludeTestMaxRange("MaxHeapSize"); excludeTestMaxRange("MaxRAM"); diff --git a/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java b/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java index 09db906b151..6fd1da6f2ae 100644 --- a/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java +++ b/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java @@ -28,6 +28,7 @@ * VMThreadStackSize values should result in an error message that shows * the minimum stack size value for each thread type. * @library /test/lib + * @requires vm.flagless * @modules java.base/jdk.internal.misc * java.management * @run driver TooSmallStackSize diff --git a/test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java deleted file mode 100644 index accb74025ce..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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. - * - * 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 8276126 - * @summary Test static dumping with java agent transforming a class loaded - * by the boot class loader. - * @requires vm.cds.write.archived.java.heap - * @requires vm.jvmti - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @compile test-classes/Hello.java - * @compile test-classes/TransformBootClass.java - * @run driver LambdaWithJavaAgent - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.cds.CDSTestUtils; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class LambdaWithJavaAgent { - - public static String agentClasses[] = { - TransformBootClass.class.getName(), - }; - - public static void main(String[] args) throws Exception { - String mainClass = Hello.class.getName(); - String namePrefix = "lambda-with-java-agent"; - JarBuilder.build(namePrefix, mainClass); - - String appJar = TestCommon.getTestJar(namePrefix + ".jar"); - String classList = namePrefix + ".list"; - String archiveName = namePrefix + ".jsa"; - - String agentJar = - ClassFileInstaller.writeJar("TransformBootClass.jar", - ClassFileInstaller.Manifest.fromSourceFile("test-classes/TransformBootClass.mf"), - agentClasses); - String useJavaAgent = "-javaagent:" + agentJar + "=jdk/internal/math/FDBigInteger"; - - // dump class list - CDSTestUtils.dumpClassList(classList, "-cp", appJar, mainClass); - - // create archive with the class list - CDSOptions opts = (new CDSOptions()) - .addPrefix("-XX:ExtraSharedClassListFile=" + classList, - "-cp", appJar, - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - useJavaAgent, - "-Xlog:class+load,cds+class=debug,aot,cds") - .setArchiveName(archiveName); - OutputAnalyzer output = CDSTestUtils.createArchiveAndCheck(opts); - output.shouldContain("heap objects cannot be written because class jdk.internal.math.FDBigInteger maybe modified by ClassFileLoadHook") - .shouldContain("Skipping jdk/internal/math/FDBigInteger: Unsupported location") - .shouldMatch(".class.load.*jdk.internal.math.FDBigInteger.*source.*modules"); - - // run with archive - CDSOptions runOpts = (new CDSOptions()) - .addPrefix("-cp", appJar, "-Xlog:class+load=debug,aot=debug,cds=debug,class+path=debug", - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - useJavaAgent) - .setArchiveName(archiveName) - .setUseVersion(false) - .addSuffix(mainClass); - output = CDSTestUtils.runWithArchive(runOpts); - TestCommon.checkExecReturn(output, 0, true, - "Hello source: shared objects file"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java b/test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java deleted file mode 100644 index 88620fc039f..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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. - * - * 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 8323950 - * @summary Transforming an interface of an archived lambda proxy class should not - * crash the VM. The lambda proxy class should be regenerated during runtime. - * @requires vm.cds - * @requires vm.cds.default.archive.available - * @requires vm.jvmti - * @requires vm.flagless - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @compile test-classes/SimpleTest.java - * @compile test-classes/TransformBootClass.java - * @run driver TransformInterfaceOfLambda - */ - -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class TransformInterfaceOfLambda { - - public static String agentClasses[] = { - TransformBootClass.class.getName(), - }; - - public static void main(String[] args) throws Exception { - String mainClass = SimpleTest.class.getName(); - String namePrefix = "transform-interface-of-lambda"; - JarBuilder.build(namePrefix, mainClass); - - String appJar = TestCommon.getTestJar(namePrefix + ".jar"); - - String agentJar = - ClassFileInstaller.writeJar("TransformBootClass.jar", - ClassFileInstaller.Manifest.fromSourceFile("test-classes/TransformBootClass.mf"), - agentClasses); - String useJavaAgent = "-javaagent:" + agentJar + "=java/util/function/IntFunction"; - - ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder( - "-cp", appJar, "-Xlog:class+load,cds=debug", - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - useJavaAgent, - mainClass); - OutputAnalyzer out = new OutputAnalyzer(pb.start()); - System.out.println(out.getStdout()); - out.shouldHaveExitValue(0) - // the class loaded by the SimpleTest should be from the archive - .shouldContain("[class,load] java.text.SimpleDateFormat source: shared objects file") - // the IntFunction is the interface which is being transformed. The - // interface is a super type of the following lambda proxy class. - .shouldContain("Transforming class java/util/function/IntFunction") - // the lambda proxy class should be regenerated - .shouldMatch(".class.load.*sun.util.locale.provider.LocaleProviderAdapter[$][$]Lambda/0x.*source:.*sun.util.locale.provider.LocaleProviderAdapter"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java index a0feb76a910..491a5bf1fa7 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java @@ -26,7 +26,7 @@ /* * @test id=static * @bug 8361725 - * @summary -javaagent should be disabled with -Xshare:dump -XX:+AOTClassLinking + * @summary -javaagent is not allowed when creating static CDS archive * @requires vm.cds.supports.aot.class.linking * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes * @build JavaAgent JavaAgentTransformer Util @@ -34,6 +34,18 @@ * @run driver JavaAgent STATIC */ +/** + * @test id=dynamic + * @bug 8362561 + * @summary -javaagent is not allowed when creating dynamic CDS archive + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes + * @build JavaAgent JavaAgentTransformer Util + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar JavaAgentApp JavaAgentApp$ShouldBeTransformed + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. JavaAgent DYNAMIC + */ + /* * @test id=aot * @summary -javaagent should be allowed in AOT workflow. However, classes transformed/redefined by agents will not @@ -64,7 +76,13 @@ public class JavaAgent { ClassFileInstaller.Manifest.fromSourceFile("JavaAgentTransformer.mf"), agentClasses); - new Tester().run(args); + Tester t = new Tester(); + if (args[0].equals("STATIC") || args[0].equals("DYNAMIC")) { + // Some child processes may have non-zero exits. These are checked by + // checkExecutionForStaticWorkflow() and checkExecutionForDynamicWorkflow + t.setCheckExitValue(false); + } + t.run(args); } static class Tester extends CDSAppTester { @@ -80,8 +98,6 @@ public class JavaAgent { @Override public String[] vmArgs(RunMode runMode) { return new String[] { - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", "-javaagent:" + agentJar, "-Xlog:aot,cds", "-XX:+AOTClassLinking", @@ -99,8 +115,10 @@ public class JavaAgent { public void checkExecution(OutputAnalyzer out, RunMode runMode) throws Exception { if (isAOTWorkflow()) { checkExecutionForAOTWorkflow(out, runMode); - } else { + } else if (isStaticWorkflow()) { checkExecutionForStaticWorkflow(out, runMode); + } else { + checkExecutionForDynamicWorkflow(out, runMode); } } @@ -133,12 +151,31 @@ public class JavaAgent { public void checkExecutionForStaticWorkflow(OutputAnalyzer out, RunMode runMode) throws Exception { switch (runMode) { - case RunMode.DUMP_STATIC: - out.shouldContain("Disabled all JVMTI agents with -Xshare:dump -XX:+AOTClassLinking"); - out.shouldNotContain(agentPremainFinished); - break; - default: + case RunMode.TRAINING: out.shouldContain(agentPremainFinished); + out.shouldHaveExitValue(0); + break; + case RunMode.DUMP_STATIC: + out.shouldContain("JVMTI agents are not allowed when dumping CDS archives"); + out.shouldNotHaveExitValue(0); + break; + case RunMode.PRODUCTION: + out.shouldContain("Unable to use shared archive: invalid archive"); + out.shouldNotHaveExitValue(0); + break; + } + } + + public void checkExecutionForDynamicWorkflow(OutputAnalyzer out, RunMode runMode) throws Exception { + switch (runMode) { + case RunMode.DUMP_DYNAMIC: + out.shouldContain("JVMTI agents are not allowed when dumping CDS archives"); + out.shouldNotHaveExitValue(0); + break; + case RunMode.PRODUCTION: + out.shouldContain("Unable to use shared archive: invalid archive"); + out.shouldNotHaveExitValue(0); + break; } } } diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java index e1f5f548593..9e43d00e872 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java @@ -40,20 +40,6 @@ * @run driver BulkLoaderTest STATIC */ -/* - * @test id=dynamic - * @requires vm.cds.supports.aot.class.linking - * @library /test/jdk/lib/testlibrary /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @build InitiatingLoaderTester BadOldClassA BadOldClassB - * @build jdk.test.whitebox.WhiteBox BulkLoaderTest SimpleCusty - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar BulkLoaderTestApp.jar BulkLoaderTestApp MyUtil InitiatingLoaderTester - * BadOldClassA BadOldClassB - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar - * SimpleCusty - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar WhiteBox.jar jdk.test.whitebox.WhiteBox - * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:WhiteBox.jar BulkLoaderTest DYNAMIC - */ - /* * @test id=aot * @requires vm.cds.supports.aot.class.linking @@ -279,10 +265,6 @@ class BulkLoaderTestApp { } try { - // In dynamic dump, the VM loads BadOldClassB and then attempts to - // link it. This will leave BadOldClassB in a "failed verification" state. - // All refernces to BadOldClassB from the CP should be purged from the CDS - // archive. c = BadOldClassB.class; c.newInstance(); throw new RuntimeException("Must not succeed"); diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java deleted file mode 100644 index 90d403bc380..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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. - * - * 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 8276184 - * @summary If the caller class is redefined during dump time, the caller class - * and its lambda proxy class should not be archived. - * @requires vm.cds - * @requires vm.jvmti - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds - * /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * /test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/test-classes - * @build jdk.test.whitebox.WhiteBox OldProvider - * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @run driver RedefineClassHelper - * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. RedefineCallerClassTest - */ - -import jdk.test.lib.helpers.ClassFileInstaller; - -public class RedefineCallerClassTest extends DynamicArchiveTestBase { - static String mainClass = RedefineCallerClass.class.getName(); - - static String providerClass = OldProvider.class.getName(); - - static String sharedClasses[] = { - mainClass, - "SimpleLambda", // caller class will be redefined in RedefineCallerClass - providerClass, // inteface with class file major version < 50 - "jdk/test/lib/compiler/InMemoryJavaCompiler", - "jdk/test/lib/compiler/InMemoryJavaCompiler$FileManagerWrapper", - "jdk/test/lib/compiler/InMemoryJavaCompiler$FileManagerWrapper$1", - "jdk/test/lib/compiler/InMemoryJavaCompiler$SourceFile", - "jdk/test/lib/compiler/InMemoryJavaCompiler$ClassFile" - }; - - public static void main(String[] args) throws Exception { - runTest(RedefineCallerClassTest::test); - } - - static void test() throws Exception { - String topArchiveName = getNewArchiveName(); - String appJar = ClassFileInstaller.writeJar("redefine_caller_class.jar", sharedClasses); - - String[] mainArgs = { - "redefineCaller", // redefine caller class only - "useOldInf", // use old interface only - "both" // both of the above - }; - - for (String mainArg : mainArgs) { - String[] options = { - "-Xlog:class+load,cds", - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - "-javaagent:redefineagent.jar", - "-cp", appJar, mainClass, mainArg - }; - - dump(topArchiveName, options) - .assertNormalExit(output -> { - output.shouldHaveExitValue(0); - if (mainArg.equals("both") || mainArg.equals("useOldInf")) { - output.shouldContain("Skipping OldProvider: Old class has been linked") - .shouldMatch("Skipping.SimpleLambda[$][$]Lambda.*0x.*:.*Old.class.has.been.linked"); - } - if (mainArg.equals("both") || mainArg.equals("redefineCaller")) { - output.shouldContain("Skipping SimpleLambda: Has been redefined"); - } - }); - - run(topArchiveName, options) - .assertNormalExit(output -> { - output.shouldHaveExitValue(0) - .shouldContain("RedefineCallerClass source: shared objects file (top)") - .shouldMatch(".class.load. SimpleLambda[$][$]Lambda.*/0x.*source:.*SimpleLambda"); - if (mainArg.equals("both") || mainArg.equals("useOldInf")) { - output.shouldMatch(".class.load. OldProvider.source:.*redefine_caller_class.jar"); - } - if (mainArg.equals("both") || mainArg.equals("redefineCaller")) { - output.shouldMatch(".class.load. SimpleLambda.source:.*redefine_caller_class.jar"); - } - }); - } - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java deleted file mode 100644 index 5d9ef9ef760..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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. - * - * 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 - * @summary Out of memory When dumping the CDS archive - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds.write.archived.java.heap - * @requires vm.jvmti - * @run driver ExceptionDuringDumpAtObjectsInitPhase - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class ExceptionDuringDumpAtObjectsInitPhase { - public static String appClasses[] = { - Hello.class.getName(), - }; - public static String agentClasses[] = { - GCDuringDumpTransformer.class.getName(), - GCDuringDumpTransformer.MyCleaner.class.getName(), - }; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("GCDuringDumpTransformer.jar", - ClassFileInstaller.Manifest.fromSourceFile("GCDuringDumpTransformer.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("GCDuringDumpApp.jar", appClasses); - - String gcLog = Boolean.getBoolean("test.cds.verbose.gc") ? - "-Xlog:gc*=info,gc+region=trace,gc+alloc+region=debug" : "-showversion"; - - // 1. Test with exception - System.out.println("1. Exception during dump"); - TestCommon.dump(appJar, - TestCommon.list(Hello.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - "-javaagent:" + agentJar, - "-Xlog:cds,class+load", - "-Xmx32m", - "-Dtest.with.exception=true", - gcLog).shouldNotHaveExitValue(0) - .shouldContain("Preload Warning: Cannot find jdk/internal/math/FDBigInteger") - .shouldContain("Unexpected exception, use -Xlog:aot,cds,exceptions=trace for detail"); - - // 2. Test with OOM - System.out.println("2. OOM during dump"); - TestCommon.dump(appJar, - TestCommon.list(Hello.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - "-javaagent:" + agentJar, - "-Dtest.with.oom=true", - "-Xlog:cds,class+load", - "-Xmx12M", - gcLog).shouldNotHaveExitValue(0) - .shouldContain("Out of memory. Please run with a larger Java heap, current MaxHeapSize"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java deleted file mode 100644 index dd57c7efa2e..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2017, 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. - * - * 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 - * @summary When dumping the CDS archive, try to cause garbage collection while classes are being loaded. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds - * @requires vm.jvmti - * @run driver GCDuringDump - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class GCDuringDump { - public static String appClasses[] = { - Hello.class.getName(), - }; - public static String agentClasses[] = { - GCDuringDumpTransformer.class.getName(), - GCDuringDumpTransformer.MyCleaner.class.getName(), - }; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("GCDuringDumpTransformer.jar", - ClassFileInstaller.Manifest.fromSourceFile("GCDuringDumpTransformer.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("GCDuringDumpApp.jar", appClasses); - - String gcLog = Boolean.getBoolean("test.cds.verbose.gc") ? - "-Xlog:gc*=info,gc+region=trace,gc+alloc+region=debug" : "-showversion"; - - for (int i=0; i<3; i++) { - // i = 0 -- run without agent = no extra GCs - // i = 1 -- run with agent = cause extra GCs - // i = 2 -- run with agent = cause extra GCs + use java.lang.ref.Cleaner - - String extraArg = (i == 0) ? "-showversion" : "-javaagent:" + agentJar; - String extraOption = (i == 0) ? "-showversion" : "-XX:+AllowArchivingWithJavaAgent"; - String extraOption2 = (i != 2) ? "-showversion" : "-Dtest.with.cleaner=true"; - - TestCommon.testDump(appJar, TestCommon.list(Hello.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", extraOption, extraOption2, - "-Xlog:exceptions=trace", - extraArg, "-Xmx32m", gcLog); - - TestCommon.run( - "-cp", appJar, - "-Xmx32m", - "-Xlog:cds=info", - "-XX:+UnlockDiagnosticVMOptions", extraOption, - gcLog, - Hello.class.getName()) - .assertNormalExit(); - } - } -} - diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java deleted file mode 100644 index 1dac0149964..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2017, 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. - * - * 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 - * @summary Similar to GCDuringDumping.java, this test adds the -XX:SharedArchiveConfigFile - * option for testing the interaction with GC and shared strings. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds.write.archived.java.heap - * @requires vm.jvmti - * @build jdk.test.whitebox.WhiteBox - * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @run driver/timeout=480 GCSharedStringsDuringDump - */ - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.cds.CDSTestUtils; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class GCSharedStringsDuringDump { - static { - // EpsilonGC will run out of memory. - CDSOptions.disableRuntimePrefixForEpsilonGC(); - } - public static String appClasses[] = { - GCSharedStringsDuringDumpWb.class.getName(), - }; - public static String agentClasses[] = { - GCDuringDumpTransformer.class.getName(), - }; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("GCDuringDumpTransformer.jar", - ClassFileInstaller.Manifest.fromSourceFile("GCDuringDumpTransformer.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("GCSharedStringsDuringDumpApp.jar", appClasses); - - String gcLog = Boolean.getBoolean("test.cds.verbose.gc") ? - "-Xlog:gc*=info,gc+region=trace,gc+alloc+region=debug" : "-showversion"; - - String sharedArchiveCfgFile = - CDSTestUtils.getOutputDir() + File.separator + "GCSharedStringDuringDump_gen.txt"; - try (FileOutputStream fos = new FileOutputStream(sharedArchiveCfgFile)) { - PrintWriter out = new PrintWriter(new OutputStreamWriter(fos)); - out.println("VERSION: 1.0"); - out.println("@SECTION: String"); - out.println("31: shared_test_string_unique_14325"); - for (int i=0; i<100000; i++) { - String s = "generated_string " + i; - out.println(s.length() + ": " + s); - } - out.close(); - } - - JarBuilder.build(true, "WhiteBox", "jdk/test/whitebox/WhiteBox"); - String whiteBoxJar = TestCommon.getTestJar("WhiteBox.jar"); - String bootClassPath = "-Xbootclasspath/a:" + whiteBoxJar; - - for (int i=0; i<2; i++) { - // i = 0 -- run without agent = no extra GCs - // i = 1 -- run with agent = cause extra GCs - - String extraArg = (i == 0) ? "-showversion" : "-javaagent:" + agentJar; - String extraOption = (i == 0) ? "-showversion" : "-XX:+AllowArchivingWithJavaAgent"; - OutputAnalyzer output = TestCommon.dump( - appJar, TestCommon.list(GCSharedStringsDuringDumpWb.class.getName()), - bootClassPath, extraArg, "-Xmx32m", gcLog, - "-XX:SharedArchiveConfigFile=" + sharedArchiveCfgFile, - "-XX:+UnlockDiagnosticVMOptions", extraOption); - - if (output.getStdout().contains("Too many string space regions") || - output.getStderr().contains("Unable to write archive heap memory regions") || - output.getStdout().contains("Try increasing NewSize") || - !output.getStdout().contains("oa0 space:") || - output.getExitValue() != 0) { - // Try again with larger heap and NewSize, this should increase the - // G1 heap region size to 2M - TestCommon.testDump( - appJar, TestCommon.list(GCSharedStringsDuringDumpWb.class.getName()), - bootClassPath, extraArg, "-Xmx8g", "-XX:NewSize=8m", gcLog, - "-XX:SharedArchiveConfigFile=" + sharedArchiveCfgFile, - "-XX:+UnlockDiagnosticVMOptions", extraOption); - } - - TestCommon.run( - "-cp", appJar, - bootClassPath, - extraArg, - "-Xlog:cds=info,class+path=info", - "-Xmx32m", - "-Xlog:cds=info", - "-XX:+UnlockDiagnosticVMOptions", - extraOption, - "-XX:+WhiteBoxAPI", - gcLog, - GCSharedStringsDuringDumpWb.class.getName()) - .assertNormalExit(); - } - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java deleted file mode 100644 index 391596160d6..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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. - * - * 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 8249276 - * @summary When dumping the CDS archive, try to lock some objects. These objects should be archived - * without the locking bits in the markWord. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds - * @requires vm.cds - * @requires vm.jvmti - * @modules java.instrument - * @run driver LockDuringDump - */ - -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class LockDuringDump { - public static String appClasses[] = { - LockDuringDumpApp.class.getName(), - }; - public static String agentClasses[] = { - LockDuringDumpAgent.class.getName(), - }; - - private static final String MANIFEST = - "Manifest-Version: 1.0\nPremain-Class: LockDuringDumpAgent\n"; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("LockDuringDumpAgent.jar", - ClassFileInstaller.Manifest.fromString(MANIFEST), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("LockDuringDumpApp.jar", appClasses); - - for (int i = 0; i < 3; i++) { - // i = 0 -- dump without agent - // i = 1 -- dump with agent - - String agentArg = (i == 0) ? "-showversion" : "-javaagent:" + agentJar; - String agentArg2 = (i == 0) ? "-showversion" : "-XX:+AllowArchivingWithJavaAgent"; - - OutputAnalyzer out = - TestCommon.testDump(appJar, TestCommon.list(LockDuringDumpApp.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", - agentArg, agentArg2); - if (i != 0 && !out.getStdout().contains("LockDuringDumpAgent timeout")) { - out.shouldContain("Let's hold the lock on the literal string"); - } - - TestCommon.run( - "-cp", appJar, - "-XX:+UnlockDiagnosticVMOptions", agentArg2, - LockDuringDumpApp.class.getName()) - .assertNormalExit("I am able to lock the literal string"); - } - } -} - diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java deleted file mode 100644 index 54cb3fb23fb..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 - * 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 - * @summary CDS dumping with java agent. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds - * @requires vm.jvmti - * @build SimpleAgent Hello AppWithBMH - * @run main/othervm DumpingWithJavaAgent - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class DumpingWithJavaAgent { - public static String appClasses[] = { - "Hello", - "AppWithBMH", - }; - public static String agentClasses[] = { - "SimpleAgent", - "SimpleAgent$1" - }; - - public static String warningMessages[] = { - "This shared archive file was created with AllowArchivingWithJavaAgent", - "It should be used for testing purposes only and should not be used in a production environment", - }; - - public static String errorMessage = - "The setting of the AllowArchivingWithJavaAgent is different from the setting in the shared archive file."; - - public static String diagnosticOption = "-XX:+AllowArchivingWithJavaAgent"; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("SimpleAgent.jar", - ClassFileInstaller.Manifest.fromSourceFile("SimpleAgent.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("DumpingWithJavaAgent.jar", appClasses); - - // CDS dumping with a java agent performing class transformation on BoundMethodHandle$Species classes - OutputAnalyzer output = TestCommon.testDump(appJar, TestCommon.list("AppWithBMH"), - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "-javaagent:" + agentJar + "=doTransform", - "AppWithBMH"); - TestCommon.checkDump(output); - output.shouldContain(warningMessages[0]); - output.shouldContain(warningMessages[1]); - output.shouldContain("inside SimpleAgent"); - - // CDS dumping with a java agent with the AllowArchvingWithJavaAgent diagnostic option. - output = TestCommon.testDump(appJar, TestCommon.list("Hello"), - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "-javaagent:" + agentJar); - TestCommon.checkDump(output); - output.shouldContain(warningMessages[0]); - output.shouldContain(warningMessages[1]); - output.shouldContain("inside SimpleAgent"); - - // Using the archive with the AllowArchvingWithJavaAgent diagnostic option. - output = TestCommon.exec( - appJar, - "-Xlog:class+load=trace", - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "Hello"); - if (!TestCommon.isUnableToMap(output)) { - output.shouldHaveExitValue(0); - output.shouldContain(warningMessages[0]); - output.shouldContain(warningMessages[1]); - output.shouldContain("[class,load] Hello source: shared objects file"); - } - - // Using the archive with -Xshare:on without the diagnostic option. - // VM should exit with an error message. - output = TestCommon.exec( - appJar, - "Hello"); - output.shouldHaveExitValue(1); - output.shouldContain(errorMessage); - - // Using the archive with -Xshare:auto without the diagnostic option. - // VM should continue execution with a warning message. The archive - // will not be used. - output = TestCommon.execAuto( - "-cp", appJar, - "-Xlog:class+load=trace,cds=info", - "Hello"); - if (!TestCommon.isUnableToMap(output)) { - output.shouldHaveExitValue(0); - output.shouldContain(errorMessage); - output.shouldMatch(".class.load.* Hello source:.*DumpingWithJavaAgent.jar"); - - // CDS dumping with a java agent without the AllowArchvingWithJavaAgent diagnostic option. - // VM will exit with an error message. - output = TestCommon.dump(appJar, TestCommon.list("Hello"), - "-javaagent:" + agentJar); - } - output.shouldContain("Must enable AllowArchivingWithJavaAgent in order to run Java agent during CDS dumping") - .shouldHaveExitValue(1); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java deleted file mode 100644 index 9f3c5864f1b..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2018, 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 - * 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 - * @summary CDS dumping with JVMTI agent. - * @requires vm.cds - * @requires vm.jvmti - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds - * @compile ../../test-classes/Hello.java - * @run main/othervm/native DumpingWithJvmtiAgent - */ - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import jdk.test.lib.process.OutputAnalyzer; - -public class DumpingWithJvmtiAgent { - private static final String AGENT_LIB_ONLOAD = "AddToSystemCLSearchOnLoad"; - - public static void main(String[] args) throws Exception { - String appJar = JarBuilder.getOrCreateHelloJar(); - - // CDS dump with a JVMTI agent with the AllowArchivingWithJavaAgent option. - // vm should exit with an error message. - OutputAnalyzer out = TestCommon.dump( - appJar, - TestCommon.list("Hello"), - "-XX:+UnlockDiagnosticVMOptions", "-XX:+AllowArchivingWithJavaAgent", - "-agentlib:" + AGENT_LIB_ONLOAD + "=" + appJar, - "-Djava.library.path=" + System.getProperty("java.library.path")); - out.shouldContain("CDS dumping does not support native JVMTI agent, name: " + AGENT_LIB_ONLOAD) - .shouldHaveExitValue(1); - - // CDS dump with a JVMTI agent without the AllowArchivingWithJavaAgent option. - // vm should exit with an error message. - out = TestCommon.dump( - appJar, - TestCommon.list("Hello"), - "-agentlib:" + AGENT_LIB_ONLOAD + "=" + appJar, - "-Djava.library.path=" + System.getProperty("java.library.path")); - out.shouldContain("CDS dumping does not support native JVMTI agent, name: " + AGENT_LIB_ONLOAD) - .shouldHaveExitValue(1); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java deleted file mode 100644 index a0a34eab9f9..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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. - * - * 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 8261090 - * @summary Dump old class with java agent. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds - * @requires vm.jvmti - * @compile ../../test-classes/OldSuper.jasm - * @compile SimpleAgent.java - * @run main/othervm OldClassWithJavaAgent - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class OldClassWithJavaAgent { - public static String appClasses[] = {"OldSuper"}; - public static String agentClasses[] = {"SimpleAgent"}; - public static String diagnosticOption = "-XX:+AllowArchivingWithJavaAgent"; - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("SimpleAgent.jar", - ClassFileInstaller.Manifest.fromSourceFile("SimpleAgent.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("OldClassWithJavaAgent.jar", appClasses); - OutputAnalyzer output = TestCommon.testDump(appJar, TestCommon.list("OldSuper"), - "-Xlog:cds=debug,class+load", - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "-javaagent:" + agentJar + "=OldSuper"); - - // The java agent will load and link the class. We will skip old classes - // which have been linked during CDS dump. - output.shouldContain("Skipping OldSuper: Old class has been linked"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java deleted file mode 100644 index d280964c07d..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 - * 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. - * - */ -import java.lang.instrument.ClassFileTransformer; -import java.lang.instrument.Instrumentation; -import java.security.ProtectionDomain; -import java.util.Arrays; - -public class SimpleAgent { - public static void premain(String agentArg, Instrumentation instrumentation) throws Exception { - System.out.println("inside SimpleAgent"); - if (agentArg == null) return; - if (agentArg.equals("OldSuper")) { - // Only load the class if the test requires it. - Class cls = Class.forName("OldSuper", true, ClassLoader.getSystemClassLoader()); - } else if (agentArg.equals("doTransform")) { - ClassFileTransformer transformer = new ClassFileTransformer() { - @Override - public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { - if (loader == SimpleAgent.class.getClassLoader()) { - // Transform only classes loaded by the apploader. - System.out.printf("%n Transforming %s", className); - return Arrays.copyOf(classfileBuffer, classfileBuffer.length); - } else { - return null; - } - } - }; - instrumentation.addTransformer(transformer); - } - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf deleted file mode 100644 index f53b5299627..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 -Premain-Class: SimpleAgent diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java index 7581b2367aa..26bccdf764e 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -67,7 +67,6 @@ public class OldClassAndRedefineClass { out = TestCommon.exec( appJar, "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", "-XX:+WhiteBoxAPI", "-Xlog:cds,class+load", agentCmdArg, diff --git a/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java b/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java index 2610dac16c0..bc2ac9db2ab 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java @@ -73,7 +73,9 @@ public class ResolvedConstants { static boolean aotClassLinking; public static void main(String[] args) throws Exception { test(args, false); - test(args, true); + if (!args[0].equals("DYNAMIC")) { + test(args, true); + } } static void test(String[] args, boolean testMode) throws Exception { diff --git a/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java b/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java index 47590ec3d54..1447012073a 100644 --- a/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java +++ b/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java @@ -99,8 +99,8 @@ public class SystemMapTestBase { regexBase_java_heap + "JAVAHEAP.*", // metaspace regexBase_committed + "META.*", - // parts of metaspace should be uncommitted - regexBase + "-" + space + "META.*", + // parts of metaspace should be uncommitted, those parts may or may not be be thp-eligible + regexBase + "(-|thpel)" + space + "META.*", // code cache regexBase_committed + "CODE.*", // Main thread stack diff --git a/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java index 5af6b1e7322..0c447610013 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java +++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, 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 @@ -82,37 +82,47 @@ public class SelfSuspendDisablerTest { }); Thread t2 = Thread.ofVirtual().factory().newThread(() -> { testJvmtiThreadState(Thread.currentThread(), RUNNABLE); - while(!isSuspended(t1)) { + selfSuspend(); + }); + Thread t3 = Thread.ofVirtual().factory().newThread(() -> { + testJvmtiThreadState(Thread.currentThread(), RUNNABLE); + while(!isSuspended(t1) || !isSuspended(t2)) { Thread.yield(); } Thread.yield(); // provoke unmount testJvmtiThreadState(t1, SUSPENDED); + testJvmtiThreadState(t2, SUSPENDED); resume(t1); + resume(t2); suspendAllVirtualThreads(); }); testJvmtiThreadState(t1, NEW); testJvmtiThreadState(t2, NEW); + testJvmtiThreadState(t3, NEW); t1.start(); t2.start(); + t3.start(); - while(!isSuspended(t2)) { + while(!isSuspended(t3)) { sleep(100); } - testJvmtiThreadState(t2, SUSPENDED); + testJvmtiThreadState(t3, SUSPENDED); resumeAllVirtualThreads(); + t3.join(); t2.join(); t1.join(); testJvmtiThreadState(t1, TERMINATED); testJvmtiThreadState(t2, TERMINATED); + testJvmtiThreadState(t3, TERMINATED); } } diff --git a/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java b/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java index 895e5504feb..b7ed9cc72c8 100644 --- a/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java +++ b/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, 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 @@ -36,7 +36,7 @@ import jdk.test.lib.process.OutputAnalyzer; public class TestBasicLogOutput { public static void main(String[] args) throws Exception { - ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:all=trace", "-version"); + ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:all=info", "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldMatch("\\[logging *\\]"); // expected tag(s) output.shouldContain("Log configuration fully initialized."); // expected message diff --git a/test/hotspot/jtreg/serviceability/logging/TestFullNames.java b/test/hotspot/jtreg/serviceability/logging/TestFullNames.java index 0a3bc855d8f..cd3fcc55772 100644 --- a/test/hotspot/jtreg/serviceability/logging/TestFullNames.java +++ b/test/hotspot/jtreg/serviceability/logging/TestFullNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, 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 @@ -64,7 +64,7 @@ public class TestFullNames { Asserts.assertFalse(file.exists()); // Run with logging=trace on stdout so that we can verify the log configuration afterwards. ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:logging=trace", - "-Xlog:all=trace:" + logOutput, + "-Xlog:all=info:" + logOutput, "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldHaveExitValue(0); diff --git a/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java b/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java index dea656130ab..db724aeb86a 100644 --- a/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java +++ b/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, 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 @@ -74,7 +74,7 @@ public class TestQuotedLogOutputs { for (String logOutput : validOutputs) { // Run with logging=trace on stdout so that we can verify the log configuration afterwards. ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:logging=trace", - "-Xlog:all=trace:" + logOutput, + "-Xlog:all=info:" + logOutput, "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldHaveExitValue(0); @@ -99,7 +99,7 @@ public class TestQuotedLogOutputs { }; for (String logOutput : invalidOutputs) { ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:logging=trace", - "-Xlog:all=trace:" + logOutput, + "-Xlog:all=info:" + logOutput, "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldHaveExitValue(1); diff --git a/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java b/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java index cb90067f2f7..b6919a566a9 100644 --- a/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java +++ b/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java @@ -22,7 +22,7 @@ */ /** - * @test + * @test id=parallel * @bug 8192985 * @summary Test the clhsdb 'scanoops' command * @requires vm.gc.Parallel @@ -33,7 +33,7 @@ */ /** - * @test + * @test id=serial * @bug 8192985 * @summary Test the clhsdb 'scanoops' command * @requires vm.gc.Serial diff --git a/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java b/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java index d721c7d63c5..11694f72837 100644 --- a/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java +++ b/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java @@ -292,6 +292,8 @@ public class CtwRunner { "--add-exports", "java.base/jdk.internal.misc=ALL-UNNAMED", "--add-exports", "java.base/jdk.internal.reflect=ALL-UNNAMED", "--add-exports", "java.base/jdk.internal.access=ALL-UNNAMED", + // Graphics clinits may run, force headless mode + "-Djava.awt.headless=true", // enable diagnostic logging "-XX:+LogCompilation", // use phase specific log, hs_err and ciReplay files diff --git a/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java b/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java index c7f8badf83b..5615cce983a 100644 --- a/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java +++ b/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java @@ -1486,7 +1486,7 @@ class CompilationOutputOfFails { @Test @IR(failOn = IRNode.ALLOC) - @IR(counts = {IRNode.COUNTED_LOOP, "1"}) // not fail + @IR(counts = {IRNode.COUNTED_LOOP, ">1"}) // not fail public void macro3() { for (int i = 0; i < 100; i++) { obj = new Object(); diff --git a/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java b/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java index 1ab01e8179f..eff35559626 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java +++ b/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.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 @@ -30,6 +30,11 @@ /* @test @key stress randomness @library /vmTestbase /test/lib @run main/othervm -Xlog:gc=debug:gc.log gc.ArrayJuggle.Juggle2 */ /* @test @key stress randomness @library /vmTestbase /test/lib @run main/othervm -Xlog:gc=debug:gc.log gc.ArrayJuggle.Juggle2 -tg */ +/* + * The next test stresses the interaction between (mostly) full garbage collections and refinement. + */ +/* @test @key stress randomness @library /vmTestbase /test/lib @run main/othervm -XX:-G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=0 -XX:G1HeapRegionSize=1m -XX:G1RSetUpdatingPauseTimePercent=0 -XX:+UnlockDiagnosticVMOptions -XX:G1PerThreadPendingCardThreshold=0 -XX:+VerifyAfterGC -Xlog:gc=debug,gc+refine=debug:gc.log gc.ArrayJuggle.Juggle2 -tg */ + package gc.ArrayJuggle; import nsk.share.test.*; diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java index b5ea8c34cbd..8cc023acaae 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java @@ -244,6 +244,7 @@ public class referringObjects001 extends HeapwalkingDebugger { } } } catch (Throwable t) { + setSuccess(false); log.complain("Unexpected exception:"); t.printStackTrace(log.getOutStream()); } diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java index 862600dc697..a04b323d0d1 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2024, 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 @@ -45,12 +45,11 @@ * - Debugee VM * - stop all threads and remove all references to threads and thread group * - Debugger VM - * - check that thread group have only 1 referrer: parent thread group - * - check that threre are no references to test threads in target VM - * - Debugger VM - * - test ObjectReference.disableCollection, ObjectReference.enableCollection for ThreadGroupReference: - * can't force collection of thread group because of thread group always has 1 referrer - parent thread group, so - * just test disableCollection/enableCollection don't throw any unexpected exceptions + * - force GC + * - check that the test thread group has been collected. The reference from the + * parent thread group is weak and should not prevent the test thread group from + * being collected. + * - check that there are no references to test threads in target VM * * @requires !vm.graal.enabled * @library /vmTestbase @@ -107,9 +106,8 @@ public class referringObjects003 extends HeapwalkingDebugger { if (referrerCount != expectedCount) { setSuccess(false); - log - .complain("List with wrong size was returned by ObjectReference.referringObjects(ThreadGroupReference): " - + referrerCount + ", expected: " + expectedCount); + log.complain("List with wrong size was returned by ObjectReference.referringObjects(ThreadGroupReference): " + + referrerCount + ", expected: " + expectedCount); } } } @@ -181,57 +179,36 @@ public class referringObjects003 extends HeapwalkingDebugger { if (!isDebuggeeReady()) return; - checkDebugeeAnswer_instances("java.lang.ThreadGroup", threadGroupsToFilter.size() + 1); + // Force the test ThreadGroup to be collected. The only reference to it is a weak + // reference from the parent ThreadGroup. + forceGC(); + + checkDebugeeAnswer_instances("java.lang.ThreadGroup", threadGroupsToFilter.size()); checkDebugeeAnswer_instances("java.lang.Thread", threadsToFilter.size()); threadGroups = HeapwalkingDebugger.filterObjectReferrence(threadGroupsToFilter, HeapwalkingDebugger .getObjectReferences("java.lang.ThreadGroup", vm)); - // 1 referrer(parent thread group) is left - checkThreadGroupReferrersCount(threadGroups, 1); + if (threadGroups.size() != 0) { + setSuccess(false); + log.complain("All test threads groups should be removed"); + log.complain("Unexpected threads groups:"); + for (ObjectReference objectReference : threadGroups) { + log.complain(objectReference.toString()); + } + } threads = HeapwalkingDebugger.filterObjectReferrence(threadsToFilter, HeapwalkingDebugger.getObjectReferences( "java.lang.Thread", vm)); if (threads.size() != 0) { + setSuccess(false); log.complain("All test threads should be removed"); log.complain("Unexpected threads:"); for (ObjectReference objectReference : threads) { log.complain(objectReference.toString()); } } - - checkThreadGroupDisableCollection(threadGroups); - } - - // can't force collection of thread group because of 1 reference is always - // left in parent tread group - public void checkThreadGroupDisableCollection(List objectReferences) { - try { - for (ObjectReference objectReference : objectReferences) - objectReference.disableCollection(); - } catch (Throwable t) { - log.complain("Unexpected exception: " + t); - t.printStackTrace(log.getOutStream()); - } - - forceGC(); - try { - for (ObjectReference objectReference : objectReferences) - objectReference.enableCollection(); - } catch (Throwable t) { - log.complain("Unexpected exception: " + t); - t.printStackTrace(log.getOutStream()); - } - - forceGC(); - try { - for (ObjectReference objectReference : objectReferences) - objectReference.referringObjects(0); - } catch (Throwable t) { - log.complain("Unexpected exception: " + t); - t.printStackTrace(log.getOutStream()); - } } } diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java index 8cc997fa4ab..f1d18211f3d 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java @@ -25,9 +25,10 @@ package nsk.jvmti.ResourceExhausted; import java.io.File; import java.io.FileInputStream; import java.io.PrintStream; +import java.net.URI; +import java.nio.file.Path; +import java.security.CodeSource; import java.security.ProtectionDomain; -import java.util.regex.Pattern; -import java.util.regex.Matcher; import nsk.share.Consts; import nsk.share.test.Stresser; @@ -80,24 +81,12 @@ public class resexhausted003 { public static int run(String args[], PrintStream out) { - String testclasspath = System.getProperty("test.class.path"); - String [] testpaths = testclasspath.split(System.getProperty("path.separator")); - String classesDir = ""; - - Pattern pattern = Pattern.compile("^(.*)classes(.*)vmTestbase(.*)$"); - for (int i = 0 ; i < testpaths.length; i++) { - if (pattern.matcher(testpaths[i]).matches()) { - classesDir = testpaths[i]; - } - } - if (classesDir.equals("")) { - System.err.println("TEST BUG: Classes directory not found in test,class.path."); - return Consts.TEST_FAILED; - } Stresser stress = new Stresser(args); String className = Helper.class.getName(); - byte[] bloatBytes = fileBytes(classesDir + File.separator + className.replace('.', '/') + ".class"); + CodeSource classCodeSource = Helper.class.getProtectionDomain().getCodeSource(); + Path classFilePath = Path.of(URI.create(classCodeSource.getLocation().toString())); + byte[] bloatBytes = fileBytes(classFilePath.resolve(className.replace('.', '/') + ".class").toString()); int count = 0; Helper.resetExhaustedEvent(); diff --git a/test/jaxp/TEST.ROOT b/test/jaxp/TEST.ROOT index 6398c399f0a..bafa67a700e 100644 --- a/test/jaxp/TEST.ROOT +++ b/test/jaxp/TEST.ROOT @@ -23,7 +23,7 @@ modules=java.xml groups=TEST.groups # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Path to libraries in the topmost test directory. This is needed so @library # does not need ../../ notation to reach them diff --git a/test/jdk/ProblemList-Virtual.txt b/test/jdk/ProblemList-Virtual.txt index d7692b578cd..d578f296ff9 100644 --- a/test/jdk/ProblemList-Virtual.txt +++ b/test/jdk/ProblemList-Virtual.txt @@ -55,7 +55,6 @@ java/lang/StackWalker/StackWalkTest.java 0000000 generic-all java/lang/StackWalker/CallerFromMain.java 0000000 generic-all java/lang/Thread/MainThreadTest.java 0000000 generic-all java/lang/Thread/UncaughtExceptionsTest.java 0000000 generic-all -java/lang/Thread/virtual/GetStackTraceWhenRunnable.java 0000000 generic-all java/lang/invoke/condy/CondyNestedResolutionTest.java 0000000 generic-all java/lang/ref/OOMEInReferenceHandler.java 0000000 generic-all java/util/concurrent/locks/Lock/OOMEInAQS.java 0000000 generic-all @@ -63,9 +62,6 @@ jdk/internal/vm/Continuation/Scoped.java 0000000 generic-all # The problems with permissions jdk/jfr/startupargs/TestDumpOnExit.java 0000000 generic-all -java/lang/SecurityManager/modules/CustomSecurityManagerTest.java 0000000 generic-all -java/util/PluggableLocale/PermissionTest.java 0000000 generic-all -java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all javax/management/ImplementationVersion/ImplVersionTest.java 0000000 generic-all javax/management/remote/mandatory/version/ImplVersionTest.java 0000000 generic-all diff --git a/test/jdk/ProblemList-Xcomp.txt b/test/jdk/ProblemList-Xcomp.txt index 680806e6e31..5ed171a1fea 100644 --- a/test/jdk/ProblemList-Xcomp.txt +++ b/test/jdk/ProblemList-Xcomp.txt @@ -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 @@ -29,4 +29,3 @@ java/lang/invoke/MethodHandles/CatchExceptionTest.java 8146623 generic-all java/lang/reflect/callerCache/ReflectionCallerCacheTest.java 8332028 generic-all -com/sun/jdi/InterruptHangTest.java 8043571 generic-all diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 2628d18ae54..d4685743fd7 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -409,7 +409,6 @@ java/awt/Mouse/EnterExitEvents/ResizingFrameTest.java 8005021 macosx-all java/awt/Mouse/EnterExitEvents/FullscreenEnterEventTest.java 8051455 macosx-all java/awt/Mouse/MouseModifiersUnitTest/MouseModifiersUnitTest_Standard.java 7124407,8302787 macosx-all,windows-all java/awt/Mouse/RemovedComponentMouseListener/RemovedComponentMouseListener.java 8157170 macosx-all -java/awt/Modal/ToFront/DialogToFrontModeless1Test.java 8213530 linux-all java/awt/Modal/ToFront/DialogToFrontNonModalTest.java 8221899 linux-all java/awt/Modal/ToBack/ToBackAppModal1Test.java 8196441 linux-all,macosx-all java/awt/Modal/ToBack/ToBackAppModal2Test.java 8196441 linux-all,macosx-all @@ -453,7 +452,6 @@ java/awt/Focus/NonFocusableBlockedOwnerTest/NonFocusableBlockedOwnerTest.java 71 java/awt/Focus/TranserFocusToWindow/TranserFocusToWindow.java 6848810 macosx-all,linux-all java/awt/FileDialog/ModalFocus/FileDialogModalFocusTest.java 8194751 linux-all java/awt/image/VolatileImage/BitmaskVolatileImage.java 8133102 linux-all -java/awt/SplashScreen/MultiResolutionSplash/unix/UnixMultiResolutionSplashTest.java 8203004 linux-all java/awt/ScrollPane/ScrollPaneEventType.java 8296516 macosx-all java/awt/Robot/AcceptExtraMouseButtons/AcceptExtraMouseButtons.java 7107528 linux-all,macosx-all java/awt/Mouse/MouseDragEvent/MouseDraggedTest.java 8080676 linux-all @@ -472,7 +470,7 @@ java/awt/event/MouseEvent/ClickDuringKeypress/ClickDuringKeypress.java 8233568 m java/awt/event/KeyEvent/DeadKey/DeadKeyMacOSXInputText.java 8233568 macosx-all java/awt/event/KeyEvent/DeadKey/deadKeyMacOSX.java 8233568 macosx-all java/awt/TrayIcon/RightClickWhenBalloonDisplayed/RightClickWhenBalloonDisplayed.java 8238720 windows-all -java/awt/PopupMenu/PopupMenuLocation.java 8259913,8315878 windows-all,macosx-aarch64 +java/awt/PopupMenu/PopupMenuLocation.java 8259913 windows-all java/awt/GridLayout/ComponentPreferredSize/ComponentPreferredSize.java 8238720,8324782 windows-all,macosx-all java/awt/GridLayout/ChangeGridSize/ChangeGridSize.java 8238720,8324782 windows-all,macosx-all java/awt/GridBagLayout/ComponentShortage.java 8355280 windows-all,linux-all @@ -491,7 +489,6 @@ java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeForModalDia java/awt/KeyboardFocusmanager/TypeAhead/MenuItemActivatedTest/MenuItemActivatedTest.java 8302787 windows-all java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java 8321303 linux-all -java/awt/Window/GetScreenLocation/GetScreenLocationTest.java 8225787 linux-x64 java/awt/Dialog/MakeWindowAlwaysOnTop/MakeWindowAlwaysOnTop.java 8266243 macosx-aarch64 java/awt/Dialog/ChoiceModalDialogTest.java 8161475 macosx-all java/awt/Dialog/FileDialogUserFilterTest.java 8001142 generic-all @@ -532,6 +529,7 @@ java/lang/invoke/LFCaching/LFMultiThreadCachingTest.java 8151492 generic- java/lang/invoke/LFCaching/LFGarbageCollectedTest.java 8078602 generic-all java/lang/invoke/lambda/LambdaFileEncodingSerialization.java 8249079 linux-all java/lang/invoke/RicochetTest.java 8251969 generic-all +java/lang/ProcessBuilder/Basic.java#id0 8368192 macosx-all ############################################################################ @@ -545,7 +543,7 @@ java/lang/instrument/RetransformBigClass.sh 8065756 generic- # jdk_io java/io/pathNames/GeneralWin32.java 8180264 windows-all -java/io/IO/IO.java 8337935 linux-ppc64le +java/lang/IO/IO.java 8337935 linux-ppc64le ############################################################################ @@ -555,7 +553,6 @@ com/sun/management/OperatingSystemMXBean/GetProcessCpuLoad.java 8030957 aix-all com/sun/management/OperatingSystemMXBean/GetSystemCpuLoad.java 8030957 aix-all java/lang/management/MemoryMXBean/Pending.java 8158837 generic-all -java/lang/management/MemoryMXBean/PendingAllGC.sh 8158837 generic-all java/lang/management/ThreadMXBean/ThreadMXBeanStateTest.java 8247426 generic-all sun/management/jdp/JdpDefaultsTest.java 8308807,8241865 aix-ppc64,macosx-all @@ -724,8 +721,6 @@ javax/swing/plaf/synth/7158712/bug7158712.java 8324782 macosx-all # jdk_jdi -com/sun/jdi/RepStep.java 8043571 generic-all - com/sun/jdi/InvokeHangTest.java 8218463 linux-all ############################################################################ diff --git a/test/jdk/TEST.ROOT b/test/jdk/TEST.ROOT index 0fa78bebc3f..9d12d384399 100644 --- a/test/jdk/TEST.ROOT +++ b/test/jdk/TEST.ROOT @@ -121,7 +121,7 @@ requires.properties= \ jdk.static # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Path to libraries in the topmost test directory. This is needed so @library # does not need ../../ notation to reach them diff --git a/test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java b/test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java new file mode 100644 index 00000000000..047eaff66d4 --- /dev/null +++ b/test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +import java.awt.Insets; +import java.awt.Rectangle; + +import com.sun.java.swing.SwingUtilities3; + +/* + * @test + * @bug 8365379 + * @summary Verify SwingUtilities3 insets return correct result + * independent of initial values + * @modules java.desktop/com.sun.java.swing + * @run main ApplyInsetsTest + */ + +public class ApplyInsetsTest { + public static void main(String[] args) { + Rectangle rect = new Rectangle(10, 20, 60, 60); + Insets insets = new Insets(5, 10, 15, 20); + Rectangle expected = + new Rectangle(rect.x + insets.left, + rect.y + insets.top, + rect.width - (insets.left + insets.right), + rect.height - (insets.top + insets.bottom)); + + SwingUtilities3.applyInsets(rect, insets); + if (!rect.equals(expected)) { + throw new RuntimeException("Test failed: expected " + expected + + " but got " + rect); + } + + // Right to left test + rect.setRect(10, 20, 60, 60); + expected.x = rect.x + insets.right; + SwingUtilities3.applyInsets(rect, insets, false); + if (!rect.equals(expected)) { + throw new RuntimeException("Right to left test failed: expected " + + expected + " but got " + rect); + } + + System.out.println("Test passed."); + } +} diff --git a/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java b/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java index cb90f374d84..7824b030d41 100644 --- a/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java +++ b/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.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 @@ -26,25 +26,25 @@ * @bug 4928019 * @key headful * @summary Makes sure all the basic classes can be created with GTK. - * @author Scott Violet + * @requires (os.family != "windows" & os.family != "mac") + * @library /test/lib + * @build jtreg.SkippedException + * @run main bug4928019 */ import javax.swing.*; import javax.swing.plaf.basic.*; +import jtreg.SkippedException; + public class bug4928019 { public static void main(String[] args) throws Throwable { try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); - } catch (UnsupportedLookAndFeelException ex) { - System.err.println("GTKLookAndFeel is not supported on this platform." + - " Test is considered passed."); - return; - } catch (ClassNotFoundException ex) { - System.err.println("GTKLookAndFeel class is not found." + - " Test is considered passed."); - return; + } catch (Exception e) { + throw new SkippedException("GTKLookAndFeel isn't supported", e); } + new JButton() { public void updateUI() { setUI(new BasicButtonUI()); diff --git a/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java b/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java index ddf8363e2ee..eb3e1e7ca62 100644 --- a/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java +++ b/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2010, 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 @@ -21,18 +21,28 @@ * questions. */ -/* @test - @bug 6635110 - @key headful - @summary GTK icons should not throw NPE when called by non-GTK UI - @author Peter Zhelezniakov - @run main Test6635110 +/* + * @test + * @bug 6635110 + * @key headful + * @summary GTK icons should not throw NPE when called by non-GTK UI + * @requires (os.family != "windows" & os.family != "mac") + * @library /test/lib + * @build jtreg.SkippedException + * @run main Test6635110 */ -import javax.swing.*; -import java.awt.*; +import java.awt.Component; import java.awt.image.BufferedImage; -import javax.swing.plaf.basic.*; + +import javax.swing.JMenu; +import javax.swing.JToolBar; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.plaf.basic.BasicMenuUI; +import javax.swing.plaf.basic.BasicToolBarUI; + +import jtreg.SkippedException; public class Test6635110 implements Runnable { @@ -53,7 +63,7 @@ public class Test6635110 implements Runnable { paint(tb); } - void paint(Component c) { + private void paint(Component c) { c.setSize(WIDTH, HEIGHT); c.paint(IMAGE.getGraphics()); } @@ -62,9 +72,9 @@ public class Test6635110 implements Runnable { try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); } catch (Exception e) { - System.out.println("GTKLookAndFeel cannot be set, skipping this test"); - return; + throw new SkippedException("GTKLookAndFeel isn't supported", e); } + SwingUtilities.invokeAndWait(new Test6635110()); } } diff --git a/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java b/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java index 962a2d1a0af..ea072419a59 100644 --- a/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java +++ b/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.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 @@ -21,23 +21,28 @@ * questions. */ -/* @test - @bug 6963870 - @key headful - @summary Tests that GTKPainter.ListTableFocusBorder.getBorderInsets() - doesn't return null - @author Peter Zhelezniakov - @run main Test6963870 -*/ +/* + * @test + * @bug 6963870 + * @key headful + * @summary Tests that GTKPainter.ListTableFocusBorder.getBorderInsets() + * doesn't return null + * @requires (os.family != "windows" & os.family != "mac") + * @library /test/lib + * @build jtreg.SkippedException + * @run main Test6963870 + */ import java.awt.Insets; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; +import jtreg.SkippedException; + public class Test6963870 implements Runnable { - final static String[] UI_NAMES = { + static final String[] UI_NAMES = { "List.focusCellHighlightBorder", "List.focusSelectedCellHighlightBorder", "List.noFocusBorder", @@ -45,6 +50,7 @@ public class Test6963870 implements Runnable { "Table.focusSelectedCellHighlightBorder", }; + @Override public void run() { for (String uiName: UI_NAMES) { test(uiName); @@ -63,11 +69,9 @@ public class Test6963870 implements Runnable { try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); } catch (Exception e) { - System.out.println("GTKLookAndFeel cannot be set, skipping this test"); - return; + throw new SkippedException("GTKLookAndFeel isn't supported", e); } SwingUtilities.invokeAndWait(new Test6963870()); } } - diff --git a/test/jdk/com/sun/net/httpserver/SANTest.java b/test/jdk/com/sun/net/httpserver/SANTest.java index dc7f5e7bdd5..d35a590931d 100644 --- a/test/jdk/com/sun/net/httpserver/SANTest.java +++ b/test/jdk/com/sun/net/httpserver/SANTest.java @@ -36,6 +36,18 @@ * java.base/sun.net.www.http * java.base/sun.net.www * java.base/sun.net + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.base/jdk.internal.util * * @run main/othervm SANTest * @summary Update SimpleSSLContext keystore to use SANs for localhost IP addresses diff --git a/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java b/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java index d6ec3eb0695..0a46ac1ea8a 100644 --- a/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java +++ b/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java @@ -256,63 +256,65 @@ public class CustomFileSystemTest { @Test public void testNotReadableFileGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

    File not found

    -

    /aFile.txt

    - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = createDirectoryInCustomFs("testNotReadableFileGET"); - var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

    File not found

    +

    /aFile.txt

    + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = createDirectoryInCustomFs("testNotReadableFileGET"); + var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); - file.toFile().setReadable(false, false); - assert !Files.isReadable(file); + file.toFile().setReadable(false, false); + assert !Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - file.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + file.toFile().setReadable(true, false); } } @Test public void testNotReadableSegmentGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

    File not found

    -

    /dir/aFile.txt

    - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = createDirectoryInCustomFs("testNotReadableSegmentGET"); - var dir = Files.createDirectory(root.resolve("dir")); - var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

    File not found

    +

    /dir/aFile.txt

    + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = createDirectoryInCustomFs("testNotReadableSegmentGET"); + var dir = Files.createDirectory(root.resolve("dir")); + var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); - dir.toFile().setReadable(false, false); - assert !Files.isReadable(dir); - assert Files.isReadable(file); + dir.toFile().setReadable(false, false); + assert !Files.isReadable(dir); + assert Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - dir.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + dir.toFile().setReadable(true, false); } } diff --git a/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java b/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java index ea1c73b361b..167fcd3b5d3 100644 --- a/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java +++ b/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java @@ -309,63 +309,65 @@ public class SimpleFileServerTest { @Test public void testNotReadableFileGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

    File not found

    -

    /aFile.txt

    - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableFileGET")); - var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

    File not found

    +

    /aFile.txt

    + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableFileGET")); + var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); - file.toFile().setReadable(false, false); - assert !Files.isReadable(file); + file.toFile().setReadable(false, false); + assert !Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - file.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + file.toFile().setReadable(true, false); } } @Test public void testNotReadableSegmentGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

    File not found

    -

    /dir/aFile.txt

    - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableSegmentGET")); - var dir = Files.createDirectory(root.resolve("dir")); - var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

    File not found

    +

    /dir/aFile.txt

    + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableSegmentGET")); + var dir = Files.createDirectory(root.resolve("dir")); + var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); - dir.toFile().setReadable(false, false); - assert !Files.isReadable(dir); - assert Files.isReadable(file); + dir.toFile().setReadable(false, false); + assert !Files.isReadable(dir); + assert Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - dir.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + dir.toFile().setReadable(true, false); } } @@ -680,18 +682,22 @@ public class SimpleFileServerTest { var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO)); assertTrue(iae.getMessage().contains("does not exist")); } - { // not readable - if (!Platform.isWindows()) { // not applicable on Windows - Path p = Files.createDirectory(TEST_DIR.resolve("aDir")); - p.toFile().setReadable(false, false); - assert !Files.isReadable(p); - try { - var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO)); - assertTrue(iae.getMessage().contains("not readable")); - } finally { - p.toFile().setReadable(true, false); - } - } + } + + @Test + public void testNonReadablePath() throws Exception { + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var addr = LOOPBACK_ADDR; + Path p = Files.createDirectory(TEST_DIR.resolve("aDir")); + p.toFile().setReadable(false, false); + assert !Files.isReadable(p); + try { + var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO)); + assertTrue(iae.getMessage().contains("not readable")); + } finally { + p.toFile().setReadable(true, false); } } diff --git a/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java b/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java index 9edbcf8f71c..602a376655c 100644 --- a/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java +++ b/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java @@ -22,8 +22,10 @@ */ import java.awt.Button; +import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Frame; +import java.awt.Robot; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; @@ -39,27 +41,36 @@ public class InitialFocusTest1 extends Frame implements FocusListener { Button button1 = new Button("Button1"); Button button2 = new Button("Button2"); private static volatile Object focused; + private static InitialFocusTest1 app; public static void main(final String[] args) throws Exception { - InitialFocusTest1 app = new InitialFocusTest1(); try { - app.setSize(200, 200); - app.setLocationRelativeTo(null); - app.setLayout(new FlowLayout()); + Robot robot = new Robot(); + EventQueue.invokeAndWait(() -> { + app = new InitialFocusTest1(); + app.setLayout(new FlowLayout()); - app.button1.addFocusListener(app); - app.button2.addFocusListener(app); - app.add(app.button1); - app.add(app.button2); - app.setVisible(true); - app.button2.requestFocus(); + app.button1.addFocusListener(app); + app.button2.addFocusListener(app); + app.add(app.button1); + app.add(app.button2); + + app.setSize(200, 200); + app.setLocationRelativeTo(null); + app.setVisible(true); + }); + robot.waitForIdle(); + robot.delay(1000); + EventQueue.invokeAndWait(() -> { + app.button2.requestFocus(); + }); // wait for the very very last focus event - Thread.sleep(10000); + robot.delay(1000); if (app.button2 != focused) { throw new RuntimeException("Wrong focus owner: " + focused); } } finally { - app.dispose(); + EventQueue.invokeAndWait(() -> app.dispose()); } } diff --git a/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java b/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java index c7ec6c6d969..aabaf0b511b 100644 --- a/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java +++ b/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java @@ -24,18 +24,22 @@ /* * @test * @key headful - * @summary To check proper WINDOW_EVENTS are triggered when Frame gains or losses the focus - * @library /lib/client - * @build ExtendedRobot + * @summary To check proper WINDOW_EVENTS are triggered when Frame gains + * or loses the focus * @run main ActiveAWTWindowTest */ +import java.awt.AWTException; import java.awt.BorderLayout; import java.awt.Button; import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Frame; +import java.awt.Point; +import java.awt.Robot; import java.awt.TextField; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -43,101 +47,74 @@ import java.awt.event.InputEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.swing.JButton; +import javax.swing.JComponent; public class ActiveAWTWindowTest { - private Frame frame, frame2; - private Button button, button2; - private TextField textField, textField2; - private volatile int eventType; - private final Object lock1 = new Object(); - private final Object lock2 = new Object(); - private final Object lock3 = new Object(); - private boolean passed = true; - private final int delay = 150; + private static Frame frame, frame2; + private static Button button, button2; + private static TextField textField, textField2; + + private static CountDownLatch windowActivatedLatch = new CountDownLatch(1); + private static CountDownLatch windowDeactivatedLatch = new CountDownLatch(1); + private static CountDownLatch windowFocusGainedLatch = new CountDownLatch(1); public static void main(String[] args) throws Exception { - ActiveAWTWindowTest test = new ActiveAWTWindowTest(); - try { - test.doTest(); - } finally { - EventQueue.invokeAndWait(() -> { - if (test.frame != null) { - test.frame.dispose(); - } - if (test.frame2 != null) { - test.frame2.dispose(); - } - }); - } + EventQueue.invokeAndWait(() -> { + initializeGUI(); + }); + doTest(); + EventQueue.invokeAndWait(() -> { + if (frame != null) { + frame.dispose(); + } + if (frame2 != null) { + frame2.dispose(); + } + }); } - public ActiveAWTWindowTest() { - try{ - EventQueue.invokeAndWait( () -> { - initializeGUI(); - }); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("Interrupted or unexpected Exception occured"); - } - } - - private void initializeGUI() { + private static void initializeGUI() { frame = new Frame(); frame.setLayout(new FlowLayout()); - frame.setLocation(5, 20); frame.setSize(200, 200); frame.setUndecorated(true); + frame.addWindowFocusListener(new WindowFocusListener() { + @Override public void windowGainedFocus(WindowEvent event) { System.out.println("Frame Focus gained"); - synchronized (lock3) { - try { - lock3.notifyAll(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + windowFocusGainedLatch.countDown(); } + @Override public void windowLostFocus(WindowEvent event) { - System.out.println("Frame Focus lost"); + System.out.println("Frame Focus lost"); } }); + frame.addWindowListener(new WindowAdapter() { + @Override public void windowActivated(WindowEvent e) { - eventType = WindowEvent.WINDOW_ACTIVATED; - System.out.println("Undecorated Frame is activated\n"); - synchronized (lock1) { - try { - lock1.notifyAll(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + System.out.println("Undecorated Frame is activated"); + windowActivatedLatch.countDown(); } + @Override public void windowDeactivated(WindowEvent e) { - eventType = WindowEvent.WINDOW_DEACTIVATED; - System.out.println("Undecorated Frame got Deactivated\n"); - synchronized (lock2) { - try { - lock2.notifyAll(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + System.out.println("Undecorated Frame got Deactivated"); + windowDeactivatedLatch.countDown(); } }); + textField = new TextField("TextField"); button = new Button("Click me"); - button.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - textField.setText("Focus gained"); - } - }); + button.addActionListener(e -> textField.setText("Focus gained")); frame.setBackground(Color.green); frame.add(button); @@ -149,86 +126,46 @@ public class ActiveAWTWindowTest { frame2.setLocation(5, 250); frame2.setSize(200, 200); frame2.setBackground(Color.green); + button2 = new Button("Click me"); textField2 = new TextField("TextField"); - button2.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - textField2.setText("Got the focus"); - } - }); + button2.addActionListener(e -> textField2.setText("Got the focus")); frame2.add(button2, BorderLayout.SOUTH); frame2.add(textField2, BorderLayout.NORTH); frame2.setVisible(true); - - frame.toFront(); } - public void doTest() { - ExtendedRobot robot; - try { - robot = new ExtendedRobot(); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("Cannot create robot"); - } - - robot.setAutoDelay(delay); + private static void doTest() throws AWTException, InterruptedException { + Robot robot = new Robot(); + robot.setAutoDelay(150); robot.setAutoWaitForIdle(true); - - robot.waitForIdle(5*delay); - robot.mouseMove(button.getLocationOnScreen().x + button.getSize().width / 2, - button.getLocationOnScreen().y + button.getSize().height / 2); - robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); - robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); - - if (eventType != WindowEvent.WINDOW_ACTIVATED) { - synchronized (lock1) { - try { - lock1.wait(delay * 10); - } catch (Exception e) { - e.printStackTrace(); - } - } + if (!windowFocusGainedLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Frame did not gain focus"); } - if (eventType != WindowEvent.WINDOW_ACTIVATED) { - passed = false; - System.err.println("WINDOW_ACTIVATED event did not occur when the " + - "undecorated frame is activated!"); + clickButtonCenter(robot, button); + + if (!windowActivatedLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Frame was not activated"); } + clickButtonCenter(robot, button2); - eventType = -1; - - robot.mouseMove(button2.getLocationOnScreen().x + button2.getSize().width / 2, - button2.getLocationOnScreen().y + button2.getSize().height / 2); - robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); - robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); - - if (eventType != WindowEvent.WINDOW_DEACTIVATED) { - synchronized (lock2) { - try { - lock2.wait(delay * 10); - } catch (Exception e) { - } - } - } - if (eventType != WindowEvent.WINDOW_DEACTIVATED) { - passed = false; - System.err.println("FAIL: WINDOW_DEACTIVATED event did not occur for the " + - "undecorated frame when another frame gains focus!"); + if (!windowDeactivatedLatch.await(2000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Frame was not deactivated"); } if (frame.hasFocus()) { - passed = false; - System.err.println("FAIL: The undecorated frame has focus even when " + - "another frame is clicked!"); - } - - if (!passed) { - //captureScreenAndSave(); - System.err.println("Test failed!"); - throw new RuntimeException("Test failed."); - } else { - System.out.println("Test passed"); + throw new RuntimeException("Frame did not lose focus"); } } + + private static void clickButtonCenter(Robot robot, Component button) { + Point location = button.getLocationOnScreen(); + Dimension size = button.getSize(); + int x = location.x + size.width / 2; + int y = location.y + size.height / 2; + robot.mouseMove(x, y); + robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); + robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); + } } + diff --git a/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java b/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java index c1cc07afac0..e96485dcb54 100644 --- a/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java +++ b/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.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 @@ -27,7 +27,6 @@ import java.awt.Frame; import java.awt.TextField; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.lang.reflect.InvocationTargetException; /* * @test @@ -54,16 +53,17 @@ public class SetEchoCharTest4 extends Frame implements ActionListener { Make sure the actual text matches what you typed in for each field. Press Pass if everything's ok, otherwise Fail - """; + """; public SetEchoCharTest4() { + super("SetEchoCharTest4"); setLayout(new FlowLayout()); tf1.setEchoChar('*'); tf2.setEchoChar('$'); tf3.setEchoChar('#'); addStuff(); b.addActionListener(this); - setSize (200,200); + setSize (300, 150); } private void addStuff() { @@ -78,7 +78,6 @@ public class SetEchoCharTest4 extends Frame implements ActionListener { PassFailJFrame.log("TextField2 = " + tf2.getText()); PassFailJFrame.log("TextField3 = " + tf3.getText()); PassFailJFrame.log("--------------"); - setVisible(false); remove(tf1); remove(tf2); remove(tf3); @@ -86,16 +85,14 @@ public class SetEchoCharTest4 extends Frame implements ActionListener { addStuff(); } - public static void main(String[] args) throws InterruptedException, - InvocationTargetException { + public static void main(String[] args) throws Exception { PassFailJFrame.builder() - .title("Set Echo Character Test") - .testUI(SetEchoCharTest4::new) - .instructions(INSTRUCTIONS) - .rows((int) INSTRUCTIONS.lines().count() + 1) - .columns(40) - .logArea() - .build() - .awaitAndCheck(); + .title("Set Echo Character Test") + .testUI(SetEchoCharTest4::new) + .instructions(INSTRUCTIONS) + .columns(40) + .logArea() + .build() + .awaitAndCheck(); } } diff --git a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java b/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java index e2ded58e7f2..4b4041dbcfd 100644 --- a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java +++ b/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.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 @@ -22,55 +22,52 @@ */ /* - * test + * @test * @bug 6282388 - * @summary Tests that AWT use correct toolkit to be wrapped into HeadlessToolkit - * @author artem.ananiev@sun.com: area=awt.headless - * @run shell WrappedToolkitTest.sh + * @summary Tests that AWT uses correct toolkit wrapped into HeadlessToolkit + * @modules java.desktop/sun.awt:open + * @library /test/lib + * @run main/othervm -Djava.awt.headless=true TestWrapped */ -import java.awt.*; +import java.awt.Toolkit; +import java.lang.Class; +import java.lang.reflect.Field; -import java.lang.reflect.*; +import jdk.test.lib.Platform; -import sun.awt.*; +public final class TestWrapped { -public class TestWrapped -{ - public static void main(String[] args) - { - try - { - if (args.length != 1) { - System.err.println("No correct toolkit class name is specified, test is not run"); - System.exit(0); + private static final String HEADLESS_TOOLKIT = "sun.awt.HeadlessToolkit"; + private static final String MACOSX_TOOLKIT = "sun.lwawt.macosx.LWCToolkit"; + private static final String UNIX_TOOLKIT = "sun.awt.X11.XToolkit"; + private static final String WINDOWS_TOOLKIT = "sun.awt.windows.WToolkit"; + + public static void main(String[] args) throws Exception { + String expectedToolkitClassName; + if (Platform.isWindows()) { + expectedToolkitClassName = WINDOWS_TOOLKIT; + } else if (Platform.isOSX()) { + expectedToolkitClassName = MACOSX_TOOLKIT; + } else { + expectedToolkitClassName = UNIX_TOOLKIT; } - String correctToolkitClassName = args[0]; Toolkit tk = Toolkit.getDefaultToolkit(); - Class tkClass = tk.getClass(); - if (!tkClass.getName().equals("sun.awt.HeadlessToolkit")) - { - System.err.println(tkClass.getName()); - System.err.println("Error: default toolkit is not an instance of HeadlessToolkit"); - System.exit(-1); + Class tkClass = tk.getClass(); + if (!tkClass.getName().equals(HEADLESS_TOOLKIT)) { + System.err.println("Expected: " + HEADLESS_TOOLKIT); + System.err.println("Actual: " + tkClass.getName()); + throw new RuntimeException("Wrong default toolkit"); } Field f = tkClass.getDeclaredField("tk"); f.setAccessible(true); - Class wrappedClass = f.get(tk).getClass(); - if (!wrappedClass.getName().equals(correctToolkitClassName)) { - System.err.println(wrappedClass.getName()); - System.err.println("Error: wrapped toolkit is not an instance of " + correctToolkitClassName); - System.exit(-1); + Class wrappedClass = f.get(tk).getClass(); + if (!wrappedClass.getName().equals(expectedToolkitClassName)) { + System.err.println("Expected: " + expectedToolkitClassName); + System.err.println("Actual: " + wrappedClass.getName()); + throw new RuntimeException("Wrong wrapped toolkit"); } - } - catch (Exception z) - { - z.printStackTrace(System.err); - System.exit(-1); - } - - System.exit(0); } } diff --git a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh b/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh deleted file mode 100644 index ac0cf58942c..00000000000 --- a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/ksh -p - -# -# Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved. -# 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 6282388 8030640 -# @summary Tests that AWT use correct toolkit to be wrapped into HeadlessToolkit -# @author artem.ananiev@sun.com: area=awt.headless -# compile TestWrapped.java -# @run shell WrappedToolkitTest.sh - -# Beginning of subroutines: -status=1 - -#Call this from anywhere to fail the test with an error message -# usage: fail "reason why the test failed" -fail() - { echo "The test failed :-(" - echo "$*" 1>&2 - echo "exit status was $status" - exit $status - } #end of fail() - -#Call this from anywhere to pass the test with a message -# usage: pass "reason why the test passed if applicable" -pass() - { echo "The test passed!!!" - echo "$*" 1>&2 - exit 0 - } #end of pass() - -# end of subroutines - - -# The beginning of the script proper - -# Checking for proper OS -OS=`uname -s` -case "$OS" in - AIX | CYGWIN* | Darwin | Linux ) - FILESEP="/" - ;; - - Windows* ) - FILESEP="\\" - ;; - - # catch all other OSs - * ) - echo "Unrecognized system! $OS" - fail "Unrecognized system! $OS" - ;; -esac - -# check that some executable or other file you need is available, abort if not -# note that the name of the executable is in the fail string as well. -# this is how to check for presence of the compiler, etc. -#RESOURCE=`whence SomeProgramOrFileNeeded` -#if [ "${RESOURCE}" = "" ] ; -# then fail "Need SomeProgramOrFileNeeded to perform the test" ; -#fi - -# Want this test to run standalone as well as in the harness, so do the -# following to copy the test's directory into the harness's scratch directory -# and set all appropriate variables: - -if [ -z "${TESTJAVA}" ] ; then - # TESTJAVA is not set, so the test is running stand-alone. - # TESTJAVA holds the path to the root directory of the build of the JDK - # to be tested. That is, any java files run explicitly in this shell - # should use TESTJAVA in the path to the java interpreter. - # So, we'll set this to the JDK spec'd on the command line. If none - # is given on the command line, tell the user that and use a cheesy - # default. - # THIS IS THE JDK BEING TESTED. - if [ -n "$1" ] ; - then TESTJAVA=$1 - else fail "no JDK specified on command line!" - fi - TESTSRC=. - TESTCLASSES=. - STANDALONE=1; -fi -echo "JDK under test is: $TESTJAVA" - -#if in test harness, then copy the entire directory that the test is in over -# to the scratch directory. This catches any support files needed by the test. -if [ -z "${STANDALONE}" ] ; - then cp ${TESTSRC}/* . -fi -case "$OS" in - Windows* | CYGWIN* ) - ${COMPILEJAVA}/bin/javac ${TESTJAVACOPTS} \ - --add-exports java.desktop/sun.awt=ALL-UNNAMED \ - --add-exports java.desktop/sun.awt.windows=ALL-UNNAMED ${CP} \ - *.java - status=$? - if [ ! $status -eq "0" ]; then - fail "Compilation failed"; - fi - ;; - - AIX | Linux ) - ${COMPILEJAVA}/bin/javac ${TESTJAVACOPTS} \ - --add-exports java.desktop/sun.awt=ALL-UNNAMED \ - --add-exports java.desktop/sun.awt.X11=ALL-UNNAMED ${CP} \ - *.java - status=$? - if [ ! $status -eq "0" ]; then - fail "Compilation failed"; - fi - ;; - - Darwin) - ${COMPILEJAVA}/bin/javac ${TESTJAVACOPTS} \ - --add-exports java.desktop/sun.awt=ALL-UNNAMED \ - --add-exports java.desktop/sun.lwawt.macosx=ALL-UNNAMED ${CP} \ - *.java - status=$? - if [ ! $status -eq "0" ]; then - fail "Compilation failed"; - fi - ;; - -esac - -#Just before executing anything, make sure it has executable permission! -chmod 777 ./* - -############### YOUR TEST CODE HERE!!!!!!! ############# - -case "$OS" in - Windows* | CYGWIN* ) - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.windows=ALL-UNNAMED ${CP} \ - TestWrapped sun.awt.windows.WToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.windows.WToolkit"; - fi - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.windows=ALL-UNNAMED ${CP} \ - -Dawt.toolkit=sun.awt.windows.WToolkit \ - TestWrapped sun.awt.windows.WToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.windows.WToolkit"; - fi - ;; - - AIX | Linux ) - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.X11=ALL-UNNAMED ${CP} \ - -Dawt.toolkit=sun.awt.X11.XToolkit \ - TestWrapped sun.awt.X11.XToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.xawt.XToolkit"; - fi - AWT_TOOLKIT=XToolkit ${TESTJAVA}/bin/java ${TESTVMOPTS} \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.X11=ALL-UNNAMED ${CP} \ - -Djava.awt.headless=true \ - TestWrapped sun.awt.X11.XToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.xawt.XToolkit"; - fi - ;; - - Darwin) - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED ${CP} \ - TestWrapped sun.lwawt.macosx.LWCToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.lwawt.macosx.LWCToolkit"; - fi - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED ${CP} \ - -Dawt.toolkit=sun.lwawt.macosx.LWCToolkit \ - TestWrapped sun.lwawt.macosx.LWCToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.lwawt.macosx.LWCToolkit"; - fi - ;; - -esac - -pass "All the tests are PASSED"; - -#For additional examples of how to write platform independent KSH scripts, -# see the jtreg file itself. It is a KSH script for both Solaris and Win32 diff --git a/test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java b/test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java new file mode 100644 index 00000000000..31117e495bc --- /dev/null +++ b/test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java @@ -0,0 +1,316 @@ +/* + * 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 4138921 + * @summary Confirm constructor behavior for various edge cases. + */ + +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.font.TextHitInfo; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class TextLayoutConstructorTest { + + public static void main(String[] args) throws Exception { + testFontConstructor(); + testMapConstructor(); + testIteratorConstructor(); + } + + private static void testFontConstructor() { + + // new TextLayout(String, Font, FontRenderContext) + + Font font = new Font(Font.DIALOG, Font.PLAIN, 20); + FontRenderContext frc = new FontRenderContext(null, true, true); + + assertThrows(() -> new TextLayout(null, font, frc), + IllegalArgumentException.class, + "Null string passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", (Font) null, frc), + IllegalArgumentException.class, + "Null font passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", font, null), + IllegalArgumentException.class, + "Null font render context passed to TextLayout constructor."); + + Function< String, TextLayout > creator = (s) -> new TextLayout(s, font, frc); + assertEmptyTextLayoutBehavior(creator); + } + + private static void testMapConstructor() { + + // new TextLayout(String, Map, FontRenderContext) + + Map< TextAttribute, Object > attributes = Map.of(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); + FontRenderContext frc = new FontRenderContext(null, true, true); + + assertThrows(() -> new TextLayout(null, attributes, frc), + IllegalArgumentException.class, + "Null string passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", (Map) null, frc), + IllegalArgumentException.class, + "Null map passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", attributes, null), + IllegalArgumentException.class, + "Null font render context passed to TextLayout constructor."); + + Function< String, TextLayout > creator = (s) -> new TextLayout(s, attributes, frc); + assertEmptyTextLayoutBehavior(creator); + } + + private static void testIteratorConstructor() { + + // new TextLayout(AttributedCharacterIterator, FontRenderContext) + + Map< TextAttribute, Object > attributes = Map.of(); + FontRenderContext frc = new FontRenderContext(null, true, true); + + assertThrows(() -> new TextLayout(null, frc), + IllegalArgumentException.class, + "Null iterator passed to TextLayout constructor."); + + AttributedCharacterIterator it1 = new AttributedString("test", attributes).getIterator(); + assertThrows(() -> new TextLayout(it1, null), + IllegalArgumentException.class, + "Null font render context passed to TextLayout constructor."); + + Function< String, TextLayout > creator = (s) -> { + AttributedCharacterIterator it2 = new AttributedString(s, attributes).getIterator(); + return new TextLayout(it2, frc); + }; + assertEmptyTextLayoutBehavior(creator); + } + + private static void assertEmptyTextLayoutBehavior(Function< String, TextLayout > creator) { + + TextLayout tl = creator.apply(""); + TextLayout ref = creator.apply(" "); // space + FontRenderContext frc = new FontRenderContext(null, true, true); + Rectangle zero = new Rectangle(0, 0, 0, 0); + Rectangle2D.Float zero2D = new Rectangle2D.Float(0, 0, 0, 0); + Rectangle2D.Float oneTwo = new Rectangle2D.Float(1, 2, 0, 0); + Rectangle2D.Float kilo = new Rectangle2D.Float(0, 0, 1000, 1000); + AffineTransform identity = new AffineTransform(); + TextLayout.CaretPolicy policy = new TextLayout.CaretPolicy(); + TextHitInfo start = TextHitInfo.trailing(-1); + TextHitInfo end = TextHitInfo.leading(0); + + assertEqual(0, tl.getJustifiedLayout(100).getAdvance(), "justified advance"); + assertEqual(0, tl.getBaseline(), "baseline"); + + float[] offsets = tl.getBaselineOffsets(); + float[] refOffsets = ref.getBaselineOffsets(); + assertEqual(3, offsets.length, "baseline offsets"); + assertEqual(refOffsets[0], offsets[0], "baseline offset 1"); + assertEqual(refOffsets[1], offsets[1], "baseline offset 2"); + assertEqual(refOffsets[2], offsets[2], "baseline offset 3"); + + assertEqual(0, tl.getAdvance(), "advance"); + assertEqual(0, tl.getVisibleAdvance(), "visible advance"); + assertEqual(ref.getAscent(), tl.getAscent(), "ascent"); + assertEqual(ref.getDescent(), tl.getDescent(), "descent"); + assertEqual(ref.getLeading(), tl.getLeading(), "leading"); + assertEqual(zero2D, tl.getBounds(), "bounds"); + assertEqual(zero2D, tl.getPixelBounds(frc, 0, 0), "pixel bounds 1"); + assertEqual(oneTwo, tl.getPixelBounds(frc, 1, 2), "pixel bounds 2"); + assertEqual(true, tl.isLeftToRight(), "left to right"); + assertEqual(false, tl.isVertical(), "is vertical"); + assertEqual(0, tl.getCharacterCount(), "character count"); + + float[] caretInfo = tl.getCaretInfo(start, kilo); + float[] refCaretInfo = ref.getCaretInfo(start, kilo); + assertEqual(6, caretInfo.length, "caret info length 1"); + assertEqual(refCaretInfo[0], caretInfo[0], "first caret info 1"); + assertEqual(refCaretInfo[1], caretInfo[1], "second caret info 1"); + assertEqual(refCaretInfo[2], caretInfo[2], "third caret info 1"); + assertEqual(refCaretInfo[3], caretInfo[3], "fourth caret info 1"); + assertEqual(refCaretInfo[4], caretInfo[4], "fifth caret info 1"); + assertEqual(refCaretInfo[5], caretInfo[5], "sixth caret info 1"); + + float[] caretInfo2 = tl.getCaretInfo(start); + float[] refCaretInfo2 = ref.getCaretInfo(start); + assertEqual(6, caretInfo2.length, "caret info length 2"); + assertEqual(refCaretInfo2[0], caretInfo2[0], "first caret info 2"); + assertEqual(refCaretInfo2[1], caretInfo2[1], "second caret info 2"); + assertEqual(refCaretInfo2[2], caretInfo2[2], "third caret info 2"); + assertEqual(refCaretInfo2[3], caretInfo2[3], "fourth caret info 2"); + assertEqual(refCaretInfo2[4], caretInfo2[4], "fifth caret info 2"); + assertEqual(refCaretInfo2[5], caretInfo2[5], "sixth caret info 2"); + + assertEqual(null, tl.getNextRightHit(start), "next right hit 1"); + assertEqual(null, tl.getNextRightHit(end), "next right hit 2"); + assertEqual(null, tl.getNextRightHit(0, policy), "next right hit 3"); + assertEqual(null, tl.getNextRightHit(0), "next right hit 4"); + assertEqual(null, tl.getNextLeftHit(start), "next left hit 1"); + assertEqual(null, tl.getNextLeftHit(end), "next left hit 2"); + assertEqual(null, tl.getNextLeftHit(0, policy), "next left hit 3"); + assertEqual(null, tl.getNextLeftHit(0), "next left hit 4"); + assertEqual(end, tl.getVisualOtherHit(start), "visual other hit"); + + Shape caretShape = tl.getCaretShape(start, kilo); + Shape refCaretShape = ref.getCaretShape(start, kilo); + assertEqual(refCaretShape.getBounds(), caretShape.getBounds(), "caret shape 1"); + + Shape caretShape2 = tl.getCaretShape(start); + Shape refCaretShape2 = ref.getCaretShape(start); + assertEqual(refCaretShape2.getBounds(), caretShape2.getBounds(), "caret shape 2"); + + assertEqual(0, tl.getCharacterLevel(0), "character level"); + + Shape[] caretShapes = tl.getCaretShapes(0, kilo, policy); + Shape[] refCaretShapes = ref.getCaretShapes(0, kilo, policy); + assertEqual(2, caretShapes.length, "caret shapes length 1"); + assertEqual(refCaretShapes[0].getBounds(), caretShapes[0].getBounds(), "caret shapes strong 1"); + assertEqual(refCaretShapes[1], caretShapes[1], "caret shapes weak 1"); + assertEqual(null, caretShapes[1], "caret shapes weak 1"); + + Shape[] caretShapes2 = tl.getCaretShapes(0, kilo); + Shape[] refCaretShapes2 = ref.getCaretShapes(0, kilo); + assertEqual(2, caretShapes2.length, "caret shapes length 2"); + assertEqual(refCaretShapes2[0].getBounds(), caretShapes2[0].getBounds(), "caret shapes strong 2"); + assertEqual(refCaretShapes2[1], caretShapes2[1], "caret shapes weak 2"); + assertEqual(null, caretShapes2[1], "caret shapes weak 2"); + + Shape[] caretShapes3 = tl.getCaretShapes(0); + Shape[] refCaretShapes3 = ref.getCaretShapes(0); + assertEqual(2, caretShapes3.length, "caret shapes length 3"); + assertEqual(refCaretShapes3[0].getBounds(), caretShapes3[0].getBounds(), "caret shapes strong 3"); + assertEqual(refCaretShapes3[1], caretShapes3[1], "caret shapes weak 3"); + assertEqual(null, caretShapes3[1], "caret shapes weak 3"); + + assertEqual(0, tl.getLogicalRangesForVisualSelection(start, start).length, "logical ranges for visual selection"); + assertEqual(zero2D, tl.getVisualHighlightShape(start, start, kilo).getBounds(), "visual highlight shape 1"); + assertEqual(zero2D, tl.getVisualHighlightShape(start, start).getBounds(), "visual highlight shape 2"); + assertEqual(zero, tl.getLogicalHighlightShape(0, 0, kilo).getBounds(), "logical highlight shape 1"); + assertEqual(zero, tl.getLogicalHighlightShape(0, 0).getBounds(), "logical highlight shape 2"); + assertEqual(zero, tl.getBlackBoxBounds(0, 0).getBounds(), "black box bounds"); + + TextHitInfo hit = tl.hitTestChar(0, 0); + assertEqual(-1, hit.getCharIndex(), "hit test char index 1"); + assertEqual(false, hit.isLeadingEdge(), "hit test leading edge 1"); + + TextHitInfo hit2 = tl.hitTestChar(0, 0, kilo); + assertEqual(-1, hit2.getCharIndex(), "hit test char index 2"); + assertEqual(false, hit2.isLeadingEdge(), "hit test leading edge 2"); + + assertEqual(false, tl.equals(creator.apply("")), "equals"); + assertEqual(false, tl.toString().isEmpty(), "to string"); + assertDoesNotDraw(tl); + assertEqual(zero2D, tl.getOutline(identity).getBounds(), "outline"); + assertEqual(null, tl.getLayoutPath(), "layout path"); + + Point2D.Float point = new Point2D.Float(7, 7); + tl.hitToPoint(start, point); + assertEqual(0, point.x, "hit to point x"); + assertEqual(0, point.y, "hit to point y"); + } + + private static void assertEqual(int expected, int actual, String name) { + if (expected != actual) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertEqual(float expected, float actual, String name) { + if (expected != actual) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertEqual(boolean expected, boolean actual, String name) { + if (expected != actual) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertEqual(Object expected, Object actual, String name) { + if (!Objects.equals(expected, actual)) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertThrows(Runnable r, Class< ? > type, String message) { + Class< ? > actualType; + String actualMessage; + Exception actualException; + try { + r.run(); + actualType = null; + actualMessage = null; + actualException = null; + } catch (Exception e) { + actualType = e.getClass(); + actualMessage = e.getMessage(); + actualException = e; + } + if (!Objects.equals(type, actualType)) { + throw new RuntimeException(type + " != " + actualType, actualException); + } + if (!Objects.equals(message, actualMessage)) { + throw new RuntimeException(message + " != " + actualMessage, actualException); + } + } + + private static void assertDoesNotDraw(TextLayout layout) { + + int w = 200; + int h = 200; + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g2d = image.createGraphics(); + int expected = image.getRGB(0, 0); + + layout.draw(g2d, w / 2f, h / 2f); // should not actually draw anything + + int[] rowPixels = new int[w]; + for (int y = 0; y < h; y++) { + image.getRGB(0, y, w, 1, rowPixels, 0, w); + for (int x = 0; x < w; x++) { + if (rowPixels[x] != expected) { + throw new RuntimeException( + "pixel (" + x + ", " + y +"): " + expected + " != " + rowPixels[x]); + } + } + } + } +} diff --git a/test/jdk/java/net/InetAddress/ptr/Lookup.java b/test/jdk/java/net/InetAddress/ptr/Lookup.java index 1248916023e..39e5f720cef 100644 --- a/test/jdk/java/net/InetAddress/ptr/Lookup.java +++ b/test/jdk/java/net/InetAddress/ptr/Lookup.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 @@ -46,6 +46,7 @@ import java.util.stream.Collectors; import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; +import jtreg.SkippedException; public class Lookup { private static final String HOST = "icann.org"; @@ -91,8 +92,7 @@ public class Lookup { String tmp = lookupWithIPv4Prefer(); System.out.println("IPv4 lookup results: [" + tmp + "]"); if (SKIP.equals(tmp)) { - System.out.println(HOST + " can't be resolved - test skipped."); - return; + throw new SkippedException(HOST + " can't be resolved - test skipped."); } String[] strs = tmp.split(":"); @@ -104,8 +104,7 @@ public class Lookup { tmp = reverseWithIPv4Prefer(addr); System.out.println("IPv4 reverse lookup results: [" + tmp + "]"); if (SKIP.equals(tmp)) { - System.out.println(addr + " can't be resolved with preferIPv4 - test skipped."); - return; + throw new SkippedException(addr + " can't be resolved with preferIPv4 - test skipped."); } strs = tmp.split(":"); diff --git a/test/jdk/java/net/httpclient/AbstractNoBody.java b/test/jdk/java/net/httpclient/AbstractNoBody.java index 603fe16d216..9cc26704bb5 100644 --- a/test/jdk/java/net/httpclient/AbstractNoBody.java +++ b/test/jdk/java/net/httpclient/AbstractNoBody.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, 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 @@ -29,9 +29,6 @@ import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.net.http.HttpClient; @@ -39,6 +36,7 @@ import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -49,6 +47,8 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; public abstract class AbstractNoBody implements HttpServerAdapters { @@ -58,6 +58,7 @@ public abstract class AbstractNoBody implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -66,12 +67,16 @@ public abstract class AbstractNoBody implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final String SIMPLE_STRING = "Hello world. Goodbye world"; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test static final ExecutorService executor = Executors.newFixedThreadPool(ITERATION_COUNT * 2); static final ExecutorService serverExecutor = Executors.newFixedThreadPool(ITERATION_COUNT * 4); + static final AtomicLong serverCount = new AtomicLong(); static final AtomicLong clientCount = new AtomicLong(); static final long start = System.nanoTime(); public static String now() { @@ -85,6 +90,11 @@ public abstract class AbstractNoBody implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ + { http3URI_fixed, false,}, + { http3URI_chunk, false }, + { http3URI_fixed, true,}, + { http3URI_chunk, true }, + { httpURI_fixed, false }, { httpURI_chunk, false }, { httpsURI_fixed, false }, @@ -112,17 +122,39 @@ public abstract class AbstractNoBody implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } HttpRequest.Builder newRequestBuilder(String uri) { var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } return builder; } + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() .executor(executor) .proxy(NO_PROXY) .sslContext(sslContext) @@ -190,10 +222,22 @@ public abstract class AbstractNoBody implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/noBodyFixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/noBodyChunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new FixedLengthNoBodyHandler(); + HttpTestHandler h3_chunkedHandler = new ChunkedNoBodyHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/noBodyFixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/noBodyChunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/noBodyHead"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/noBodyFixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/noBodyChunk"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/noBodyHead"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); var shared = newHttpClient(true); @@ -201,9 +245,13 @@ public abstract class AbstractNoBody implements HttpServerAdapters { out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); - + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); out.println("Shared client is: " + shared); + headRequest(shared); + printStamp(END,"setup"); } @@ -215,6 +263,7 @@ public abstract class AbstractNoBody implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); executor.close(); serverExecutor.close(); printStamp(END, "teardown"); @@ -270,26 +319,4 @@ public abstract class AbstractNoBody implements HttpServerAdapters { } } } - - /* - * Converts a ByteBuffer containing bytes encoded using - * the given charset into a string. - * This method does not throw but will replace - * unrecognized sequences with the replacement character. - */ - 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. - */ - public static String asString(ByteBuffer buffer) { - return asString(buffer, StandardCharsets.UTF_8); - } } diff --git a/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java b/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java index 859169dcaae..9a9c2b44cd7 100644 --- a/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java +++ b/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java @@ -21,6 +21,7 @@ * questions. */ +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -30,7 +31,6 @@ import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; import javax.net.ssl.SSLContext; import java.io.IOException; @@ -39,6 +39,7 @@ import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; @@ -70,9 +71,12 @@ import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.String.format; +import static java.lang.System.err; import static java.lang.System.out; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -84,6 +88,7 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -92,6 +97,9 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final int ITERATION_COUNT = 1; // a shared executor helps reduce the amount of threads created by the test @@ -151,6 +159,41 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { + (params == null ? "()" : Arrays.toString(result.getParameters())); } + static Version version(String uri) { + if (uri.contains("/http1/") || uri.contains("/https1/")) + return HTTP_1_1; + if (uri.contains("/http2/") || uri.contains("/https2/")) + return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + System.out.println("\n" + now() + "--- Sending HEAD request ----\n"); + System.err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + System.out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + System.err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + @BeforeMethod void beforeMethod(ITestContext context) { if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { @@ -189,6 +232,8 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, httpURI_fixed, httpURI_chunk, httpsURI_fixed, @@ -326,7 +371,7 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -354,8 +399,12 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { HttpClient client = null; out.printf("%n%s testSanity(%s, %b)%n", now(), uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } SubmissionPublisher publisher = new SubmissionPublisher<>(executor,10); @@ -374,7 +423,7 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { }, executor); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .POST(bodyPublisher) .build(); BodyHandler handler = BodyHandlers.ofString(); @@ -445,18 +494,21 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { for (Where where : whereValues) { //if (where == Where.ON_SUBSCRIBE) continue; //if (where == Where.ON_ERROR) continue; - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } ThrowingBodyPublisher bodyPublisher = new ThrowingBodyPublisher(where.select(thrower), publishers.get()); - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .header("X-expect-exception", "true") .POST(bodyPublisher) .build(); BodyHandler handler = BodyHandlers.ofString(); - System.out.println("try throwing in " + where); + System.out.println(now() + " try throwing in " + where); HttpResponse response = null; if (async) { try { @@ -564,8 +616,12 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { static final class UncheckedCustomExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedCustomException(where.name()); + var thread = Thread.currentThread().getName(); + var thrown = new UncheckedCustomException("[" + thread + "] " + where.name()); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + thrown.printStackTrace(); + throw thrown; } @Override @@ -591,8 +647,13 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { static final class UncheckedIOExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedIOException(new CustomIOException(where.name())); + var thread = Thread.currentThread().getName(); + var cause = new CustomIOException("[" + thread + "] " + where.name()); + var thrown = new UncheckedIOException(cause); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + cause.printStackTrace(); + throw thrown; } @Override @@ -719,6 +780,9 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { @BeforeTest public void setup() throws Exception { + System.out.println(now() + "setup"); + System.err.println(now() + "setup"); + sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); @@ -732,12 +796,16 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x"; httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x"; + System.out.println(now() + "HTTP/1.1 server created"); + httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed"); httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk"); httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed/x"; httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk/x"; + System.out.println(now() + "TLS HTTP/1.1 server created"); + // HTTP/2 HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler(); @@ -748,31 +816,67 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x"; + System.out.println(now() + "HTTP/2 server created"); + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(4); + System.out.println(now() + "TLS HTTP/2 server created"); + + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + + System.out.println(now() + "HTTP/3 server created"); + System.err.println(now() + "Starting servers"); + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + + out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); + out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); + out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); + out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); + + System.out.println(now() + "setup done"); + System.err.println(now() + "setup done"); } @AfterTest public void teardown() throws Exception { + System.out.println(now() + "teardown"); + System.err.println(now() + "teardown"); + String sharedClientName = sharedClient == null ? null : sharedClient.toString(); sharedClient = null; Thread.sleep(100); - AssertionError fail = TRACKER.check(500); + AssertionError fail = TRACKER.check(10000); try { httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { @@ -781,6 +885,8 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { throw fail; } } + System.out.println(now() + "teardown done"); + System.err.println(now() + "teardown done"); } static class HTTP_FixedLengthHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java b/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java index b5230d48d10..a7aea89c379 100644 --- a/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java +++ b/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -50,6 +50,7 @@ import org.testng.annotations.DataProvider; import javax.net.ssl.SSLContext; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -58,8 +59,10 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -88,12 +91,14 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.lang.System.err; import static java.lang.String.format; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -103,10 +108,13 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters SSLContext sslContext; HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http2URI_fixed; String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 1; // a shared executor helps reduce the amount of threads created by the test @@ -205,6 +213,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, http2URI_fixed, http2URI_chunk, https2URI_fixed, @@ -282,7 +292,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() + .version(HTTP_3) .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -302,6 +313,22 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters } } + Http3DiscoveryMode config(String uri) { + return uri.contains("/http3/") ? HTTP_3_URI_ONLY : null; + } + + Version version(String uri) { + return uri.contains("/http3/") ? HTTP_3 : HTTP_2; + } + + HttpRequest request(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)) + .version(version(uri)); + var config = config(uri); + if (config != null) builder.setOption(H3_DISCOVERY, config); + return builder.build(); + } + // @Test(dataProvider = "sanity") protected void testSanityImpl(String uri, boolean sameClient) throws Exception { @@ -311,8 +338,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters if (!sameClient || client == null) client = newHttpClient(sameClient); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) - .build(); + HttpRequest req = request(uri); + BodyHandler> handler = new ThrowingBodyHandler((w) -> {}, BodyHandlers.ofLines()); @@ -339,7 +366,7 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters .collect(Collectors.joining("|")); assertEquals(promisedBody, promised.uri().toASCIIString()); } - assertEquals(3, pushPromises.size()); + assertEquals(pushPromises.size(), 3); if (!sameClient) { // Wait for the client to be garbage collected. // we use the ReferenceTracker API rather than HttpClient::close here, @@ -423,9 +450,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters if (!sameClient || client == null) client = newHttpClient(sameClient); - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) - .build(); + HttpRequest req = request(uri); + ConcurrentMap>> promiseMap = new ConcurrentHashMap<>(); Supplier> throwing = () -> @@ -739,24 +765,31 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters throw new AssertionError("Unexpected null sslContext"); // HTTP/2 - HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); - HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler(); + HttpTestHandler fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler chunkedHandler = new HTTP_ChunkedHandler(); http2TestServer = HttpTestServer.create(HTTP_2); - http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed"); - http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk"); + http2TestServer.addHandler(fixedLengthHandler, "/http2/fixed"); + http2TestServer.addHandler(chunkedHandler, "/http2/chunk"); http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x"; https2TestServer = HttpTestServer.create(HTTP_2, sslContext); - https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); - https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); + https2TestServer.addHandler(fixedLengthHandler, "/https2/fixed"); + https2TestServer.addHandler(chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(2); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + + serverCount.addAndGet(3); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -769,6 +802,7 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters try { http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { @@ -794,15 +828,69 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); out.printf("TestServer: %s Pushing promise: %s%n", now(), promise); err.printf("TestServer: %s Pushing promise: %s%n", now(), promise); - HttpHeaders headers; + HttpHeaders reqHaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; if (fixed) { String length = String.valueOf(promiseBytes.length); - headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), ACCEPT_ALL); } else { - headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty } - t.serverPush(promise, headers, promiseBytes); + t.serverPush(promise, reqHaders, rspHeaders, promiseBytes); + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static long sendHttp3PushPromiseFrame(HttpTestExchange t, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending PushPromiseFrame: %s%n", now(), promise); + err.printf("TestServer: %s Pushing PushPromiseFrame: %s%n", now(), promise); + // headers are added to the request headers sent in the push promise + HttpHeaders headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + long pushId = t.sendHttp3PushPromiseFrame(-1, promise, headers); + out.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + err.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + return pushId; + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static void sendHttp3PushResponse(HttpTestExchange t, + long pushId, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending push response pushId=%s: %s%n", now(), pushId, promise); + err.printf("TestServer: %s Pushing push response pushId=%s: %s%n", now(), pushId, promise); + HttpHeaders reqHaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; + if (fixed) { + String length = String.valueOf(promiseBytes.length); + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + ACCEPT_ALL); + } else { + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + } + t.sendHttp3PushResponse(pushId, promise, reqHaders, rspHeaders, new ByteArrayInputStream(promiseBytes)); } catch (URISyntaxException x) { throw new IOException(x.getMessage(), x); } @@ -822,13 +910,31 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters } byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8); t.sendResponseHeaders(200, resp.length); //fixed content length + + // With HTTP/3 fixed length we send a single DataFrame, + // therefore we can't interleave a PushPromiseFrame in + // the middle of the DataFrame, so we're going to send + // the PushPromiseFrame before the DataFrame, and then + // fulfill the promise later while sending the response + // body. + long[] pushIds = new long[2]; + if (t.getExchangeVersion() == HTTP_3) { + for (int i = 0; i < 2; i++) { + String path = requestURI.getPath() + "/after/promise-" + (i + 2); + pushIds[i] = sendHttp3PushPromiseFrame(t, requestURI, path, true); + } + } try (OutputStream os = t.getResponseBody()) { int bytes = resp.length/3; for (int i = 0; i<2; i++) { - String path = requestURI.getPath() + "/after/promise-" + (i + 2); os.write(resp, i * bytes, bytes); os.flush(); - pushPromiseFor(t, requestURI, path, true); + String path = requestURI.getPath() + "/after/promise-" + (i + 2); + if (t.getExchangeVersion() == HTTP_2) { + pushPromiseFor(t, requestURI, path, true); + } else if (t.getExchangeVersion() == HTTP_3) { + sendHttp3PushResponse(t, pushIds[i], requestURI, path, true); + } } os.write(resp, 2*bytes, resp.length - 2*bytes); } diff --git a/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java b/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java index 7362ada9772..0dc808b8bb2 100644 --- a/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java +++ b/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java @@ -21,6 +21,7 @@ * questions. */ +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -40,6 +41,7 @@ import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; @@ -66,10 +68,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import static java.lang.System.err; import static java.lang.System.out; import static java.lang.String.format; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -81,6 +86,7 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -89,6 +95,9 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final int ITERATION_COUNT = 1; static final int REPEAT_RESPONSE = 3; @@ -149,6 +158,41 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters + (params == null ? "()" : Arrays.toString(result.getParameters())); } + static Version version(String uri) { + if (uri.contains("/http1/") || uri.contains("/https1/")) + return HTTP_1_1; + if (uri.contains("/http2/") || uri.contains("/https2/")) + return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + System.out.println("\n" + now() + "--- Sending HEAD request ----\n"); + System.err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + System.out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + System.err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + @BeforeMethod void beforeMethod(ITestContext context) { if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { @@ -188,6 +232,8 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, httpURI_fixed, httpURI_chunk, httpsURI_fixed, @@ -238,7 +284,7 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters private HttpClient makeNewClient() { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -296,10 +342,14 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters String uri2 = uri + "-" + URICOUNT.incrementAndGet() + "/sanity"; out.printf("%ntestSanity(%s, %b)%n", uri2, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } - HttpRequest req = HttpRequest.newBuilder(URI.create(uri2)) + HttpRequest req = newRequestBuilder(uri2) .build(); BodyHandler handler = new ThrowingBodyHandler((w) -> {}, @@ -439,12 +489,15 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters HttpClient client = null; var throwing = thrower; for (Where where : EnumSet.complementOf(excludes)) { - - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } + String uri2 = uri + "-" + where; - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri2)) + HttpRequest req = newRequestBuilder(uri2) .build(); thrower = thrower(where, throwing); @@ -593,8 +646,12 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters static final class UncheckedCustomExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedCustomException(where.name()); + var thread = Thread.currentThread().getName(); + var thrown = new UncheckedCustomException("[" + thread + "] " + where.name()); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + thrown.printStackTrace(); + throw thrown; } @Override @@ -611,8 +668,13 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters static final class UncheckedIOExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedIOException(new CustomIOException(where.name())); + var thread = Thread.currentThread().getName(); + var cause = new CustomIOException("[" + thread + "] " + where.name()); + var thrown = new UncheckedIOException(cause); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + cause.printStackTrace(); + throw thrown; } @Override @@ -759,10 +821,15 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters @BeforeTest public void setup() throws Exception { + System.out.println(now() + "setup"); + System.err.println(now() + "setup"); + sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); + System.out.println(now() + "HTTP/1.1 server created"); + // HTTP/1.1 HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler(); HttpTestHandler h1_chunkHandler = new HTTP_ChunkedHandler(); @@ -772,6 +839,8 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x"; httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x"; + System.out.println(now() + "TLS HTTP/1.1 server created"); + httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed"); httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk"); @@ -788,21 +857,56 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x"; + System.out.println(now() + "HTTP/2 server created"); + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(4); + System.out.println(now() + "TLS HTTP/2 server created"); + + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + + System.out.println(now() + "HTTP/3 server created"); + System.err.println(now() + "Starting servers"); + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + + out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); + out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); + out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); + out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); + + System.out.println(now() + "setup done"); + System.err.println(now() + "setup done"); } @AfterTest public void teardown() throws Exception { + System.out.println(now() + "teardown"); + System.err.println(now() + "teardown"); + String sharedClientName = sharedClient == null ? null : sharedClient.toString(); sharedClient = null; @@ -813,6 +917,7 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { @@ -821,6 +926,8 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters throw fail; } } + System.out.println(now() + "teardown done"); + System.err.println(now() + "teardown done"); } static class HTTP_FixedLengthHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java b/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java index a879525b4b4..8ec3b256e62 100644 --- a/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java +++ b/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java @@ -28,7 +28,7 @@ * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters * ReferenceTracker AggregateRequestBodyTest * @run testng/othervm -Djdk.internal.httpclient.debug=true - * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,headers,frames * AggregateRequestBodyTest * @summary Tests HttpRequest.BodyPublishers::concat */ @@ -69,6 +69,7 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import javax.net.ssl.SSLContext; import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; import org.testng.Assert; import org.testng.ITestContext; import org.testng.ITestResult; @@ -83,6 +84,8 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -95,14 +98,15 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) - String http1URI; - String https1URI; - String http2URI; - String https2URI; + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) + URI http1URI; + URI https1URI; + URI http2URI; + URI https2URI; + URI http3URI; static final int RESPONSE_CODE = 200; static final int ITERATION_COUNT = 4; - static final Class IAE = IllegalArgumentException.class; static final Class CE = CompletionException.class; // a shared executor helps reduce the amount of threads created by the test static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); @@ -197,12 +201,13 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { } } - private String[] uris() { - return new String[] { + private URI[] uris() { + return new URI[] { http1URI, https1URI, http2URI, https2URI, + http3URI, }; } @@ -213,12 +218,18 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { return new Object[0][]; } - String[] uris = uris(); + URI[] uris = uris(); Object[][] result = new Object[uris.length * 2][]; int i = 0; for (boolean sameClient : List.of(false, true)) { - for (String uri : uris()) { - result[i++] = new Object[]{uri, sameClient}; + for (URI uri : uris()) { + HttpClient.Version version = null; + if (uri.equals(http1URI) || uri.equals(https1URI)) version = HttpClient.Version.HTTP_1_1; + else if (uri.equals(http2URI) || uri.equals(https2URI)) version = HttpClient.Version.HTTP_2; + else if (uri.equals(http3URI)) version = HTTP_3; + else throw new AssertionError("Unexpected URI: " + uri); + + result[i++] = new Object[]{uri, version, sameClient}; } } assert i == uris.length * 2; @@ -227,7 +238,7 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -802,7 +813,7 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { } @Test(dataProvider = "variants") - public void test(String uri, boolean sameClient) throws Exception { + public void test(URI uri, HttpClient.Version version, boolean sameClient) throws Exception { checkSkip(); System.out.printf("Request to %s (sameClient: %s)%n", uri, sameClient); System.err.printf("Request to %s (sameClient: %s)%n", uri, sameClient); @@ -814,9 +825,12 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { .map(BodyPublishers::ofString) .toArray(HttpRequest.BodyPublisher[]::new) ); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + + HttpRequest request = HttpRequest.newBuilder(uri) + .version(version) .POST(publisher) .build(); + for (int i = 0; i < ITERATION_COUNT; i++) { System.out.println(uri + ": Iteration: " + i); System.err.println(uri + ": Iteration: " + i); @@ -826,9 +840,19 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode())); assertEquals(response.body(), BODIES.stream().collect(Collectors.joining())); } + if (!sameClient) client.close(); System.out.println("test: DONE"); } + private URI buildURI(String scheme, String path, int port) { + return URIBuilder.newBuilder() + .scheme(scheme) + .loopback() + .port(port) + .path(path) + .buildUnchecked(); + } + @BeforeTest public void setup() throws Exception { sslContext = new SimpleSSLContext().get(); @@ -838,32 +862,37 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { HttpTestHandler handler = new HttpTestEchoHandler(); http1TestServer = HttpTestServer.create(HTTP_1_1); http1TestServer.addHandler(handler, "/http1/echo/"); - http1URI = "http://" + http1TestServer.serverAuthority() + "/http1/echo/x"; + http1URI = buildURI("http", "/http1/echo/x", http1TestServer.getAddress().getPort()); https1TestServer = HttpTestServer.create(HTTP_1_1, sslContext); https1TestServer.addHandler(handler, "/https1/echo/"); - https1URI = "https://" + https1TestServer.serverAuthority() + "/https1/echo/x"; + https1URI = buildURI("https", "/https1/echo/x", https1TestServer.getAddress().getPort()); - // HTTP/2 http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(handler, "/http2/echo/"); - http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo/x"; + http2URI = buildURI("http", "/http2/echo/x", http2TestServer.getAddress().getPort()); https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(handler, "/https2/echo/"); - https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo/x"; + https2URI = buildURI("https", "/https2/echo/x", https2TestServer.getAddress().getPort()); - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(handler, "/http3/echo/"); + http3URI = buildURI("https", "/http3/echo/x", http3TestServer.getAddress().getPort()); + + serverCount.addAndGet(5); http1TestServer.start(); https1TestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest public void teardown() throws Exception { String sharedClientName = sharedClient == null ? null : sharedClient.toString(); + sharedClient.close(); sharedClient = null; Thread.sleep(100); AssertionError fail = TRACKER.check(500); @@ -872,6 +901,7 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { https1TestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/AltServiceUsageTest.java b/test/jdk/java/net/httpclient/AltServiceUsageTest.java new file mode 100644 index 00000000000..2322b36918b --- /dev/null +++ b/test/jdk/java/net/httpclient/AltServiceUsageTest.java @@ -0,0 +1,454 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.OptionalLong; + +import javax.net.ssl.SSLContext; + +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/* + * @test + * @summary Verifies that the HTTP client correctly handles various alt-svc usages + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters + * + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * AltServiceUsageTest + */ +public class AltServiceUsageTest implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer originServer; + private HttpTestServer altServer; + + private DatagramChannel udpNotResponding; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + + // attempt to create an HTTP/3 server, an HTTP/2 server, and a + // DatagramChannel bound to the same port as the HTTP/2 server + int count = 0; + InetSocketAddress altServerAddress = null, originServerAddress = null; + while (count++ < 10) { + + createServers(); + altServerAddress = altServer.getAddress(); + originServerAddress = originServer.getAddress(); + + if (originServerAddress.equals(altServerAddress)) break; + udpNotResponding = DatagramChannel.open(); + try { + udpNotResponding.bind(originServerAddress); + break; + } catch (IOException x) { + System.out.printf("Failed to bind udpNotResponding to %s: %s%n", + originServerAddress, x); + safeStop(altServer); + safeStop(originServer); + udpNotResponding.close(); + } + } + if (count > 10) { + throw new AssertionError("Couldn't reserve UDP port at " + originServerAddress); + } + + System.out.println("HTTP/3 service started at " + altServerAddress); + System.out.println("HTTP/2 service started at " + originServerAddress); + System.err.println("**** All servers started. Test will start shortly ****"); + } + + private void createServers() throws IOException { + altServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + altServer.addHandler(new All200OKHandler(), "/foo/"); + altServer.addHandler(new RequireAltUsedHeader(), "/bar/"); + altServer.addHandler(new All200OKHandler(), "/maxAgeAltSvc/"); + altServer.start(); + + originServer = HttpTestServer.create(HTTP_2, sslContext); + originServer.addHandler(new H3AltServicePublisher(altServer.getAddress()), "/foo/"); + originServer.addHandler(new H3AltSvcPublisherWith421(altServer.getAddress()), "/foo421/"); + originServer.addHandler(new H3AltServicePublisher(altServer.getAddress()), "/bar/"); + originServer.addHandler(new H3AltSvcPublisherWithMaxAge(altServer.getAddress()), "/maxAgeAltSvc/"); + originServer.start(); + } + + @AfterClass + public void afterClass() throws Exception { + safeStop(originServer); + safeStop(altServer); + udpNotResponding.close(); + } + + private static void safeStop(final HttpTestServer server) { + if (server == null) { + return; + } + final InetSocketAddress serverAddr = server.getAddress(); + try { + System.out.println("Stopping server " + serverAddr); + server.stop(); + } catch (Exception e) { + System.err.println("Ignoring exception: " + e.getMessage() + " that occurred " + + "during stop of server: " + serverAddr); + } + } + + private static class H3AltServicePublisher implements HttpTestHandler { + private static final String RESPONSE_CONTENT = "apple"; + + private final String altSvcHeader; + + /** + * @param altServerAddr Address of the alt service which will be advertised by this handler + */ + private H3AltServicePublisher(final InetSocketAddress altServerAddr) { + this.altSvcHeader = "h3=\"" + altServerAddr.getHostName() + ":" + altServerAddr.getPort() + + "\"; persist=1; intentional-unknown-param-which-is-expected-to-be-ignored=foo;"; + } + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + String maxAgeParam = ""; + if (getMaxAge().isPresent()) { + maxAgeParam = "; ma=" + getMaxAge().getAsLong(); + } + exchange.getResponseHeaders().addHeader("alt-svc", altSvcHeader + maxAgeParam); + final int statusCode = getResponseCode(); + System.out.println("Sending response with status code " + statusCode + " and " + + "alt-svc header " + altSvcHeader); + final byte[] content = RESPONSE_CONTENT.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(statusCode, content.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } + + protected OptionalLong getMaxAge() { + return OptionalLong.empty(); + } + + protected int getResponseCode() { + return 200; + } + } + + private static final class H3AltSvcPublisherWith421 extends H3AltServicePublisher { + + private H3AltSvcPublisherWith421(InetSocketAddress h3ServerAddr) { + super(h3ServerAddr); + } + + @Override + protected int getResponseCode() { + return 421; + } + } + + private static final class H3AltSvcPublisherWithMaxAge extends H3AltServicePublisher { + + private H3AltSvcPublisherWithMaxAge(InetSocketAddress h3ServerAddr) { + super(h3ServerAddr); + } + + @Override + protected OptionalLong getMaxAge() { + return OptionalLong.of(2); + } + } + + private static final class All200OKHandler implements HttpTestHandler { + private static final String RESPONSE_CONTENT = "orange"; + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + final byte[] content = RESPONSE_CONTENT.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, content.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } + } + + private static final class RequireAltUsedHeader implements HttpTestHandler { + private static final String RESPONSE_CONTENT = "tomato"; + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + final Optional altUsed = exchange.getRequestHeaders().firstValue("alt-used"); + if (altUsed.isEmpty()) { + System.out.println("Error - Missing alt-used header in request"); + exchange.sendResponseHeaders(400, 0); + return; + } + if (altUsed.get().isBlank()) { + System.out.println("Error - Blank value for alt-used header in request"); + exchange.sendResponseHeaders(400, 0); + return; + } + System.out.println("Found alt-used request header: " + altUsed.get()); + final byte[] content = RESPONSE_CONTENT.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, content.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } + } + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server. The test then issues the exact same request again and this time it + * expects the alt-service host/server to handle that request. + */ + @Test + public void testAltSvcHeaderUsage() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/foo/"); + final HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(secondResponse.body(), All200OKHandler.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server and a response code of 421. The spec states that when this response code + * is sent, any alt-svc headers should be ignored by the HTTP client. This test then issues the same + * request again and expects that the alt-service wasn't used. + */ + @Test + public void testDontUseAltServiceFor421() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/foo421/"); + final HttpRequest request = HttpRequest.newBuilder().GET().uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 421, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 421, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server. The test then issues the exact same request again and this time it + * expects the alt-service host/server to handle that request. The alt-service host/server which + * handles this request will assert that the request came in with an "alt-used" header (which + * is expected to be set by the HTTP client). If such a header is missing then that server + * responds back with an erroneous response code of 4xx. + */ + @Test + public void testAltUsedHeader() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/bar/"); + final HttpRequest request = HttpRequest.newBuilder().GET().uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + // (which on the server side will assert the presence of the alt-used header, set by the + // HTTP client) + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(secondResponse.body(), RequireAltUsedHeader.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server. The advertised alt-svc is expected to have a max age of some seconds. + * The test then immediately issues the exact same request again and this time it + * expects the alt-service host/server to handle that request. Once this is done, the test waits + * for a while (duration is greater than the max age of the advertised alt-service) and then + * issues the exact same request again. This time the request is expected to be handled by the + * origin server and not the alt-service (since the alt-service is expected to have expired by + * now) + */ + @Test + public void testAltSvcMaxAgeExpiry() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/maxAgeAltSvc/"); + final HttpRequest request = HttpRequest.newBuilder().GET().uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(secondResponse.body(), All200OKHandler.RESPONSE_CONTENT, + "Unexpected response body"); + + // wait for alt-service to expire + final long sleepDuration = 4000; + System.out.println("Sleeping for " + sleepDuration + " milli seconds for alt-service to expire"); + Thread.sleep(sleepDuration); + // now issue the same request again and this time expect it to be handled by the origin server + // since the alt-service is expected to be expired + System.out.println("Issuing request for a third time " + reqURI); + final HttpResponse thirdResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(thirdResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(thirdResponse.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + System.gc(); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + private static String toHostPort(final HttpTestServer server) { + final InetSocketAddress addr = server.getAddress(); + return addr.getHostName() + ":" + addr.getPort(); + } +} diff --git a/test/jdk/java/net/httpclient/AsFileDownloadTest.java b/test/jdk/java/net/httpclient/AsFileDownloadTest.java index 31d919230a4..42567602dff 100644 --- a/test/jdk/java/net/httpclient/AsFileDownloadTest.java +++ b/test/jdk/java/net/httpclient/AsFileDownloadTest.java @@ -35,6 +35,8 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; @@ -48,11 +50,16 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; + import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.common.TestServerConfigurator; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; @@ -61,6 +68,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.lang.System.out; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.http.HttpResponse.BodyHandlers.ofFileDownload; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardOpenOption.*; @@ -75,6 +84,7 @@ import static org.testng.Assert.fail; * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * jdk.test.lib.Platform jdk.test.lib.util.FileUtils + * jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm/timeout=480 AsFileDownloadTest */ @@ -85,10 +95,12 @@ public class AsFileDownloadTest { HttpsServer httpsTestServer; // HTTPS/1.1 Http2TestServer http2TestServer; // HTTP/2 ( h2c ) Http2TestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h3TestServer; // HTTP/3 String httpURI; String httpsURI; String http2URI; String https2URI; + String h3URI; final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; Path tempDir; @@ -144,38 +156,50 @@ public class AsFileDownloadTest { List list = new ArrayList<>(); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {httpURI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {httpURI + "?" + e[0], e[1], e[2], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], e[2], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {http2URI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {http2URI + "?" + e[0], e[1], e[2], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {https2URI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {https2URI + "?" + e[0], e[1], e[2], Optional.empty()}) + .forEach(list::add); + Arrays.asList(contentDispositionValues).stream() + .map(e -> new Object[] {h3URI + "?" + e[0], e[1], e[2], Optional.of(Version.HTTP_3)}) .forEach(list::add); return list.stream().toArray(Object[][]::new); } + HttpClient newHttpClient(Optional version) { + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpClient.newBuilder() + : HttpServerAdapters.createClientBuilderForH3().version(Version.HTTP_3); + return builder.sslContext(sslContext).proxy(Builder.NO_PROXY).build(); + } + @Test(dataProvider = "positive") - void test(String uriString, String contentDispositionValue, String expectedFilename) - throws Exception - { + void test(String uriString, String contentDispositionValue, String expectedFilename, + Optional requestVersion) throws Exception { out.printf("test(%s, %s, %s): starting", uriString, contentDispositionValue, expectedFilename); - HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); - TRACKER.track(client); - ReferenceQueue queue = new ReferenceQueue<>(); - WeakReference ref = new WeakReference<>(client, queue); - try { + try (HttpClient client = newHttpClient(requestVersion)) { + TRACKER.track(client); + ReferenceQueue queue = new ReferenceQueue<>(); + WeakReference ref = new WeakReference<>(client, queue); URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri) - .POST(BodyPublishers.ofString("May the luck of the Irish be with you!")) - .build(); + HttpRequest.Builder requestBuilder = newRequestBuilder(uriString); + if (requestVersion.isPresent()) { + requestBuilder.version(requestVersion.get()); + } + HttpRequest request = requestBuilder.POST( + BodyPublishers.ofString("May the luck of the Irish be with you!")).build(); BodyHandler bh = ofFileDownload(tempDir.resolve(uri.getPath().substring(1)), CREATE, TRUNCATE_EXISTING, WRITE); + out.println("Issuing request " + request); HttpResponse response = client.send(request, bh); Path body = response.body(); out.println("Got response: " + response); @@ -189,6 +213,10 @@ public class AsFileDownloadTest { assertEquals(response.headers().firstValue("Content-Disposition").get(), contentDispositionValue); assertEquals(fileContents, "May the luck of the Irish be with you!"); + if (requestVersion.isPresent()) { + assertEquals(response.version(), requestVersion.get(), "unexpected HTTP version" + + " in response"); + } if (!body.toAbsolutePath().startsWith(tempDir.toAbsolutePath())) { System.out.println("Tempdir = " + tempDir.toAbsolutePath()); @@ -198,16 +226,9 @@ public class AsFileDownloadTest { // additional checks unrelated to file download caseInsensitivityOfHeaders(request.headers()); caseInsensitivityOfHeaders(response.headers()); - } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { - System.gc(); - if (queue.remove(100) == ref) break; - } - AssertionError failed = TRACKER.checkShutdown(1000); - if (failed != null) throw failed; } + AssertionError failed = TRACKER.checkClosed(1000); + if (failed != null) throw failed; } // --- Negative @@ -238,54 +259,49 @@ public class AsFileDownloadTest { List list = new ArrayList<>(); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {httpURI + "?" + e[0], e[1]}) + .map(e -> new Object[] {httpURI + "?" + e[0], e[1], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {httpsURI + "?" + e[0], e[1]}) + .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {http2URI + "?" + e[0], e[1]}) + .map(e -> new Object[] {http2URI + "?" + e[0], e[1], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {https2URI + "?" + e[0], e[1]}) + .map(e -> new Object[] {https2URI + "?" + e[0], e[1], Optional.empty()}) + .forEach(list::add); + Arrays.asList(contentDispositionBADValues).stream() + .map(e -> new Object[] {h3URI + "?" + e[0], e[1], Optional.of(Version.HTTP_3)}) .forEach(list::add); - return list.stream().toArray(Object[][]::new); } @Test(dataProvider = "negative") - void negativeTest(String uriString, String contentDispositionValue) - throws Exception - { + void negativeTest(String uriString, String contentDispositionValue, + Optional requestVersion) throws Exception { out.printf("negativeTest(%s, %s): starting", uriString, contentDispositionValue); - HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); - TRACKER.track(client); - ReferenceQueue queue = new ReferenceQueue<>(); - WeakReference ref = new WeakReference<>(client, queue); + try (HttpClient client = newHttpClient(requestVersion)) { + TRACKER.track(client); + ReferenceQueue queue = new ReferenceQueue<>(); + WeakReference ref = new WeakReference<>(client, queue); - try { - URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri) - .POST(BodyPublishers.ofString("Does not matter")) + HttpRequest.Builder reqBuilder = newRequestBuilder(uriString); + if (requestVersion.isPresent()) { + reqBuilder.version(requestVersion.get()); + } + HttpRequest request = reqBuilder.POST(BodyPublishers.ofString("Does not matter")) .build(); - BodyHandler bh = ofFileDownload(tempDir, CREATE, TRUNCATE_EXISTING, WRITE); try { + out.println("Issuing request " + request); HttpResponse response = client.send(request, bh); fail("UNEXPECTED response: " + response + ", path:" + response.body()); } catch (UncheckedIOException | IOException ioe) { System.out.println("Caught expected: " + ioe); } - } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { - System.gc(); - if (queue.remove(100) == ref) break; - } - AssertionError failed = TRACKER.checkShutdown(1000); - if (failed != null) throw failed; } + AssertionError failed = TRACKER.checkClosed(1000); + if (failed != null) throw failed; } // -- Infrastructure @@ -297,6 +313,23 @@ public class AsFileDownloadTest { return h + ":" + server.getAddress().getPort(); } + Version version(String uri) { + if (uri.contains("/http1/")) return Version.HTTP_1_1; + if (uri.contains("/https1/")) return Version.HTTP_1_1; + if (uri.contains("/http2/")) return Version.HTTP_2; + if (uri.contains("/https2/")) return Version.HTTP_2; + if (uri.contains("/h3/")) return Version.HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == Version.HTTP_3) { + builder.setOption(H3_DISCOVERY, h3TestServer.h3DiscoveryConfig()); + } + return builder; + } + @BeforeTest public void setup() throws Exception { tempDir = Paths.get("asFileDownloadTest.tmp.dir"); @@ -309,6 +342,7 @@ public class AsFileDownloadTest { Files.createDirectories(tempDir.resolve("https1/afdt/")); Files.createDirectories(tempDir.resolve("http2/afdt/")); Files.createDirectories(tempDir.resolve("https2/afdt/")); + Files.createDirectories(tempDir.resolve("h3/afdt/")); // HTTP/1.1 server logging in case of security exceptions ( uncomment if needed ) //Logger logger = Logger.getLogger("com.sun.net.httpserver"); @@ -339,10 +373,15 @@ public class AsFileDownloadTest { https2TestServer.addHandler(new Http2FileDispoHandler(), "/https2/afdt"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new Http2FileDispoHandler(), "/h3/afdt"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/afdt"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -351,6 +390,7 @@ public class AsFileDownloadTest { httpsTestServer.stop(0); http2TestServer.stop(); https2TestServer.stop(); + h3TestServer.stop(); if (Files.exists(tempDir)) { // clean up @@ -392,7 +432,8 @@ public class AsFileDownloadTest { } } - static class Http2FileDispoHandler implements Http2Handler { + static class Http2FileDispoHandler implements Http2Handler, HttpServerAdapters.HttpTestHandler { + @Override public void handle(Http2TestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); @@ -407,6 +448,21 @@ public class AsFileDownloadTest { os.write(bytes); } } + + @Override + public void handle(HttpTestExchange t) throws IOException { + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + byte[] bytes = is.readAllBytes(); + + String value = contentDispositionValueFromURI(t.getRequestURI()); + if (!value.equals("<>")) + t.getResponseHeaders().addHeader("Content-Disposition", value); + + t.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } } // --- diff --git a/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java b/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java index 7e7709c033c..5338d64892b 100644 --- a/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java +++ b/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java @@ -40,12 +40,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -62,15 +62,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; -import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import javax.net.ssl.SSLContext; + import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -82,6 +77,9 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -94,15 +92,21 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } static final Random RANDOM = RandomFactory.getRandom(); + ExecutorService readerService; SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/2 ( h2+h3 ) + HttpTestServer h3TestServer; // HTTP/2 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h3URI; + String h2h3Head; static final String MESSAGE = "AsyncExecutorShutdown message body"; static final int ITERATIONS = 3; @@ -110,10 +114,12 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig() }, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig() }, + { httpURI, HTTP_1_1, null }, + { httpsURI, HTTP_1_1, null }, + { http2URI, HTTP_2, null }, + { https2URI, HTTP_2, null }, }; } @@ -127,8 +133,9 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { return t; } - static String readBody(InputStream in) { - try { + static String readBody(InputStream body) { + out.printf("[%s] reading body%n", Thread.currentThread()); + try (var in = body) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); @@ -159,26 +166,37 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting (%s, %s, %s) ----%n%n", uriString, version, config); ExecutorService executorService = Executors.newCachedThreadPool(); - ExecutorService readerService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) .executor(executorService) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); assert client.executor().isPresent(); + Throwable failed = null; int step = RANDOM.nextInt(ITERATIONS); + int head = Math.min(1, step); + List> bodies = new ArrayList<>(); try { - List> bodies = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { + if (i == head && version == HTTP_3 && config != HTTP_3_URI_ONLY) { + // let's the first request go through whatever version, + // but ensure that the second will find an AltService + // record + out.printf("%d: sending head request%n", i); + headRequest(client); + out.printf("%d: head request sent%n", i); + } URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; @@ -189,11 +207,15 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { .thenApply((response) -> { out.println(si + ": Got response: " + response); assertEquals(response.statusCode(), 200); + if (si >= head) assertEquals(response.version(), version); return response; }); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(AsyncExecutorShutdown::readBody) - .thenApply((s) -> { assertEquals(s, MESSAGE); return s;}); + .thenApply((s) -> { + assertEquals(s, MESSAGE); + return s; + }); } catch (RejectedExecutionException x) { out.println(i + ": Got expected exception: " + x); continue; @@ -205,7 +227,8 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } if (i == step) { out.printf("%d: shutting down executor now%n", i, sleep); - executorService.shutdownNow(); + var list = executorService.shutdownNow(); + out.printf("%d: executor shut down: %s%n", i, list); } var cf = bodyCF.exceptionally((t) -> { Throwable cause = getCause(t); @@ -219,39 +242,72 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { checkCause(String.valueOf(si), cause); return null; }); + out.printf("%d: adding body to bodies list%n", i); bodies.add(cf); } - CompletableFuture.allOf(bodies.toArray(new CompletableFuture[0])).get(); + } catch (Throwable t) { + failed = t; } finally { client = null; - executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); - readerService.shutdown(); - readerService.awaitTermination(2000, TimeUnit.MILLISECONDS); + System.gc(); + try { + out.println("Awaiting executorService termination"); + executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); + out.println("Done"); + } catch (Throwable t) { + if (failed != null) { + failed.addSuppressed(t); + } else failed = t; + } + var error = TRACKER.checkShutdown(2000); + if (error != null) { + out.println("Client hasn't shutdown properly: " + error); + if (failed != null) failed.addSuppressed(error); + else failed = error; + } } + if (failed instanceof Exception fe) { + throw fe; + } else if (failed instanceof Error e) { + throw e; + } + out.println("Awaiting all bodies..."); + CompletableFuture.allOf(bodies.toArray(new CompletableFuture[0])).get(); } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting (%s, %s, %s) ----%n%n", uriString, version, config); ExecutorService executorService = Executors.newCachedThreadPool(); - ExecutorService readerService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .executor(executorService) .sslContext(sslContext) .build(); TRACKER.track(client); assert client.executor().isPresent(); + Throwable failed = null; int step = RANDOM.nextInt(ITERATIONS); + int head = Math.min(1, step); out.printf("will shutdown executor in step %d%n", step); try { for (int i = 0; i < ITERATIONS; i++) { + if (i == head && version == HTTP_3 && config != HTTP_3_URI_ONLY) { + // let's the first request go through whatever version, + // but ensure that the second will find an AltService + // record + out.printf("%d: sending head request%n", i); + headRequest(client); + out.printf("%d: head request sent%n", i); + } URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) - .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) - .build(); + .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) + .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); final int si = i; CompletableFuture> responseCF; @@ -261,6 +317,7 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { .thenApply((response) -> { out.println(si + ": Got response: " + response); assertEquals(response.statusCode(), 200); + if (si > 0) assertEquals(response.version(), version); return response; }); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) @@ -278,9 +335,10 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } if (i == step) { out.printf("%d: shutting down executor now%n", i, sleep); - executorService.shutdownNow(); + var list = executorService.shutdownNow(); + out.printf("%d: executor shut down: %s%n", i, list); } - bodyCF.handle((r,t) -> { + bodyCF.handle((r, t) -> { if (t != null) { try { Throwable cause = getCause(t); @@ -301,22 +359,63 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } }).thenCompose((c) -> c).get(); } - } finally { + } catch (Throwable t) { + t.printStackTrace(); + failed = t; + } finally { client = null; - executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); - readerService.shutdown(); - readerService.awaitTermination(2000, TimeUnit.MILLISECONDS); + System.gc(); + try { + out.println("Awaiting executorService termination"); + executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); + out.println("Done"); + } catch (Throwable t) { + t.printStackTrace(); + if (failed != null) { + failed.addSuppressed(t); + } else failed = t; + } + var error = TRACKER.checkShutdown(2000); + if (error != null) { + out.println("Client hasn't shutdown properly: " + error); + if (failed != null) failed.addSuppressed(error); + else failed = error; + } + } + if (failed instanceof Exception fe) { + throw fe; + } else if (failed instanceof Error e) { + throw e; } } // -- Infrastructure + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + + static void shutdown(ExecutorService executorService) { + try { + executorService.shutdown(); + executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + executorService.shutdownNow(); + } + } + @BeforeTest public void setup() throws Exception { out.println("\n**** Setup ****\n"); sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); + readerService = Executors.newCachedThreadPool(); httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new ServerRequestHandler(), "/http1/exec/"); @@ -332,10 +431,21 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -343,10 +453,13 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { Thread.sleep(100); AssertionError fail = TRACKER.checkShutdown(5000); try { + shutdown(readerService); httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/AsyncShutdownNow.java b/test/jdk/java/net/httpclient/AsyncShutdownNow.java index 39c82735248..2617b60ee1c 100644 --- a/test/jdk/java/net/httpclient/AsyncShutdownNow.java +++ b/test/jdk/java/net/httpclient/AsyncShutdownNow.java @@ -45,7 +45,9 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -62,6 +64,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; + import jdk.httpclient.test.lib.common.HttpServerAdapters; import javax.net.ssl.SSLContext; @@ -72,11 +75,14 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -96,10 +102,15 @@ public class AsyncShutdownNow implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "AsyncShutdownNow message body"; static final int ITERATIONS = 3; @@ -107,10 +118,12 @@ public class AsyncShutdownNow implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -124,14 +137,57 @@ public class AsyncShutdownNow implements HttpServerAdapters { return t; } - static String readBody(InputStream in) { - try { + static String readBody(InputStream body) { + try (InputStream in = body) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); } } + record ExchangeResult(int step, + Version version, + Http3DiscoveryMode config, + HttpResponse response, + boolean firstVersionMayNotMatch) { + + static ExchangeResult afterHead(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, false); + } + + static ExchangeResult ofSequential(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, true); + } + + ExchangeResult withResponse(HttpResponse response) { + return new ExchangeResult(step(), version(), config(), response, firstVersionMayNotMatch()); + } + + // Ensures that the input stream gets closed in case of assertion + ExchangeResult assertResponseState() { + out.println(step + ": Got response: " + response); + out.printf("%s: expect status 200 and version %s (%s) for %s%n", step, version, config, + response.request().uri()); + assertEquals(response.statusCode(), 200); + if (step == 0 && version == HTTP_3 && firstVersionMayNotMatch) { + out.printf("%s: version not checked%n", step); + } else { + assertEquals(response.version(), version); + out.printf("%s: got expected version %s%n", step, response.version()); + } + return this; + } + } + + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + static boolean hasExpectedMessage(IOException io) { String message = io.getMessage(); if (message == null) return false; @@ -168,11 +224,12 @@ public class AsyncShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting concurrent (%s) ----%n%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting concurrent (%s, %s, %s) ----%n%n", uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -181,21 +238,25 @@ public class AsyncShutdownNow implements HttpServerAdapters { Throwable failed = null; List> bodies = new ArrayList<>(); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; CompletableFuture bodyCF; final int si = i; + ExchangeResult result = ExchangeResult.afterHead(si, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) - .thenApply((response) -> { - out.println(si + ": Got response: " + response); - assertEquals(response.statusCode(), 200); - return response; - }); + .thenApply(result::withResponse) + .thenApplyAsync(ExchangeResult::assertResponseState, readerService) + .thenApply(ExchangeResult::response); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(AsyncShutdownNow::readBody) .thenApply((s) -> { @@ -260,11 +321,13 @@ public class AsyncShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting sequential (%s) ----%n%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting sequential (%s, %s, %s) ----%n%n", + uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -277,17 +340,17 @@ public class AsyncShutdownNow implements HttpServerAdapters { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); final int si = i; + ExchangeResult result = ExchangeResult.ofSequential(si, version, config); CompletableFuture> responseCF; CompletableFuture bodyCF; responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) - .thenApply((response) -> { - out.println(si + ": Got response: " + response); - assertEquals(response.statusCode(), 200); - return response; - }); + .thenApply(result::withResponse) + .thenApplyAsync(ExchangeResult::assertResponseState, readerService) + .thenApply(ExchangeResult::response); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(AsyncShutdownNow::readBody) .thenApply((s) -> { @@ -362,10 +425,21 @@ public class AsyncShutdownNow implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -378,6 +452,8 @@ public class AsyncShutdownNow implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/AuthFilterCacheTest.java b/test/jdk/java/net/httpclient/AuthFilterCacheTest.java index e819f96d947..32026db57fb 100644 --- a/test/jdk/java/net/httpclient/AuthFilterCacheTest.java +++ b/test/jdk/java/net/httpclient/AuthFilterCacheTest.java @@ -26,14 +26,18 @@ import java.net.*; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.Collectors; + import jdk.httpclient.test.lib.common.HttpServerAdapters; -import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsServer; import jdk.httpclient.test.lib.common.TestServerConfigurator; import org.testng.annotations.AfterClass; @@ -45,6 +49,12 @@ import javax.net.ssl.SSLContext; 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 java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; /* * @test @@ -52,9 +62,9 @@ import static java.net.http.HttpClient.Version.HTTP_2; * @summary AuthenticationFilter.Cache::remove may throw ConcurrentModificationException * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext - * DigestEchoServer jdk.httpclient.test.lib.common.TestServerConfigurator + * DigestEchoServer ReferenceTracker jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm -Dtest.requiresHost=true - * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.httpclient.HttpClient.log=requests,headers,errors,quic * -Djdk.internal.httpclient.debug=false * AuthFilterCacheTest */ @@ -63,7 +73,7 @@ public class AuthFilterCacheTest implements HttpServerAdapters { static final String RESPONSE_BODY = "Hello World!"; static final int REQUEST_COUNT = 5; - static final int URI_COUNT = 6; + static final int URI_COUNT = 8; static final CyclicBarrier barrier = new CyclicBarrier(REQUEST_COUNT * URI_COUNT); static final SSLContext context; @@ -80,11 +90,15 @@ public class AuthFilterCacheTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer h3onlyServer; + HttpTestServer h3altSvcServer; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI h3onlyURI; + URI h3altSvcURI; InetSocketAddress proxyAddress; ProxySelector proxySelector; MyAuthenticator auth; @@ -101,14 +115,15 @@ public class AuthFilterCacheTest implements HttpServerAdapters { https1URI.resolve("proxy/orig/"), http2URI.resolve("direct/orig/"), https2URI.resolve("direct/orig/"), - https2URI.resolve("proxy/orig/"))} + https2URI.resolve("proxy/orig/"), + h3onlyURI.resolve("direct/orig/"), + h3altSvcURI.resolve("direct/orig/"))} }; return uris; } public HttpClient newHttpClient(ProxySelector ps, Authenticator auth) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .executor(virtualExecutor) .sslContext(context) .authenticator(auth) @@ -154,12 +169,34 @@ public class AuthFilterCacheTest implements HttpServerAdapters { https2URI = new URI("https://" + https2Server.serverAuthority() + "/AuthFilterCacheTest/https2/"); + h3onlyServer = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + h3onlyServer.addHandler(new TestHandler(), "/AuthFilterCacheTest/h3-only/"); + h3onlyURI = new URI("https://" + h3onlyServer.serverAuthority() + + "/AuthFilterCacheTest/h3-only/"); + h3onlyServer.start(); + + h3altSvcServer = HttpTestServer.create(ANY, SSLContext.getDefault()); + h3altSvcServer.addHandler(new TestHandler(), "/AuthFilterCacheTest/h3-alt-svc/"); + h3altSvcServer.addHandler(new HttpHeadOrGetHandler(RESPONSE_BODY), + "/AuthFilterCacheTest/h3-alt-svc/direct/head/"); + h3altSvcURI = new URI("https://" + h3altSvcServer.serverAuthority() + + "/AuthFilterCacheTest/h3-alt-svc/"); + h3altSvcServer.start(); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); proxySelector = new HttpProxySelector(proxyAddress); client = newHttpClient(proxySelector, auth); + HttpRequest headRequest = HttpRequest.newBuilder(h3altSvcURI.resolve("direct/head/h2")) + .HEAD() + .version(HTTP_2).build(); + System.out.println("Sending head request: " + headRequest); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), HTTP_2); + System.out.println("Setup: done"); } catch (Exception x) { tearDown(); @@ -177,6 +214,8 @@ public class AuthFilterCacheTest implements HttpServerAdapters { https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + h3onlyServer = stop(h3onlyServer, HttpTestServer::stop); + h3altSvcServer = stop(h3altSvcServer, HttpTestServer::stop); client.close(); virtualExecutor.close(); @@ -229,6 +268,7 @@ public class AuthFilterCacheTest implements HttpServerAdapters { @Override public void handle(HttpTestExchange t) throws IOException { var count = respCounter.incrementAndGet(); + System.out.println("Server got request: " + t.getRequestURI()); System.out.println("Responses handled: " + count); t.getRequestBody().readAllBytes(); @@ -237,15 +277,19 @@ public class AuthFilterCacheTest implements HttpServerAdapters { t.getResponseHeaders() .addHeader("WWW-Authenticate", "Basic realm=\"Earth\""); t.sendResponseHeaders(401, 0); + System.out.println("Server sent 401 for " + t.getRequestURI()); } else { byte[] resp = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8); t.sendResponseHeaders(200, resp.length); + System.out.println("Server sent 200 for " + t.getRequestURI() + "; awaiting barrier"); try { barrier.await(); } catch (Exception e) { + e.printStackTrace(); throw new IOException(e); } t.getResponseBody().write(resp); + System.out.println("Server sent body for " + t.getRequestURI()); } } t.close(); @@ -255,23 +299,54 @@ public class AuthFilterCacheTest implements HttpServerAdapters { void doClient(List uris) { assert uris.size() == URI_COUNT; barrier.reset(); - System.out.println("Client opening connection to: " + uris.toString()); + System.out.println("Client will connect " + REQUEST_COUNT + " times to: " + + uris.stream().map(URI::toString) + .collect(Collectors.joining("\n\t", "\n\t", "\n"))); List>> cfs = new ArrayList<>(); + int count = 0; for (int i = 0; i < REQUEST_COUNT; i++) { for (URI uri : uris) { - HttpRequest req = HttpRequest.newBuilder() - .uri(uri) - .build(); - cfs.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString())); + String uriStr = uri.toString() + (++count); + var builder = HttpRequest.newBuilder() + .uri(URI.create(uriStr)); + var config = uriStr.contains("h3-only") ? HTTP_3_URI_ONLY + : uriStr.contains("h3-alt-svc") ? ALT_SVC + : null; + if (config != null) { + builder = builder.setOption(H3_DISCOVERY, config).version(HTTP_3); + } else { + builder = builder.version(HTTP_2); + } + HttpRequest req = builder.build(); + System.out.printf("Sending request %s (version=%s, config=%s)%n", + req, req.version(), config); + cfs.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()) + .handleAsync((r, t) -> logResponse(req, r, t)) + .thenCompose(Function.identity())); } } CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join(); } + CompletableFuture> logResponse(HttpRequest req, + HttpResponse resp, + Throwable t) { + if (t != null) { + System.out.printf("Request failed: %s (version=%s, config=%s): %s%n", + req, req.version(), req.getOption(H3_DISCOVERY).orElse(null), t); + t.printStackTrace(System.out); + return CompletableFuture.failedFuture(t); + } else { + System.out.printf("Request succeeded: %s (version=%s, config=%s): %s%n", + req, req.version(), req.getOption(H3_DISCOVERY).orElse(null), resp); + return CompletableFuture.completedFuture(resp); + } + } + static final class MyAuthenticator extends Authenticator { - private int count = 0; + private final AtomicInteger count = new AtomicInteger(); MyAuthenticator() { super(); @@ -294,7 +369,7 @@ public class AuthFilterCacheTest implements HttpServerAdapters { PasswordAuthentication passwordAuthentication; int count; synchronized (this) { - count = ++this.count; + count = this.count.incrementAndGet(); passwordAuthentication = super.requestPasswordAuthenticationInstance( host, addr, port, protocol, prompt, scheme, url, reqType); } @@ -304,13 +379,17 @@ public class AuthFilterCacheTest implements HttpServerAdapters { } public int getCount() { - return count; + return count.get(); } } @Test(dataProvider = "uris") public void test(List uris) throws Exception { - System.out.println("Server listening at " + uris.toString()); + System.out.println("Servers listening at " + + uris.stream().map(URI::toString) + .collect(Collectors.joining("\n\t", "\n\t", "\n"))); + System.out.println("h3-alt-svc server listening for h3 at: " + + h3altSvcServer.getH3AltService().map(s -> s.getAddress()).orElse(null)); doClient(uris); } } diff --git a/test/jdk/java/net/httpclient/BasicAuthTest.java b/test/jdk/java/net/httpclient/BasicAuthTest.java index e0aec3b19b7..35d5a7803d6 100644 --- a/test/jdk/java/net/httpclient/BasicAuthTest.java +++ b/test/jdk/java/net/httpclient/BasicAuthTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, 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 @@ -49,6 +49,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.net.ssl.SSLContext; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; public class BasicAuthTest implements HttpServerAdapters { @@ -62,13 +63,15 @@ public class BasicAuthTest implements HttpServerAdapters { test(Version.HTTP_2, false); test(Version.HTTP_1_1, true); test(Version.HTTP_2, true); + test(Version.HTTP_3, true); } public static void test(Version version, boolean secure) throws Exception { ExecutorService e = Executors.newCachedThreadPool(); Handler h = new Handler(); - SSLContext sslContext = secure ? new SimpleSSLContext().get() : null; + SSLContext sslContext = secure || version == Version.HTTP_3 + ? new SimpleSSLContext().get() : null; HttpTestServer server = HttpTestServer.create(version, sslContext, e); HttpTestContext serverContext = server.addHandler(h,"/test/"); ServerAuth sa = new ServerAuth("foo realm"); @@ -77,7 +80,7 @@ public class BasicAuthTest implements HttpServerAdapters { System.out.println("Server auth = " + server.serverAuthority()); ClientAuth ca = new ClientAuth(); - var clientBuilder = HttpClient.newBuilder(); + var clientBuilder = HttpServerAdapters.createClientBuilderForH3(); if (sslContext != null) clientBuilder.sslContext(sslContext); HttpClient client = clientBuilder.authenticator(ca).build(); @@ -85,6 +88,11 @@ public class BasicAuthTest implements HttpServerAdapters { String scheme = sslContext == null ? "http" : "https"; URI uri = new URI(scheme + "://" + server.serverAuthority() + "/test/foo/"+version); var builder = HttpRequest.newBuilder(uri); + if (version == Version.HTTP_3) { + builder.version(version); + var config = server.h3DiscoveryConfig(); + builder.setOption(H3_DISCOVERY, server.h3DiscoveryConfig()); + } HttpRequest req = builder.copy().GET().build(); System.out.println("\n\nSending request: " + req); diff --git a/test/jdk/java/net/httpclient/BasicHTTP2Test.java b/test/jdk/java/net/httpclient/BasicHTTP2Test.java new file mode 100644 index 00000000000..65bcf7631ea --- /dev/null +++ b/test/jdk/java/net/httpclient/BasicHTTP2Test.java @@ -0,0 +1,320 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * ReferenceTracker + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * BasicHTTP2Test + * @summary Basic HTTP/2 test when HTTP/3 is requested + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.DatagramSocket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.lang.System.out; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class BasicHTTP2Test implements HttpServerAdapters { + + SSLContext sslContext; + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + String https2URI; + DatagramSocket udp; + + // a shared executor helps reduce the amount of threads created by the test + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private volatile HttpClient sharedClient; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static final void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.entrySet().forEach((e) -> { + out.printf("\t%s: %s%n", e.getKey(), e.getValue()); + e.getValue().printStackTrace(out); + e.getValue().printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + https2URI, + }; + } + + static AtomicLong URICOUNT = new AtomicLong(); + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = HttpClient.newBuilder() + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + + @Test + public void testH2() throws Exception { + + System.err.println("XXXXX ====== xxxxx first xxxxx ====== XXXXX"); + HttpClient client = makeNewClient(); + URI uri = URI.create(https2URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(Version.HTTP_3) + .GET(); + if (udp == null) { + out.println("Using config " + ALT_SVC); + builder.setOption(H3_DISCOVERY, ALT_SVC); + } + HttpRequest request = builder.build(); + + // send first request: that will go through regular HTTP/2. + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_2, "first response version"); + + Thread.sleep(1000); + + // send second request: we still will not find an endpoint in the + // AltServicesRegistry and will send everything to an + // HTTP/2 connection + System.err.println("XXXXX ====== xxxxx second xxxxx ====== XXXXX"); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_2, "second response version"); + + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + // HTTP/2 + HttpTestHandler handler = new Handler(); + HttpTestHandler h3Handler = new Handler(); + + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); + https2TestServer.addHandler(handler, "/https2/test204/"); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test204/x"; + try { + // attempt to prevent any other server/process to use this port + // for UDP + udp = new DatagramSocket(https2TestServer.getAddress()); + } catch (Exception x) { + System.out.println("Failed to allocate UDP socket at: " + https2TestServer.getAddress()); + udp = null; + } + + serverCount.addAndGet(1); + https2TestServer.start(); + } + + @AfterTest + public void teardown() throws Exception { + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + if (udp != null) udp.close(); + https2TestServer.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + + public Handler() {} + + volatile int invocation = 0; + + @Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + String type = uri.getScheme().toLowerCase(); + InputStream is = t.getRequestBody(); + while (is.read() != -1); + is.close(); + + + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + OutputStream os = t.getResponseBody(); + os.close(); + } else { + System.err.printf("Server sending %d - 0 length\n", 200); + t.sendResponseHeaders(200, 0); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/BasicHTTP3Test.java b/test/jdk/java/net/httpclient/BasicHTTP3Test.java new file mode 100644 index 00000000000..bd31b932b80 --- /dev/null +++ b/test/jdk/java/net/httpclient/BasicHTTP3Test.java @@ -0,0 +1,482 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +import static java.lang.System.out; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; + + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * ReferenceTracker + * jdk.httpclient.test.lib.quic.QuicStandaloneServer + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djavax.net.debug=all + * BasicHTTP3Test + * @summary Basic HTTP/3 test + */ +public class BasicHTTP3Test implements HttpServerAdapters { + + SSLContext sslContext; + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + String https2URI; + HttpTestServer h3TestServer; // HTTP/2 ( h2 + h3) + String h3URI; + HttpTestServer h3qv2TestServer; // HTTP/2 ( h2 + h3 on Quic v2, incompatible nego) + String h3URIQv2; + HttpTestServer h3qv2CTestServer; // HTTP/2 ( h2 + h3 on Quic v2, compatible nego) + String h3URIQv2C; + HttpTestServer h3mtlsTestServer; // HTTP/2 ( h2 + h3), h3 requires client cert + String h3mtlsURI; + HttpTestServer h3TestServerWithRetry; // h3 + String h3URIRetry; + HttpTestServer h3TestServerWithTLSHelloRetry; // h3 + String h3URITLSHelloRetry; + + static final int ITERATION_COUNT = 4; + // a shared executor helps reduce the amount of threads created by the test + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private volatile HttpClient sharedClient; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static final void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d clients%n", + now(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.entrySet().forEach((e) -> { + out.printf("\t%s: %s%n", e.getKey(), e.getValue()); + e.getValue().printStackTrace(out); + e.getValue().printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + https2URI, + h3URI + }; + } + + @DataProvider(name = "variants") + public Object[][] variants(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + String[] uris = uris(); + Object[][] result = new Object[uris.length * 2 * 2][]; + int i = 0; + for (var version : List.of(Optional.empty(), Optional.of(Version.HTTP_3))) { + for (boolean sameClient : List.of(false, true)) { + for (String uri : uris()) { + result[i++] = new Object[]{uri, sameClient, version}; + } + } + } + assert i == uris.length * 2 * 2; + return result; + } + + @DataProvider(name = "h3URIs") + public Object[][] versions(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = { + {h3URI}, {h3URIRetry}, + {h3URIQv2}, {h3URIQv2C}, + {h3mtlsURI}, {h3URITLSHelloRetry}, + }; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + @Test(dataProvider = "variants") + public void test(String uri, boolean sameClient, Optional version) throws Exception { + System.out.println("Request to " + uri); + + HttpClient client = newHttpClient(sameClient); + + Builder builder = HttpRequest.newBuilder(URI.create(uri)) + .GET(); + version.ifPresent(builder::version); + for (int i = 0; i < ITERATION_COUNT; i++) { + // don't want to attempt direct connection as there could be another + // HTTP/3 endpoint listening at the URI port. + // sameClient should be fine because version.empty() should + // have come first and populated alt-services. + builder.setOption(H3_DISCOVERY, ALT_SVC); + HttpRequest request = builder.build(); + System.out.println("Iteration: " + i); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response: " + response); + out.println("Version: " + response.version()); + int expectedResponse = 200; + if (response.statusCode() != expectedResponse) + throw new RuntimeException("wrong response code " + response.statusCode()); + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + System.out.println("test: DONE"); + } + + @Test(dataProvider = "h3URIs") + public void testH3(final String h3URI) throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_2) + .GET(); + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_2, "first response version"); + + request = builder.version(Version.HTTP_3).build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), Version.HTTP_3, "second response version"); + + if (h3URI == h3mtlsURI) { + assertNotNull(response.sslSession().get().getLocalCertificates()); + } else { + assertNull(response.sslSession().get().getLocalCertificates()); + } + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + // verify that the client handles HTTP/3 reset stream correctly + @Test + public void testH3Reset() throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_2) + .GET(); + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_2, "first response version"); + + // instruct the server side handler to throw an exception + // that then causes the test server to reset the stream + final String resetCausingURI = h3URI + "?handlerShouldThrow=true"; + builder = HttpRequest.newBuilder(URI.create(resetCausingURI)) + .GET(); + request = builder.version(Version.HTTP_3) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + try { + response = client.send(request, BodyHandlers.ofString()); + throw new RuntimeException("Unexpectedly received a response instead of an exception," + + " response: " + response); + } catch (IOException e) { + final String msg = e.getMessage(); + if (msg == null || !msg.contains("reset by peer")) { + // unexpected message in the exception, propagate the exception + throw e; + } + } + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); + https2TestServer.addHandler(new Handler(), "/https2/test/"); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test/x"; + + // A HTTP2 server with H3 enabled on a different host:port than the HTTP2 server + h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + final HttpTestHandler h3Handler = new Handler(); + h3TestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/h3"; + assertTrue(h3TestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 server with H3 QUICv2 enabled on a different host:port than the HTTP2 server + final Http2TestServer h2q2Server = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnEphemeralPortWithVersion(QuicVersion.QUIC_V2, false); + h3qv2TestServer = HttpTestServer.of(h2q2Server); + h3qv2TestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URIQv2 = "https://" + h3qv2TestServer.serverAuthority() + "/h3/testH3/h3qv2";; + assertTrue(h3qv2TestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 server with H3 QUICv2 compatible negotiation enabled on a different host:port than the HTTP2 server + final Http2TestServer h2q2CServer = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnEphemeralPortWithVersion(QuicVersion.QUIC_V2, true); + h3qv2CTestServer = HttpTestServer.of(h2q2CServer); + h3qv2CTestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URIQv2C = "https://" + h3qv2CTestServer.serverAuthority() + "/h3/testH3/h3qv2c";; + assertTrue(h3qv2CTestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 server with H3 enabled on a different host:port than the HTTP2 server + // H3 server requires the client to authenticate with a certificate + h3mtlsTestServer = HttpTestServer.create(HTTP_3, sslContext); + h3mtlsTestServer.addHandler(h3Handler, "/h3/testH3/"); + h3mtlsTestServer.getH3AltService().get().getQuicServer().setNeedClientAuth(true); + h3mtlsURI = "https://" + h3mtlsTestServer.serverAuthority() + "/h3/testH3/h3mtls"; + assertTrue(h3mtlsTestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 test server with H3 alt service listening on different host:port + // and the underlying quic server for H3 is configured to send a RETRY packet + final Http2TestServer h2Server = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnEphemeralPort(); + // configure send retry on QUIC server + h2Server.getH3AltService().get().getQuicServer().sendRetry(true); + h3TestServerWithRetry = HttpTestServer.of(h2Server); + h3TestServerWithRetry.addHandler(h3Handler, "/h3/testH3Retry/"); + h3URIRetry = "https://" + h3TestServerWithRetry.serverAuthority() + "/h3/testH3Retry/x"; + + // A HTTP2 server with H3 enabled on a different host:port than the HTTP2 server + // TLS server rejects X25519 and secp256r1 key shares, + // which forces a hello retry at the moment of writing this test. + h3TestServerWithTLSHelloRetry = HttpTestServer.create(HTTP_3, sslContext); + h3TestServerWithTLSHelloRetry.addHandler(h3Handler, "/h3/testH3tlsretry/"); + h3TestServerWithTLSHelloRetry.getH3AltService().get().getQuicServer().setRejectKeyAgreement(Set.of("x25519", "secp256r1")); + h3URITLSHelloRetry = "https://" + h3TestServerWithTLSHelloRetry.serverAuthority() + "/h3/testH3tlsretry/x"; + assertTrue(h3TestServerWithTLSHelloRetry.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + https2TestServer.start(); + h3TestServer.start(); + h3qv2TestServer.start(); + h3qv2CTestServer.start(); + h3mtlsTestServer.start(); + h3TestServerWithRetry.start(); + h3TestServerWithTLSHelloRetry.start(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + https2TestServer.stop(); + h3TestServer.stop(); + h3qv2CTestServer.stop(); + h3qv2TestServer.stop(); + h3mtlsTestServer.stop(); + h3TestServerWithRetry.stop(); + h3TestServerWithTLSHelloRetry.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + final String query = uri.getQuery(); + if (query != null && query.contains("handlerShouldThrow=true")) { + System.err.printf("intentionally throwing an exception for request %s\n", uri); + throw new RuntimeException("intentionally thrown by handler for request " + uri); + } + try (InputStream is = t.getRequestBody()) { + is.readAllBytes(); + } + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + OutputStream os = t.getResponseBody(); + os.close(); + } else { + System.err.printf("Server sending %d - 0 length\n", 200); + t.sendResponseHeaders(200, 0); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/BasicRedirectTest.java b/test/jdk/java/net/httpclient/BasicRedirectTest.java index 8ea1653b4d0..a19b1444ac6 100644 --- a/test/jdk/java/net/httpclient/BasicRedirectTest.java +++ b/test/jdk/java/net/httpclient/BasicRedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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,25 +28,25 @@ * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext * @run testng/othervm * -Djdk.httpclient.HttpClient.log=trace,headers,requests + * -Djdk.internal.httpclient.debug=true * BasicRedirectTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import java.net.http.HttpResponse.BodyHandlers; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -56,11 +56,16 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; + public class BasicRedirectTest implements HttpServerAdapters { SSLContext sslContext; @@ -68,14 +73,21 @@ public class BasicRedirectTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; - String httpURIToMoreSecure; // redirects HTTP to HTTPS + String httpURIToMoreSecure; // redirects HTTP to HTTPS + String httpURIToH3MoreSecure; // redirects HTTP to HTTPS/3 String httpsURI; String httpsURIToLessSecure; // redirects HTTPS to HTTP String http2URI; String http2URIToMoreSecure; // redirects HTTP to HTTPS + String http2URIToH3MoreSecure; // redirects HTTP to HTTPS/3 String https2URI; String https2URIToLessSecure; // redirects HTTPS to HTTP + String https3URI; + String https3HeadURI; + String http3URIToLessSecure; // redirects HTTP3 to HTTP + String http3URIToH2cLessSecure; // redirects HTTP3 to h2c static final String MESSAGE = "Is fearr Gaeilge briste, na Bearla cliste"; static final int ITERATIONS = 3; @@ -83,31 +95,67 @@ public class BasicRedirectTest implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, Redirect.ALWAYS }, - { httpsURI, Redirect.ALWAYS }, - { http2URI, Redirect.ALWAYS }, - { https2URI, Redirect.ALWAYS }, - { httpURIToMoreSecure, Redirect.ALWAYS }, - { http2URIToMoreSecure, Redirect.ALWAYS }, - { httpsURIToLessSecure, Redirect.ALWAYS }, - { https2URIToLessSecure, Redirect.ALWAYS }, + { httpURI, Redirect.ALWAYS, Optional.empty() }, + { httpsURI, Redirect.ALWAYS, Optional.empty() }, + { http2URI, Redirect.ALWAYS, Optional.empty() }, + { https2URI, Redirect.ALWAYS, Optional.empty() }, + { https3URI, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { httpURIToMoreSecure, Redirect.ALWAYS, Optional.empty() }, + { httpURIToH3MoreSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { http2URIToMoreSecure, Redirect.ALWAYS, Optional.empty() }, + { http2URIToH3MoreSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { httpsURIToLessSecure, Redirect.ALWAYS, Optional.empty() }, + { https2URIToLessSecure, Redirect.ALWAYS, Optional.empty() }, + { http3URIToLessSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { http3URIToH2cLessSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, - { httpURI, Redirect.NORMAL }, - { httpsURI, Redirect.NORMAL }, - { http2URI, Redirect.NORMAL }, - { https2URI, Redirect.NORMAL }, - { httpURIToMoreSecure, Redirect.NORMAL }, - { http2URIToMoreSecure, Redirect.NORMAL }, + { httpURI, Redirect.NORMAL, Optional.empty() }, + { httpsURI, Redirect.NORMAL, Optional.empty() }, + { http2URI, Redirect.NORMAL, Optional.empty() }, + { https2URI, Redirect.NORMAL, Optional.empty() }, + { https3URI, Redirect.NORMAL, Optional.of(HTTP_3) }, + { httpURIToMoreSecure, Redirect.NORMAL, Optional.empty() }, + { http2URIToMoreSecure, Redirect.NORMAL, Optional.empty() }, + { httpURIToH3MoreSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, + { http2URIToH3MoreSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, }; } - @Test(dataProvider = "positive") - void test(String uriString, Redirect redirectPolicy) throws Exception { - out.printf("%n---- starting positive (%s, %s) ----%n", uriString, redirectPolicy); - HttpClient client = HttpClient.newBuilder() + HttpClient createClient(Redirect redirectPolicy, Optional version) throws Exception { + var clientBuilder = newClientBuilderForH3() .followRedirects(redirectPolicy) - .sslContext(sslContext) + .sslContext(sslContext); + HttpClient client = version.map(clientBuilder::version) + .orElse(clientBuilder) .build(); + if (version.stream().anyMatch(HTTP_3::equals)) { + var builder = HttpRequest.newBuilder(URI.create(https3HeadURI)) + .setOption(H3_DISCOVERY, ALT_SVC); + var head = builder.copy().HEAD().version(HTTP_2).build(); + var get = builder.copy().GET().build(); + out.printf("%n---- sending initial head request (%s) -----%n", head.uri()); + var resp = client.send(head, BodyHandlers.ofString()); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.version(), HTTP_2); + out.println("HEADERS: " + resp.headers()); + var length = resp.headers().firstValueAsLong("Content-Length") + .orElseThrow(AssertionError::new); + if (length < 0) throw new AssertionError("negative length " + length); + out.printf("%n---- sending initial HTTP/3 GET request (%s) -----%n", get.uri()); + resp = client.send(get, BodyHandlers.ofString()); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.version(), HTTP_3); + assertEquals(resp.body().getBytes(UTF_8).length, length, + "body \"" + resp.body() + "\": "); + } + return client; + } + + @Test(dataProvider = "positive") + void test(String uriString, Redirect redirectPolicy, Optional clientVersion) throws Exception { + out.printf("%n---- starting positive (%s, %s, %s) ----%n", uriString, redirectPolicy, + clientVersion.map(Version::name).orElse("empty")); + HttpClient client = createClient(redirectPolicy, clientVersion); URI uri = URI.create(uriString); HttpRequest request = HttpRequest.newBuilder(uri).build(); @@ -125,20 +173,24 @@ public class BasicRedirectTest implements HttpServerAdapters { assertEquals(response.body(), MESSAGE); // asserts redirected URI in response.request().uri() assertTrue(response.uri().getPath().endsWith("message")); - assertPreviousRedirectResponses(request, response); + assertPreviousRedirectResponses(request, response, clientVersion); } } static void assertPreviousRedirectResponses(HttpRequest initialRequest, - HttpResponse finalResponse) { + HttpResponse finalResponse, + Optional clientVersion) { // there must be at least one previous response finalResponse.previousResponse() .orElseThrow(() -> new RuntimeException("no previous response")); HttpResponse response = finalResponse; + List versions = new ArrayList<>(); + versions.add(response.version()); do { URI uri = response.uri(); response = response.previousResponse().get(); + versions.add(response.version()); assertTrue(300 <= response.statusCode() && response.statusCode() <= 309, "Expected 300 <= code <= 309, got:" + response.statusCode()); assertEquals(response.body(), null, "Unexpected body: " + response.body()); @@ -153,6 +205,11 @@ public class BasicRedirectTest implements HttpServerAdapters { assertEquals(initialRequest, response.request(), String.format("Expected initial request [%s] to equal last prev req [%s]", initialRequest, response.request())); + if (clientVersion.stream().anyMatch(HTTP_3::equals)) { + out.println(versions.stream().map(Version::name) + .collect(Collectors.joining(" <-- ", "Redirects: ", ";"))); + assertTrue(versions.stream().anyMatch(HTTP_3::equals), "at least one version should be HTTP/3"); + } } // -- negatives @@ -160,27 +217,33 @@ public class BasicRedirectTest implements HttpServerAdapters { @DataProvider(name = "negative") public Object[][] negative() { return new Object[][] { - { httpURI, Redirect.NEVER }, - { httpsURI, Redirect.NEVER }, - { http2URI, Redirect.NEVER }, - { https2URI, Redirect.NEVER }, - { httpURIToMoreSecure, Redirect.NEVER }, - { http2URIToMoreSecure, Redirect.NEVER }, - { httpsURIToLessSecure, Redirect.NEVER }, - { https2URIToLessSecure, Redirect.NEVER }, + { httpURI, Redirect.NEVER, Optional.empty() }, + { httpsURI, Redirect.NEVER, Optional.empty() }, + { http2URI, Redirect.NEVER, Optional.empty() }, + { https2URI, Redirect.NEVER, Optional.empty() }, + { https3URI, Redirect.NEVER, Optional.of(HTTP_3) }, + { httpURIToMoreSecure, Redirect.NEVER, Optional.empty() }, + { http2URIToMoreSecure, Redirect.NEVER, Optional.empty() }, + { httpURIToH3MoreSecure, Redirect.NEVER, Optional.of(HTTP_3) }, + { http2URIToH3MoreSecure, Redirect.NEVER, Optional.of(HTTP_3) }, + { httpsURIToLessSecure, Redirect.NEVER, Optional.empty() }, + { https2URIToLessSecure, Redirect.NEVER, Optional.empty() }, + { http3URIToLessSecure, Redirect.NEVER, Optional.of(HTTP_3) }, + { http3URIToH2cLessSecure, Redirect.NEVER, Optional.of(HTTP_3) }, - { httpsURIToLessSecure, Redirect.NORMAL }, - { https2URIToLessSecure, Redirect.NORMAL }, + { httpsURIToLessSecure, Redirect.NORMAL, Optional.empty() }, + { https2URIToLessSecure, Redirect.NORMAL, Optional.empty() }, + { http3URIToLessSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, + { http3URIToH2cLessSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, }; } @Test(dataProvider = "negative") - void testNegatives(String uriString,Redirect redirectPolicy) throws Exception { - out.printf("%n---- starting negative (%s, %s) ----%n", uriString, redirectPolicy); - HttpClient client = HttpClient.newBuilder() - .followRedirects(redirectPolicy) - .sslContext(sslContext) - .build(); + void testNegatives(String uriString, Redirect redirectPolicy, Optional clientVersion) + throws Exception { + out.printf("%n---- starting negative (%s, %s, %s) ----%n", uriString, redirectPolicy, + clientVersion.map(Version::name).orElse("empty")); + HttpClient client = createClient(redirectPolicy, clientVersion); URI uri = URI.create(uriString); HttpRequest request = HttpRequest.newBuilder(uri).build(); @@ -225,13 +288,25 @@ public class BasicRedirectTest implements HttpServerAdapters { https2TestServer.addHandler(new BasicHttpRedirectHandler(), "/https2/same/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/same/redirect"; + http3TestServer = HttpTestServer.create(ANY, sslContext); + http3TestServer.addHandler(new BasicHttpRedirectHandler(), "/http3/same/"); + https3URI = "https://" + http3TestServer.serverAuthority() + "/http3/same/redirect"; + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + https3HeadURI = "https://" + http3TestServer.serverAuthority() + "/http3/head"; + // HTTP to HTTPS redirect handler httpTestServer.addHandler(new ToSecureHttpRedirectHandler(httpsURI), "/http1/toSecure/"); httpURIToMoreSecure = "http://" + httpTestServer.serverAuthority()+ "/http1/toSecure/redirect"; + // HTTP to HTTP/3 redirect handler + httpTestServer.addHandler(new ToSecureHttpRedirectHandler(https3URI), "/http1/toSecureH3/"); + httpURIToH3MoreSecure = "http://" + httpTestServer.serverAuthority()+ "/http1/toSecureH3/redirect"; // HTTP2 to HTTP2S redirect handler http2TestServer.addHandler(new ToSecureHttpRedirectHandler(https2URI), "/http2/toSecure/"); http2URIToMoreSecure = "http://" + http2TestServer.serverAuthority() + "/http2/toSecure/redirect"; + // HTTP2 to HTTP2S redirect handler + http2TestServer.addHandler(new ToSecureHttpRedirectHandler(https3URI), "/http2/toSecureH3/"); + http2URIToH3MoreSecure = "http://" + http2TestServer.serverAuthority() + "/http2/toSecureH3/redirect"; // HTTPS to HTTP redirect handler httpsTestServer.addHandler(new ToLessSecureRedirectHandler(httpURI), "/https1/toLessSecure/"); @@ -239,11 +314,19 @@ public class BasicRedirectTest implements HttpServerAdapters { // HTTPS2 to HTTP2 redirect handler https2TestServer.addHandler(new ToLessSecureRedirectHandler(http2URI), "/https2/toLessSecure/"); https2URIToLessSecure = "https://" + https2TestServer.serverAuthority() + "/https2/toLessSecure/redirect"; + // HTTP3 to HTTP redirect handler + http3TestServer.addHandler(new ToLessSecureRedirectHandler(httpURI), "/http3/toLessSecure/"); + http3URIToLessSecure = "https://" + http3TestServer.serverAuthority() + "/http3/toLessSecure/redirect"; + // HTTP3 to HTTP2 redirect handler + http3TestServer.addHandler(new ToLessSecureRedirectHandler(http2URI), "/http3/toLessSecureH2/"); + http3URIToH2cLessSecure = "https://" + http3TestServer.serverAuthority() + "/http3/toLessSecureH2/redirect"; httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + createClient(Redirect.NEVER, Optional.of(HTTP_3)); } @AfterTest diff --git a/test/jdk/java/net/httpclient/CancelRequestTest.java b/test/jdk/java/net/httpclient/CancelRequestTest.java index 7851b112498..bfc1eff9cf9 100644 --- a/test/jdk/java/net/httpclient/CancelRequestTest.java +++ b/test/jdk/java/net/httpclient/CancelRequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, 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 @@ -49,6 +49,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -57,6 +58,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -79,8 +81,9 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.System.out; import static java.lang.System.err; -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.*; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -95,10 +98,15 @@ public class CancelRequestTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final long SERVER_LATENCY = 75; static final int MAX_CLIENT_DELAY = 75; @@ -200,6 +208,8 @@ public class CancelRequestTest implements HttpServerAdapters { httpsURI, http2URI, https2URI, + h2h3URI, + h3URI, }; } @@ -245,7 +255,7 @@ public class CancelRequestTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -265,6 +275,17 @@ public class CancelRequestTest implements HttpServerAdapters { } } + // set HTTP/3 version on the request when targeting + // an HTTP/3 server + private HttpRequest.Builder requestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (uri.contains("h3")) { + builder.version(HTTP_3); + } + return builder; + } + + final static String BODY = "Some string | that ? can | be split ? several | ways."; // should accept SSLHandshakeException because of the connectionAborter @@ -273,8 +294,12 @@ public class CancelRequestTest implements HttpServerAdapters { // rewrap in "Request Cancelled" when the multi exchange was aborted... private static boolean isCancelled(Throwable t) { while (t instanceof ExecutionException) t = t.getCause(); - if (t instanceof CancellationException) return true; - if (t instanceof IOException) return String.valueOf(t).contains("Request cancelled"); + Throwable cause = t; + while (cause != null) { + if (cause instanceof CancellationException) return true; + if (cause instanceof IOException && String.valueOf(cause).contains("Request cancelled")) return true; + cause = cause.getCause(); + } out.println("Not a cancellation exception: " + t); t.printStackTrace(out); return false; @@ -290,6 +315,15 @@ public class CancelRequestTest implements HttpServerAdapters { } } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @Test(dataProvider = "asyncurls") public void testGetSendAsync(String uri, boolean sameClient, boolean mayInterruptIfRunning) throws Exception { @@ -302,14 +336,19 @@ public class CancelRequestTest implements HttpServerAdapters { client = newHttpClient(sameClient); Tracker tracker = TRACKER.getTracker(client); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + // Populate alt-svc registry with h3 service + if (uri.contains("h2h3")) headRequest(client); + Http3DiscoveryMode config = uri.contains("h3-only") ? h3TestServer.h3DiscoveryConfig() : null; + HttpRequest req = requestBuilder(uri) .GET() + .setOption(H3_DISCOVERY, config) .build(); BodyHandler handler = BodyHandlers.ofString(); CountDownLatch latch = new CountDownLatch(1); CompletableFuture> response = client.sendAsync(req, handler); var cf1 = response.whenComplete((r,t) -> System.out.println(t)); CompletableFuture> cf2 = cf1.whenComplete((r,t) -> latch.countDown()); + out.println("iteration: " + i + ", req: " + req.uri()); out.println("response: " + response); out.println("cf1: " + cf1); out.println("cf2: " + cf2); @@ -352,10 +391,12 @@ public class CancelRequestTest implements HttpServerAdapters { Throwable wrapped = x.getCause(); Throwable cause = wrapped; if (mayInterruptIfRunning) { - assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass()), - "Unexpected exception: " + wrapped); - cause = wrapped.getCause(); - out.println("CancellationException cause: " + x); + if (CancellationException.class.isAssignableFrom(wrapped.getClass())) { + cause = wrapped.getCause(); + out.println("CancellationException cause: " + x); + } else if (!isCancelled(cause)) { + throw new RuntimeException("Unexpected cause: " + cause); + } if (cause instanceof HttpConnectTimeoutException) { cause.printStackTrace(out); throw new RuntimeException("Unexpected timeout exception", cause); @@ -426,8 +467,12 @@ public class CancelRequestTest implements HttpServerAdapters { } }; - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + // Populate alt-svc registry with h3 service + if (uri.contains("h2h3")) headRequest(client); + Http3DiscoveryMode config = uri.contains("h3-only") ? h3TestServer.h3DiscoveryConfig() : null; + HttpRequest req = requestBuilder(uri) .POST(HttpRequest.BodyPublishers.ofByteArrays(iterable)) + .setOption(H3_DISCOVERY, config) .build(); BodyHandler handler = BodyHandlers.ofString(); CountDownLatch latch = new CountDownLatch(1); @@ -473,8 +518,13 @@ public class CancelRequestTest implements HttpServerAdapters { } catch (ExecutionException x) { assertTrue(response.isDone()); Throwable wrapped = x.getCause(); - assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass())); - Throwable cause = wrapped.getCause(); + Throwable cause = wrapped; + if (CancellationException.class.isAssignableFrom(wrapped.getClass())) { + cause = wrapped.getCause(); + out.println("CancellationException cause: " + x); + } else if (!isCancelled(cause)) { + throw new RuntimeException("Unexpected cause: " + cause); + } assertTrue(IOException.class.isAssignableFrom(cause.getClass())); if (cause instanceof HttpConnectTimeoutException) { cause.printStackTrace(out); @@ -536,8 +586,12 @@ public class CancelRequestTest implements HttpServerAdapters { return List.of(BODY.getBytes(UTF_8)).iterator(); }; - HttpRequest req = HttpRequest.newBuilder(URI.create(uriStr)) + // Populate alt-svc registry with h3 service + if (uri.contains("h2h3")) headRequest(client); + Http3DiscoveryMode config = uri.contains("h3-only") ? h3TestServer.h3DiscoveryConfig() : null; + HttpRequest req = requestBuilder(uriStr) .POST(HttpRequest.BodyPublishers.ofByteArrays(iterable)) + .setOption(H3_DISCOVERY, config) .build(); String body = null; Exception failed = null; @@ -613,11 +667,24 @@ public class CancelRequestTest implements HttpServerAdapters { https2TestServer.addHandler(h2_chunkedHandler, "/https2/x/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x/"; - serverCount.addAndGet(4); + HttpTestHandler h3_chunkedHandler = new HTTPSlowHandler(); + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(h3_chunkedHandler, "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(h3_chunkedHandler, "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + + serverCount.addAndGet(6); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -632,6 +699,8 @@ public class CancelRequestTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java b/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java index e0199b18e68..e304168ba33 100644 --- a/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java +++ b/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, 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 @@ -32,11 +32,6 @@ * @run testng/othervm -Djdk.internal.httpclient.debug=true * CancelStreamedBodyTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; -import jdk.internal.net.http.common.OperationTrackers.Tracker; -import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -53,25 +48,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.Reference; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import java.util.Arrays; -import java.util.Iterator; import java.util.List; -import java.util.Random; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; @@ -79,12 +64,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import static java.lang.System.arraycopy; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -97,10 +83,12 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String https3URI; static final long SERVER_LATENCY = 75; static final int ITERATION_COUNT = 3; @@ -198,6 +186,7 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { private String[] uris() { return new String[] { + https3URI, httpURI, httpsURI, http2URI, @@ -221,9 +210,9 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { return result; } - private HttpClient makeNewClient() { + private HttpClient makeNewClient(HttpClient.Builder builder) { clientCount.incrementAndGet(); - var client = HttpClient.newBuilder() + var client = builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -236,21 +225,45 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { return TRACKER.track(client); } - HttpClient newHttpClient(boolean share) { - if (!share) return makeNewClient(); + private Version version(String uri) { + if (uri == null) return null; + if (uri.contains("/http3/")) return HTTP_3; + if (uri.contains("/http2/")) return HTTP_2; + if (uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http1/")) return HTTP_1_1; + if (uri.contains("/https1/")) return HTTP_1_1; + return null; + } + + HttpClient makeNewClient(Version version) { + var builder = (version == HTTP_3) + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return makeNewClient(builder); + } + + HttpClient newHttpClient(boolean share, String uri) { + if (!share) return makeNewClient(version(uri)); HttpClient shared = sharedClient; if (shared != null) return shared; synchronized (this) { shared = sharedClient; if (shared == null) { - shared = sharedClient = makeNewClient(); + shared = sharedClient = makeNewClient(HTTP_3); } return shared; } } - final static String BODY = "Some string |\n that ?\n can |\n be split ?\n several |\n ways."; + HttpRequest.Builder requestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + var version = version(uri); + return version == HTTP_3 + ? builder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + : builder; + } + final static String BODY = "Some string |\n that ?\n can |\n be split ?\n several |\n ways."; @Test(dataProvider = "urls") public void testAsLines(String uri, boolean sameClient) @@ -261,10 +274,10 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { out.printf("%n%s testAsLines(%s, %b)%n", now(), uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(sameClient); + client = newHttpClient(sameClient, uri); var tracker = TRACKER.getTracker(client); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = requestBuilder(uri) .GET() .build(); List lines; @@ -302,10 +315,10 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { out.printf("%n%s testInputStream(%s, %b)%n", now(), uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(sameClient); + client = newHttpClient(sameClient, uri); var tracker = TRACKER.getTracker(client); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = requestBuilder(uri) .GET() .build(); int read = -1; @@ -318,7 +331,7 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { } // Only check our still alive client for outstanding operations // and outstanding subscribers here: it should have none. - var error = TRACKER.check(tracker, 1, + var error = TRACKER.check(tracker, 500, (t) -> t.getOutstandingOperations() > 0 || t.getOutstandingSubscribers() > 0, "subscribers for testInputStream(%s)\n\t step [%s,%s]".formatted(req.uri(), i,j), false); @@ -364,11 +377,16 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { https2TestServer.addHandler(h2_chunkedHandler, "/https2/x/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x/"; - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h2_chunkedHandler, "/http3/x/"); + https3URI = "https://" + http3TestServer.serverAuthority() + "/http3/x/"; + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -386,6 +404,7 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/http2/ExpectContinueResetTest.java b/test/jdk/java/net/httpclient/CancelledPartialResponseTest.java similarity index 52% rename from test/jdk/java/net/httpclient/http2/ExpectContinueResetTest.java rename to test/jdk/java/net/httpclient/CancelledPartialResponseTest.java index 5d09c01b96f..41493bbdaff 100644 --- a/test/jdk/java/net/httpclient/http2/ExpectContinueResetTest.java +++ b/test/jdk/java/net/httpclient/CancelledPartialResponseTest.java @@ -23,16 +23,19 @@ /* * @test - * @summary Verifies that the client reacts correctly to receiving RST_STREAM at various stages of - * a Partial Response. + * @summary Verifies that the client reacts correctly to receiving RST_STREAM or StopSendingFrame at various stages of + * a Partial/Expect-continue type Response for HTTP/2 and HTTP/3. * @bug 8309118 * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters - * @run testng/othervm/timeout=40 -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=trace,errors,headers - * ExpectContinueResetTest + * @run testng/othervm/timeout=40 -Djdk.internal.httpclient.debug=false -Djdk.httpclient.HttpClient.log=trace,errors,headers + * CancelledPartialResponseTest */ import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.http2.BodyOutputStream; import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.Http2TestExchange; @@ -42,12 +45,15 @@ import jdk.httpclient.test.lib.http2.Http2TestServerConnection; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.ResetFrame; +import jdk.internal.net.http.http3.Http3Error; +import jdk.test.lib.net.SimpleSSLContext; import org.testng.TestException; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import java.io.IOException; import java.io.InputStream; @@ -55,47 +61,58 @@ import java.io.OutputStream; import java.io.PrintStream; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.util.Iterator; import java.util.concurrent.ExecutionException; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.testng.Assert.*; -public class ExpectContinueResetTest { +public class CancelledPartialResponseTest { Http2TestServer http2TestServer; + + HttpTestServer http3TestServer; + // "NoError" urls complete with an exception. "NoError" or "Error" here refers to the error code in the RST_STREAM frame // and not the outcome of the test. - URI warmup, partialResponseResetNoError, partialResponseResetError, fullResponseResetNoError, fullResponseResetError; + URI warmup, h2PartialResponseResetNoError, h2PartialResponseResetError, h2FullResponseResetNoError, h2FullResponseResetError; + URI h3PartialResponseStopSending, h3FullResponseStopSending; + + SSLContext sslContext; static PrintStream err = new PrintStream(System.err); static PrintStream out = System.out; + // TODO: Investigate further if checking against HTTP/3 Full Response is necessary @DataProvider(name = "testData") public Object[][] testData() { - // Not consuming the InputStream in the server's handler results in different handling of RST_STREAM client-side return new Object[][] { - { partialResponseResetNoError }, - { partialResponseResetError }, // Checks RST_STREAM is processed if client sees no END_STREAM - { fullResponseResetNoError }, - { fullResponseResetError } + { HTTP_2, h2PartialResponseResetNoError }, + { HTTP_2, h2PartialResponseResetError }, // Checks RST_STREAM is processed if client sees no END_STREAM + { HTTP_2, h2FullResponseResetNoError }, + { HTTP_2, h2FullResponseResetError }, + { HTTP_3, h3PartialResponseStopSending }, // All StopSending frames received by client throw exception regardless of code + { HTTP_3, h3FullResponseStopSending } }; } @Test(dataProvider = "testData") - public void test(URI uri) { - out.printf("\nTesting with Version: %s, URI: %s\n", HTTP_2, uri.toASCIIString()); - err.printf("\nTesting with Version: %s, URI: %s\n", HTTP_2, uri.toASCIIString()); + public void test(Version version, URI uri) { + out.printf("\nTesting with Version: %s, URI: %s\n", version, uri.toASCIIString()); + err.printf("\nTesting with Version: %s, URI: %s\n", version, uri.toASCIIString()); Iterable iterable = EndlessDataChunks::new; HttpRequest.BodyPublisher testPub = HttpRequest.BodyPublishers.ofByteArrays(iterable); Exception expectedException = null; try { - performRequest(testPub, uri); + performRequest(version, testPub, uri); throw new AssertionError("Expected exception not raised for " + uri); } catch (Exception e) { expectedException = e; @@ -105,20 +122,37 @@ public class ExpectContinueResetTest { throw new AssertionError("Unexpected null cause for " + expectedException, expectedException); } - assertEquals(testThrowable.getClass(), IOException.class, - "Test should have closed with an IOException"); - testThrowable.printStackTrace(); + if (!(testThrowable instanceof IOException)) { + throw new AssertionError( + "Test should have closed with an IOException, got: " + testThrowable, + testThrowable); + } + if (version == HTTP_3) { + if (testThrowable.getMessage().contains(Http3Error.H3_EXCESSIVE_LOAD.name())) { + System.out.println("Got expected message: " + testThrowable.getMessage()); + } else { + throw new AssertionError("Expected " + Http3Error.H3_EXCESSIVE_LOAD.name() + + ", got " + testThrowable, testThrowable); + } + } } static public class EndlessDataChunks implements Iterator { - byte[] data = new byte[16]; + byte[] data = new byte[32]; + boolean hasNext = true; @Override public boolean hasNext() { - return true; + return hasNext; } @Override public byte[] next() { + try { + Thread.sleep(2500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + hasNext = false; return data; } @Override @@ -129,20 +163,32 @@ public class ExpectContinueResetTest { @BeforeTest public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + http2TestServer = new Http2TestServer(false, 0); + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http2TestServer.setExchangeSupplier(ExpectContinueResetTestExchangeImpl::new); http2TestServer.addHandler(new GetHandler().toHttp2Handler(), "/warmup"); - http2TestServer.addHandler(new NoEndStreamOnPartialResponse(), "/partialResponse/codeNoError"); - http2TestServer.addHandler(new NoEndStreamOnPartialResponse(), "/partialResponse/codeError"); - http2TestServer.addHandler(new NoEndStreamOnFullResponse(), "/fullResponse/codeNoError"); - http2TestServer.addHandler(new NoEndStreamOnFullResponse(), "/fullResponse/codeError"); + http2TestServer.addHandler(new PartialResponseResetStreamH2(), "/partialResponse/codeNoError"); + http2TestServer.addHandler(new PartialResponseResetStreamH2(), "/partialResponse/codeError"); + http2TestServer.addHandler(new FullResponseResetStreamH2(), "/fullResponse/codeNoError"); + http2TestServer.addHandler(new FullResponseResetStreamH2(), "/fullResponse/codeError"); + http3TestServer.addHandler(new PartialResponseStopSendingH3(), "/partialResponse/codeNoError"); + http3TestServer.addHandler(new FullResponseStopSendingH3(), "/fullResponse/codeNoError"); warmup = URI.create("http://" + http2TestServer.serverAuthority() + "/warmup"); - partialResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeNoError"); - partialResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeError"); - fullResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeNoError"); - fullResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeError"); + h2PartialResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeNoError"); + h2PartialResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeError"); + h2FullResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeNoError"); + h2FullResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeError"); + h3PartialResponseStopSending = URI.create("https://" + http3TestServer.serverAuthority() + "/partialResponse/codeNoError"); + h3FullResponseStopSending = URI.create("https://" + http3TestServer.serverAuthority() + "/fullResponse/codeNoError"); + http2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -150,26 +196,38 @@ public class ExpectContinueResetTest { http2TestServer.stop(); } - private void performRequest(HttpRequest.BodyPublisher bodyPublisher, URI uri) + private void performRequest(Version version, HttpRequest.BodyPublisher bodyPublisher, URI uri) throws IOException, InterruptedException, ExecutionException { - try (HttpClient client = HttpClient.newBuilder().proxy(HttpClient.Builder.NO_PROXY).version(HTTP_2).build()) { + + HttpClient.Builder builder = HttpServerAdapters.createClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(version) + .sslContext(sslContext); + Http3DiscoveryMode requestConfig = null; + if (version == HTTP_3) + requestConfig = Http3DiscoveryMode.HTTP_3_URI_ONLY; + + try (HttpClient client = builder.build()) { err.printf("Performing warmup request to %s", warmup); - client.send(HttpRequest.newBuilder(warmup).GET().version(HTTP_2).build(), HttpResponse.BodyHandlers.discarding()); + if (version == HTTP_2) + client.send(HttpRequest.newBuilder(warmup).GET().version(HTTP_2).build(), + HttpResponse.BodyHandlers.discarding()); + HttpRequest postRequest = HttpRequest.newBuilder(uri) - .version(HTTP_2) + .version(version) .POST(bodyPublisher) + .setOption(H3_DISCOVERY, requestConfig) .expectContinue(true) .build(); - err.printf("Sending request (%s): %s%n", HTTP_2, postRequest); - // TODO: when test is stable and complete, see then if fromSubscriber makes our subscriber non null + err.printf("Sending request (%s): %s%n", version, postRequest); client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString()).get(); } } - static class GetHandler implements HttpServerAdapters.HttpTestHandler { + static class GetHandler implements HttpTestHandler { @Override - public void handle(HttpServerAdapters.HttpTestExchange exchange) throws IOException { + public void handle(HttpTestExchange exchange) throws IOException { try (OutputStream os = exchange.getResponseBody()) { byte[] bytes = "Response Body".getBytes(UTF_8); err.printf("Server sending 200 (length=%s)", bytes.length); @@ -180,7 +238,7 @@ public class ExpectContinueResetTest { } } - static class NoEndStreamOnPartialResponse implements Http2Handler { + static class PartialResponseResetStreamH2 implements Http2Handler { @Override public void handle(Http2TestExchange exchange) throws IOException { @@ -200,14 +258,14 @@ public class ExpectContinueResetTest { } } - static class NoEndStreamOnFullResponse implements Http2Handler { + static class FullResponseResetStreamH2 implements Http2Handler { @Override public void handle(Http2TestExchange exchange) throws IOException { err.println("Sending 100"); exchange.sendResponseHeaders(100, -1); - err.println("Sending 200"); + err.println("Sending 200"); exchange.sendResponseHeaders(200, 0); if (exchange instanceof ExpectContinueResetTestExchangeImpl testExchange) { err.println("Sending Reset"); @@ -222,6 +280,38 @@ public class ExpectContinueResetTest { } } + static class PartialResponseStopSendingH3 implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange exchange) throws IOException { + err.println("Sending 100"); + exchange.sendResponseHeaders(100, 0); + // sending StopSending(NO_ERROR) before or after sending 100 with no data + // should not fail. + System.err.println("Sending StopSendingFrame"); + exchange.requestStopSending(Http3Error.H3_REQUEST_REJECTED.code()); + // Not resetting the stream would cause the client to wait forever + exchange.resetStream(Http3Error.H3_EXCESSIVE_LOAD.code()); + } + } + + static class FullResponseStopSendingH3 implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange exchange) throws IOException { + err.println("Sending 100"); + exchange.sendResponseHeaders(100, 0); + err.println("Sending 200"); + + // sending StopSending before or after sending 200 with no data + // should not fail. + err.println("Sending StopSendingFrame"); + exchange.requestStopSending(Http3Error.H3_REQUEST_REJECTED.code()); + exchange.sendResponseHeaders(200, 10); + exchange.resetStream(Http3Error.H3_EXCESSIVE_LOAD.code()); + } + } + static class ExpectContinueResetTestExchangeImpl extends Http2TestExchangeImpl { public ExpectContinueResetTestExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { diff --git a/test/jdk/java/net/httpclient/CancelledResponse.java b/test/jdk/java/net/httpclient/CancelledResponse.java index 44be38317dc..1f28ce87221 100644 --- a/test/jdk/java/net/httpclient/CancelledResponse.java +++ b/test/jdk/java/net/httpclient/CancelledResponse.java @@ -23,7 +23,6 @@ import java.net.http.HttpClient; import java.net.http.HttpClient.Version; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import jdk.test.lib.net.SimpleSSLContext; @@ -50,7 +49,10 @@ import java.net.http.HttpResponse.BodySubscriber; import static java.lang.String.format; import static java.lang.System.out; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; /** * @test @@ -164,7 +166,10 @@ public class CancelledResponse { server.start(); HttpClient client = newHttpClient(); - HttpRequest request = HttpRequest.newBuilder(uri).version(version).build(); + HttpRequest request = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, version == HTTP_3 ? ALT_SVC : null) + .version(version) + .build(); try { for (int i = 0; i < responses.length; i++) { HttpResponse r = null; diff --git a/test/jdk/java/net/httpclient/CancelledResponse2.java b/test/jdk/java/net/httpclient/CancelledResponse2.java index 263620acf81..dfcdb03f06e 100644 --- a/test/jdk/java/net/httpclient/CancelledResponse2.java +++ b/test/jdk/java/net/httpclient/CancelledResponse2.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 @@ -21,8 +21,8 @@ * questions. */ -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -38,6 +38,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.nio.ByteBuffer; @@ -51,8 +52,11 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.System.out; -import static java.net.http.HttpClient.Version.*; -import static jdk.httpclient.test.lib.common.HttpServerAdapters.*; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -60,27 +64,36 @@ import static org.testng.Assert.assertTrue; * @test * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext + * @compile ReferenceTracker.java * @run testng/othervm -Djdk.internal.httpclient.debug=true CancelledResponse2 */ +// -Djdk.internal.httpclient.debug=true +public class CancelledResponse2 implements HttpServerAdapters { -public class CancelledResponse2 { - + private static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private static final Random RANDOM = RandomFactory.getRandom(); + private static final int MAX_CLIENT_DELAY = 160; HttpTestServer h2TestServer; URI h2TestServerURI; - private SSLContext sslContext; - private static final Random random = RandomFactory.getRandom(); - private static final int MAX_CLIENT_DELAY = 160; + URI h2h3TestServerURI; + URI h2h3HeadTestServerURI; + URI h3TestServerURI; + HttpTestServer h2h3TestServer; + HttpTestServer h3TestServer; + SSLContext sslContext; @DataProvider(name = "versions") public Object[][] positive() { return new Object[][]{ - { HTTP_2, h2TestServerURI }, + { HTTP_2, null, h2TestServerURI }, + { HTTP_3, null, h2h3TestServerURI }, + { HTTP_3, HTTP_3_URI_ONLY, h3TestServerURI }, }; } private static void delay() { - int delay = random.nextInt(MAX_CLIENT_DELAY); + int delay = RANDOM.nextInt(MAX_CLIENT_DELAY); try { System.out.println("client delay: " + delay); Thread.sleep(delay); @@ -88,13 +101,25 @@ public class CancelledResponse2 { out.println("Unexpected exception: " + x); } } - @Test(dataProvider = "versions") - public void test(Version version, URI uri) throws Exception { + public void test(Version version, Http3DiscoveryMode config, URI uri) throws Exception { for (int i = 0; i < 5; i++) { - HttpClient httpClient = HttpClient.newBuilder().sslContext(sslContext).version(version).build(); + HttpClient httpClient = newClientBuilderForH3().sslContext(sslContext).version(version).build(); + Http3DiscoveryMode reqConfig = null; + if (version.equals(HTTP_3)) { + if (config != null) { + reqConfig = (config.equals(HTTP_3_URI_ONLY)) ? HTTP_3_URI_ONLY : ALT_SVC; + } + // if config is null, we are talking to the H2H3 server, which may + // not support direct connection, in which case we should send a headRequest + if ((config == null && !h2h3TestServer.supportsH3DirectConnection()) + || (reqConfig != null && reqConfig.equals(ALT_SVC))) { + headRequest(httpClient); + } + } HttpRequest httpRequest = HttpRequest.newBuilder(uri) .version(version) + .setOption(H3_DISCOVERY, reqConfig) .GET() .build(); AtomicBoolean cancelled = new AtomicBoolean(); @@ -104,26 +129,56 @@ public class CancelledResponse2 { cf.get(); } catch (Exception e) { e.printStackTrace(); - assertTrue(e.getCause() instanceof IOException, "HTTP/2 should cancel with an IOException when the Subscription is cancelled."); + assertTrue(e.getCause() instanceof IOException, "HTTP/2 & HTTP/3 should cancel with an IOException when the Subscription is cancelled."); } assertTrue(cf.isCompletedExceptionally()); assertTrue(cancelled.get()); + + Tracker tracker = TRACKER.getTracker(httpClient); + httpClient = null; + var error = TRACKER.check(tracker, 5000); + if (error != null) throw error; } } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(h2h3HeadTestServerURI) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, HttpResponse.BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @BeforeTest public void setup() throws IOException { sslContext = new SimpleSSLContext().get(); + h2TestServer = HttpTestServer.create(HTTP_2, sslContext); h2TestServer.addHandler(new CancelledResponseHandler(), "/h2"); h2TestServerURI = URI.create("https://" + h2TestServer.serverAuthority() + "/h2"); + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new CancelledResponseHandler(), "/h2h3"); + h2h3TestServerURI = URI.create("https://" + h2h3TestServer.serverAuthority() + "/h2h3"); + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head"); + h2h3HeadTestServerURI = URI.create("https://" + h2h3TestServer.serverAuthority() + "/h2h3/head"); + + + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new CancelledResponseHandler(), "/h3"); + h3TestServerURI = URI.create("https://" + h3TestServer.serverAuthority() + "/h3"); + h2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest public void teardown() { h2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } BodyHandler ofString(String expected, AtomicBoolean cancelled) { @@ -234,6 +289,7 @@ public class CancelledResponse2 { } } + @Override public void onError(Throwable throwable) { result.completeExceptionally(throwable); diff --git a/test/jdk/java/net/httpclient/ConcurrentResponses.java b/test/jdk/java/net/httpclient/ConcurrentResponses.java index c68ebd0975c..1cabe461e80 100644 --- a/test/jdk/java/net/httpclient/ConcurrentResponses.java +++ b/test/jdk/java/net/httpclient/ConcurrentResponses.java @@ -30,16 +30,19 @@ * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm - * -Djdk.httpclient.HttpClient.log=headers,errors,channel + * -Djdk.internal.httpclient.debug=true * ConcurrentResponses */ +//* -Djdk.internal.httpclient.HttpClient.log=all import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; @@ -64,6 +67,9 @@ import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; @@ -73,6 +79,8 @@ import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; + +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpResponse.BodyHandlers.discarding; import static org.testng.Assert.assertEquals; @@ -86,8 +94,10 @@ public class ConcurrentResponses { HttpsServer httpsTestServer; // HTTPS/1.1 Http2TestServer http2TestServer; // HTTP/2 ( h2c ) Http2TestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer https3TestServer; String httpFixedURI, httpsFixedURI, httpChunkedURI, httpsChunkedURI; String http2FixedURI, https2FixedURI, http2VariableURI, https2VariableURI; + String https3FixedURI, https3VariableURI; static final int CONCURRENT_REQUESTS = 13; static final AtomicInteger IDS = new AtomicInteger(); @@ -145,7 +155,9 @@ public class ConcurrentResponses { { http2FixedURI }, { https2FixedURI }, { http2VariableURI }, - { https2VariableURI } + { https2VariableURI }, + { https3FixedURI }, + { https3VariableURI } }; } @@ -157,20 +169,25 @@ public class ConcurrentResponses { int id = IDS.getAndIncrement(); ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .name("HttpClient-" + id + "-Worker", 0).factory()); - HttpClient client = HttpClient.newBuilder() - .sslContext(sslContext) + var http3 = uri.contains("/https3/"); + Http3DiscoveryMode config = http3 ? Http3DiscoveryMode.HTTP_3_URI_ONLY : null; + var builder = http3 ? HttpServerAdapters.createClientBuilderForH3() : HttpClient.newBuilder(); + if (http3) builder.version(Version.HTTP_3); + HttpClient client = builder .executor(virtualExecutor) - .build(); + .sslContext(sslContext).build(); try { Map requests = new HashMap<>(); for (int i = 0; i < CONCURRENT_REQUESTS; i++) { HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "?" + i)) + .setOption(H3_DISCOVERY, config) .build(); requests.put(request, BODIES[i]); } // initial connection to seed the cache so next parallel connections reuse it - client.sendAsync(HttpRequest.newBuilder(URI.create(uri)).build(), discarding()).join(); + client.sendAsync(HttpRequest.newBuilder(URI.create(uri)) + .setOption(H3_DISCOVERY, config).build(), discarding()).join(); // will reuse connection cached from the previous request ( when HTTP/2 ) CompletableFuture.allOf(requests.keySet().parallelStream() @@ -192,19 +209,25 @@ public class ConcurrentResponses { int id = IDS.getAndIncrement(); ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .name("HttpClient-" + id + "-Worker", 0).factory()); - HttpClient client = HttpClient.newBuilder() + var http3 = uri.contains("/https3/"); + Http3DiscoveryMode config = http3 ? Http3DiscoveryMode.HTTP_3_URI_ONLY : null; + var builder = http3 ? HttpServerAdapters.createClientBuilderForH3() : HttpClient.newBuilder(); + if (http3) builder.version(Version.HTTP_3); + HttpClient client = builder .executor(virtualExecutor) .sslContext(sslContext).build(); try { Map requests = new HashMap<>(); for (int i = 0; i < CONCURRENT_REQUESTS; i++) { HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "?" + i)) + .setOption(H3_DISCOVERY, config) .build(); requests.put(request, BODIES[i]); } // initial connection to seed the cache so next parallel connections reuse it - client.sendAsync(HttpRequest.newBuilder(URI.create(uri)).build(), discarding()).join(); + client.sendAsync(HttpRequest.newBuilder(URI.create(uri)) + .setOption(H3_DISCOVERY, config).build(), discarding()).join(); // will reuse connection cached from the previous request ( when HTTP/2 ) CompletableFuture.allOf(requests.keySet().parallelStream() @@ -310,10 +333,17 @@ public class ConcurrentResponses { https2TestServer.addHandler(new Http2VariableHandler(), "/https2/variable"); https2VariableURI = "https://" + https2TestServer.serverAuthority() + "/https2/variable"; + https3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + https3TestServer.addHandler(new Http3FixedHandler(), "/https3/fixed"); + https3FixedURI = "https://" + https3TestServer.serverAuthority() + "/https3/fixed"; + https3TestServer.addHandler(new Http3VariableHandler(), "/https3/variable"); + https3VariableURI = "https://" + https3TestServer.serverAuthority() + "/https3/variable"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + https3TestServer.start(); } @AfterTest @@ -322,6 +352,7 @@ public class ConcurrentResponses { httpsTestServer.stop(0); http2TestServer.stop(); https2TestServer.stop(); + https3TestServer.stop(); } interface SendResponseHeadersFunction { @@ -407,4 +438,26 @@ public class ConcurrentResponses { (rcode, ignored) -> t.sendResponseHeaders(rcode, 0 /* no Content-Length */)); } } + + static class Http3FixedHandler implements HttpTestHandler { + + @Override + public void handle(HttpServerAdapters.HttpTestExchange t) throws IOException { + serverHandlerImpl(t.getRequestBody(), + t.getResponseBody(), + t.getRequestURI(), + (rcode, length) -> t.sendResponseHeaders(rcode, length)); + } + } + + static class Http3VariableHandler implements HttpTestHandler { + + @Override + public void handle(HttpServerAdapters.HttpTestExchange t) throws IOException { + serverHandlerImpl(t.getRequestBody(), + t.getResponseBody(), + t.getRequestURI(), + (rcode, ignored) -> t.sendResponseHeaders(rcode, -1/* no Content-Length */)); + } + } } diff --git a/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java b/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java index 0ff21c23a8f..f302de4ee48 100644 --- a/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java +++ b/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, 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 @@ -30,7 +30,9 @@ * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters * @bug 8283544 - * @run testng/othervm ContentLengthHeaderTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * ContentLengthHeaderTest */ @@ -51,6 +53,7 @@ import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.util.Optional; import javax.net.ssl.SSLContext; import jdk.test.lib.net.URIBuilder; @@ -58,6 +61,8 @@ import jdk.test.lib.net.URIBuilder; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -69,24 +74,29 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { static HttpTestServer testContentLengthServerH1; static HttpTestServer testContentLengthServerH2; + static HttpTestServer testContentLengthServerH3; static PrintStream testLog = System.err; static SSLContext sslContext; HttpClient hc; URI testContentLengthURIH1; URI testContentLengthURIH2; + URI testContentLengthURIH3; @BeforeTest - public void setup() throws IOException, URISyntaxException { + public void setup() throws IOException, URISyntaxException, InterruptedException { sslContext = new SimpleSSLContext().get(); testContentLengthServerH1 = HttpTestServer.create(HTTP_1_1); testContentLengthServerH2 = HttpTestServer.create(HTTP_2, sslContext); + testContentLengthServerH3 = HttpTestServer.create(HTTP_3, sslContext); // Create handlers for tests that check for the presence of a Content-length header testContentLengthServerH1.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); testContentLengthServerH2.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); + testContentLengthServerH3.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); testContentLengthServerH1.addHandler(new ContentLengthHandler(), BODY_PATH); testContentLengthServerH2.addHandler(new OptionalContentLengthHandler(), BODY_PATH); + testContentLengthServerH3.addHandler(new OptionalContentLengthHandler(), BODY_PATH); testContentLengthURIH1 = URIBuilder.newBuilder() .scheme("http") .loopback() @@ -97,6 +107,11 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { .loopback() .port(testContentLengthServerH2.getAddress().getPort()) .build(); + testContentLengthURIH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(testContentLengthServerH3.getAddress().getPort()) + .build(); testContentLengthServerH1.start(); testLog.println("HTTP/1.1 Server up at address: " + testContentLengthServerH1.getAddress()); @@ -106,25 +121,45 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { testLog.println("HTTP/2 Server up at address: " + testContentLengthServerH2.getAddress()); testLog.println("Request URI for Client: " + testContentLengthURIH2); - hc = HttpClient.newBuilder() + testContentLengthServerH3.start(); + testLog.println("HTTP/3 Server up at address: " + + testContentLengthServerH3.getAddress()); + testLog.println("HTTP/3 Quic Endpoint up at address: " + + testContentLengthServerH3.getH3AltService().get().getAddress()); + testLog.println("Request URI for Client: " + testContentLengthURIH3); + + hc = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); + var firstReq = HttpRequest.newBuilder(URI.create(testContentLengthURIH3 + NO_BODY_PATH)) + .setOption(H3_DISCOVERY, testContentLengthServerH3.h3DiscoveryConfig()) + .HEAD() + .version(HTTP_2) + .build(); + // populate alt-service registry + var resp = hc.send(firstReq, BodyHandlers.ofString()); + assertEquals(resp.statusCode(), 200); + testLog.println("**** setup done ****"); } @AfterTest public void teardown() { + testLog.println("**** tearing down ****"); if (testContentLengthServerH1 != null) testContentLengthServerH1.stop(); if (testContentLengthServerH2 != null) testContentLengthServerH2.stop(); + if (testContentLengthServerH3 != null) + testContentLengthServerH3.stop(); } @DataProvider(name = "bodies") Object[][] bodies() { return new Object[][]{ {HTTP_1_1, URI.create(testContentLengthURIH1 + BODY_PATH)}, - {HTTP_2, URI.create(testContentLengthURIH2 + BODY_PATH)} + {HTTP_2, URI.create(testContentLengthURIH2 + BODY_PATH)}, + {HTTP_3, URI.create(testContentLengthURIH3 + BODY_PATH)} }; } @@ -132,7 +167,8 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { Object[][] nobodies() { return new Object[][]{ {HTTP_1_1, URI.create(testContentLengthURIH1 + NO_BODY_PATH)}, - {HTTP_2, URI.create(testContentLengthURIH2 + NO_BODY_PATH)} + {HTTP_2, URI.create(testContentLengthURIH2 + NO_BODY_PATH)}, + {HTTP_3, URI.create(testContentLengthURIH3 + NO_BODY_PATH)} }; } diff --git a/test/jdk/java/net/httpclient/CookieHeaderTest.java b/test/jdk/java/net/httpclient/CookieHeaderTest.java index b39a23371ab..455b1048b06 100644 --- a/test/jdk/java/net/httpclient/CookieHeaderTest.java +++ b/test/jdk/java/net/httpclient/CookieHeaderTest.java @@ -75,6 +75,8 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.System.out; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -86,12 +88,14 @@ public class CookieHeaderTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) DummyServer httpDummyServer; DummyServer httpsDummyServer; String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; String httpDummy; String httpsDummy; @@ -115,6 +119,7 @@ public class CookieHeaderTest implements HttpServerAdapters { { httpsDummy, HTTP_1_1 }, { httpURI, HttpClient.Version.HTTP_2 }, { httpsURI, HttpClient.Version.HTTP_2 }, + { http3URI, HttpClient.Version.HTTP_3 }, { httpDummy, HttpClient.Version.HTTP_2 }, { httpsDummy, HttpClient.Version.HTTP_2 }, { http2URI, null }, @@ -130,7 +135,7 @@ public class CookieHeaderTest implements HttpServerAdapters { ConcurrentHashMap> cookieHeaders = new ConcurrentHashMap<>(); CookieHandler cookieManager = new TestCookieHandler(cookieHeaders); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .followRedirects(Redirect.ALWAYS) .cookieHandler(cookieManager) .sslContext(sslContext) @@ -150,6 +155,9 @@ public class CookieHeaderTest implements HttpServerAdapters { if (version != null) { requestBuilder.version(version); } + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } HttpRequest request = requestBuilder.build(); out.println("Initial request: " + request.uri()); @@ -157,7 +165,8 @@ public class CookieHeaderTest implements HttpServerAdapters { out.println("iteration: " + i); HttpResponse response = client.send(request, BodyHandlers.ofString()); - out.println(" Got response: " + response); + out.println(" Got response: " + response + ", config=" + request.getOption(H3_DISCOVERY) + + ", version=" + response.version()); out.println(" Got body Path: " + response.body()); assertEquals(response.statusCode(), 200); @@ -166,11 +175,17 @@ public class CookieHeaderTest implements HttpServerAdapters { cookies.stream() .filter(s -> !s.startsWith("LOC")) .collect(Collectors.toList())); + if (version == HTTP_3 && i > 0) { + assertEquals(response.version(), HTTP_3); + } requestBuilder = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()); if (version != null) { requestBuilder.version(version); } + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } request = requestBuilder.build(); } } @@ -196,6 +211,9 @@ public class CookieHeaderTest implements HttpServerAdapters { https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new CookieValidationHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/retry"; // DummyServer @@ -209,6 +227,7 @@ public class CookieHeaderTest implements HttpServerAdapters { httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); httpDummyServer.start(); httpsDummyServer.start(); } diff --git a/test/jdk/java/net/httpclient/CustomRequestPublisher.java b/test/jdk/java/net/httpclient/CustomRequestPublisher.java index 140a3a98ec4..c8d33030a06 100644 --- a/test/jdk/java/net/httpclient/CustomRequestPublisher.java +++ b/test/jdk/java/net/httpclient/CustomRequestPublisher.java @@ -25,13 +25,14 @@ * @test * @summary Checks correct handling of Publishers that call onComplete without demand * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext + * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * @run testng/othervm CustomRequestPublisher */ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; +import java.net.http.HttpClient.Builder; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Optional; @@ -57,9 +58,12 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.net.http.HttpResponse.BodyHandlers.ofString; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -70,10 +74,12 @@ public class CustomRequestPublisher implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; @DataProvider(name = "variants") public Object[][] variants() { @@ -98,19 +104,26 @@ public class CustomRequestPublisher implements HttpServerAdapters { { http2URI, unknownSupplier, true }, { https2URI, fixedSupplier, true,}, { https2URI, unknownSupplier, true }, + { http3URI, fixedSupplier, true,}, // always use same client with h3 + { http3URI, unknownSupplier, true }, // always use same client with h3 }; } static final int ITERATION_COUNT = 10; /** Asserts HTTP Version, and SSLSession presence when applicable. */ - static void assertVersionAndSession(HttpResponse response, String uri) { - if (uri.contains("http2") || uri.contains("https2")) + static void assertVersionAndSession(int step, HttpResponse response, String uri) { + if (uri.contains("http2") || uri.contains("https2")) { assertEquals(response.version(), HTTP_2); - else if (uri.contains("http1") || uri.contains("https1")) + } else if (uri.contains("http1") || uri.contains("https1")) { assertEquals(response.version(), HTTP_1_1); - else + } else if (uri.contains("http3")) { + if (step == 0) assertNotEquals(response.version(), HTTP_1_1); + else assertEquals(response.version(), HTTP_3, + "unexpected response version on step " + step); + } else { fail("Unknown HTTP version in test for: " + uri); + } Optional ssl = response.sslSession(); if (uri.contains("https")) { @@ -125,6 +138,28 @@ public class CustomRequestPublisher implements HttpServerAdapters { } } + HttpClient.Builder newHttpClientBuilder(String uri) { + HttpClient.Builder builder; + if (uri.contains("/http3/")) { + builder = newClientBuilderForH3(); + // ensure that the preferred version for the client + // is HTTP/3 + builder.version(HTTP_3); + } else builder = HttpClient.newBuilder(); + return builder.proxy(Builder.NO_PROXY); + } + + HttpRequest.Builder newHttpRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (uri.contains("/http3/") && !http3TestServer.supportsH3DirectConnection()) { + // Ensure we don't attempt to connect to a + // potentially different server if HTTP/3 endpoint and + // HTTP/2 endpoint are not on the same port + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + @Test(dataProvider = "variants") void test(String uri, Supplier bpSupplier, boolean sameClient) throws Exception @@ -132,10 +167,10 @@ public class CustomRequestPublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = HttpClient.newBuilder().sslContext(sslContext).build(); + client = newHttpClientBuilder(uri).sslContext(sslContext).build(); BodyPublisher bodyPublisher = bpSupplier.get(); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest request = newHttpRequestBuilder(uri) .POST(bodyPublisher) .build(); @@ -147,7 +182,7 @@ public class CustomRequestPublisher implements HttpServerAdapters { "Expected 200, got:" + resp.statusCode()); assertEquals(resp.body(), bodyPublisher.bodyAsString()); - assertVersionAndSession(resp, uri); + assertVersionAndSession(i, resp, uri); } } @@ -158,10 +193,10 @@ public class CustomRequestPublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = HttpClient.newBuilder().sslContext(sslContext).build(); + client = newHttpClientBuilder(uri).sslContext(sslContext).build(); BodyPublisher bodyPublisher = bpSupplier.get(); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest request = newHttpRequestBuilder(uri) .POST(bodyPublisher) .build(); @@ -174,7 +209,7 @@ public class CustomRequestPublisher implements HttpServerAdapters { "Expected 200, got:" + resp.statusCode()); assertEquals(resp.body(), bodyPublisher.bodyAsString()); - assertVersionAndSession(resp, uri); + assertVersionAndSession(0, resp, uri); } } @@ -325,10 +360,15 @@ public class CustomRequestPublisher implements HttpServerAdapters { https2TestServer.addHandler(new HttpTestEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new HttpTestEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -337,6 +377,7 @@ public class CustomRequestPublisher implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } } diff --git a/test/jdk/java/net/httpclient/CustomResponseSubscriber.java b/test/jdk/java/net/httpclient/CustomResponseSubscriber.java index be71278a450..e8b5073d9a2 100644 --- a/test/jdk/java/net/httpclient/CustomResponseSubscriber.java +++ b/test/jdk/java/net/httpclient/CustomResponseSubscriber.java @@ -46,7 +46,6 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; @@ -58,6 +57,7 @@ import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; import javax.net.ssl.SSLContext; + import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; diff --git a/test/jdk/java/net/httpclient/DependentActionsTest.java b/test/jdk/java/net/httpclient/DependentActionsTest.java index c1e9025fe4b..7d903157de8 100644 --- a/test/jdk/java/net/httpclient/DependentActionsTest.java +++ b/test/jdk/java/net/httpclient/DependentActionsTest.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 @@ -29,16 +29,17 @@ * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext * DependentActionsTest - * @run testng/othervm -Djdk.internal.httpclient.debug=true DependentActionsTest - */ + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * DependentActionsTest + */ import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.StackWalker.StackFrame; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; +import org.testng.SkipException; import org.testng.annotations.AfterTest; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeTest; @@ -49,12 +50,12 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -73,21 +74,20 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Flow; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.lang.String.format; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.util.stream.Collectors.toList; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -99,6 +99,7 @@ public class DependentActionsTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -107,6 +108,9 @@ public class DependentActionsTest implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); @@ -118,6 +122,7 @@ public class DependentActionsTest implements HttpServerAdapters { static volatile boolean tasksFailed; static final AtomicLong serverCount = new AtomicLong(); static final AtomicLong clientCount = new AtomicLong(); + static final AtomicReference errorRef = new AtomicReference<>(); static final long start = System.nanoTime(); public static String now() { long now = System.nanoTime() - start; @@ -184,6 +189,8 @@ public class DependentActionsTest implements HttpServerAdapters { http2URI_chunk, https2URI_fixed, https2URI_chunk, + http3URI_fixed, + http3URI_chunk }; } @@ -232,7 +239,8 @@ public class DependentActionsTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() + .proxy(Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build(); @@ -257,10 +265,14 @@ public class DependentActionsTest implements HttpServerAdapters { HttpClient client = null; out.printf("%ntestNoStalls(%s, %b)%n", uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .build(); BodyHandler handler = new StallingBodyHandler((w) -> {}, @@ -317,10 +329,16 @@ public class DependentActionsTest implements HttpServerAdapters { Staller staller) throws Exception { + if (errorRef.get() != null) { + SkipException sk = new SkipException("skipping due to previous failure: " + name); + sk.setStackTrace(new StackTraceElement[0]); + throw sk; + } out.printf("%n%s%s%n", now(), name); try { testDependent(uri, sameClient, handlers, finisher, extractor, staller); } catch (Error | Exception x) { + errorRef.compareAndSet(null, x); FAILURES.putIfAbsent(name, x); throw x; } @@ -335,20 +353,36 @@ public class DependentActionsTest implements HttpServerAdapters { { HttpClient client = null; for (Where where : EnumSet.of(Where.BODY_HANDLER)) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .build(); BodyHandler handler = new StallingBodyHandler(where.select(staller), handlers.get()); - System.out.println("try stalling in " + where); - staller.acquire(); - assert staller.willStall(); - CompletableFuture> responseCF = client.sendAsync(req, handler); - assert !responseCF.isDone(); - finisher.finish(where, responseCF, staller, extractor); + for (int i = 0; i < 2; i++) { + System.out.println("try stalling in " + where); + staller.acquire(); + assert staller.willStall(); + CompletableFuture> responseCF = client.sendAsync(req, handler); + assert !responseCF.isDone(); + var resp = finisher.finish(where, responseCF, staller, extractor); + if (version(uri) == HTTP_3 && resp.version() != HTTP_3) { + if (i == 0) continue; + // it's possible that the first request still went through HTTP/2 + // if the config was HTTP3_ANY. Retry it - the next time we should + // have HTTP/3 + assertEquals(resp.version(), HTTP_3, + "expected second request to go through HTTP/3 (serverConfig=" + + http3TestServer.h3DiscoveryConfig() + ")"); + } + break; + } + } } @@ -388,7 +422,7 @@ public class DependentActionsTest implements HttpServerAdapters { } interface Finisher { - public void finish(Where w, + public HttpResponse finish(Where w, CompletableFuture> cf, Staller staller, Extractor extractor); @@ -424,7 +458,7 @@ public class DependentActionsTest implements HttpServerAdapters { } } - void finish(Where w, CompletableFuture> cf, + HttpResponse finish(Where w, CompletableFuture> cf, Staller staller, Extractor extractor) { Thread thread = Thread.currentThread(); @@ -446,6 +480,11 @@ public class DependentActionsTest implements HttpServerAdapters { + w + ": " + response, error); } assertEquals(result, List.of(response.request().uri().getPath())); + var uriStr = response.request().uri().toString(); + if (HTTP_3 != version(uriStr) || http3TestServer.h3DiscoveryConfig() != Http3DiscoveryMode.ANY) { + assertEquals(response.version(), version(uriStr), uriStr); + } + return response; } finally { staller.reset(); } @@ -570,6 +609,37 @@ public class DependentActionsTest implements HttpServerAdapters { } } + static Version version(String uri) { + if (uri.contains("/http1/") || uri.contains("/https1/")) + return HTTP_1_1; + if (uri.contains("/http2/") || uri.contains("/https2/")) + return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + System.out.println("\n--- HEAD request succeeded ----\n"); + System.err.println("\n--- HEAD request succeeded ----\n"); + return response; + } @BeforeTest public void setup() throws Exception { @@ -608,11 +678,33 @@ public class DependentActionsTest implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(4); + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + + out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); + out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); + out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); + out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); } @AfterTest @@ -622,6 +714,7 @@ public class DependentActionsTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class HTTP_FixedLengthHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java b/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java index e47f4be46f7..12903294315 100644 --- a/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java +++ b/test/jdk/java/net/httpclient/DependentPromiseActionsTest.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 @@ -33,6 +33,7 @@ */ import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import java.lang.StackWalker.StackFrame; import jdk.test.lib.net.SimpleSSLContext; @@ -49,8 +50,10 @@ import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -81,12 +84,14 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.err; import static java.lang.System.out; import static java.lang.String.format; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -96,10 +101,13 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { SSLContext sslContext; HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http2URI_fixed; String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); @@ -170,6 +178,8 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, http2URI_fixed, http2URI_chunk, https2URI_fixed, @@ -224,9 +234,11 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() .executor(executor) .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) .build(); } @@ -243,47 +255,69 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { } } + Http3DiscoveryMode config(String uri) { + return uri.contains("/http3/") ? HTTP_3_URI_ONLY : null; + } + + Version version(String uri) { + return uri.contains("/http3/") ? HTTP_3 : HTTP_2; + } + + HttpRequest request(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)) + .version(version(uri)); + var config = config(uri); + if (config != null) builder.setOption(H3_DISCOVERY, config); + return builder.build(); + } + @Test(dataProvider = "noStalls") public void testNoStalls(String rootUri, boolean sameClient) throws Exception { if (!FAILURES.isEmpty()) return; HttpClient client = null; out.printf("%ntestNoStalls(%s, %b)%n", rootUri, sameClient); - for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) - client = newHttpClient(sameClient); + try { + for (int i = 0; i < ITERATION_COUNT; i++) { + if (!sameClient || client == null) + client = newHttpClient(sameClient); - String uri = rootUri + "/" + requestCount.incrementAndGet(); - out.printf("\tsending request %s%n", uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) - .build(); - BodyHandler> handler = - new StallingBodyHandler((w) -> {}, - BodyHandlers.ofLines()); - Map>>> pushPromises = - new ConcurrentHashMap<>(); - PushPromiseHandler> pushHandler = new PushPromiseHandler<>() { - @Override - public void applyPushPromise(HttpRequest initiatingRequest, - HttpRequest pushPromiseRequest, - Function>, - CompletableFuture>>> - acceptor) { - pushPromises.putIfAbsent(pushPromiseRequest, acceptor.apply(handler)); + String uri = rootUri + "/" + requestCount.incrementAndGet(); + out.printf("\tsending request %s%n", uri); + HttpRequest req = request(uri); + + BodyHandler> handler = + new StallingBodyHandler((w) -> {}, + BodyHandlers.ofLines()); + Map>>> pushPromises = + new ConcurrentHashMap<>(); + PushPromiseHandler> pushHandler = new PushPromiseHandler<>() { + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function>, + CompletableFuture>>> + acceptor) { + pushPromises.putIfAbsent(pushPromiseRequest, acceptor.apply(handler)); + } + }; + HttpResponse> response = + client.sendAsync(req, BodyHandlers.ofLines(), pushHandler).get(); + String body = response.body().collect(Collectors.joining("|")); + assertEquals(URI.create(body).getPath(), URI.create(uri).getPath()); + for (HttpRequest promised : pushPromises.keySet()) { + out.printf("%s Received promise: %s%n\tresponse: %s%n", + now(), promised, pushPromises.get(promised).get()); + String promisedBody = pushPromises.get(promised).get().body() + .collect(Collectors.joining("|")); + assertEquals(promisedBody, promised.uri().toASCIIString()); } - }; - HttpResponse> response = - client.sendAsync(req, BodyHandlers.ofLines(), pushHandler).get(); - String body = response.body().collect(Collectors.joining("|")); - assertEquals(URI.create(body).getPath(), URI.create(uri).getPath()); - for (HttpRequest promised : pushPromises.keySet()) { - out.printf("%s Received promise: %s%n\tresponse: %s%n", - now(), promised, pushPromises.get(promised).get()); - String promisedBody = pushPromises.get(promised).get().body() - .collect(Collectors.joining("|")); - assertEquals(promisedBody, promised.uri().toASCIIString()); + assertEquals(3, pushPromises.size()); + } + } finally { + if (!sameClient && client != null) { + client.close(); } - assertEquals(3, pushPromises.size()); } } @@ -357,29 +391,33 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { throws Exception { HttpClient client = null; - for (Where where : EnumSet.of(Where.BODY_HANDLER)) { - if (!sameClient || client == null) - client = newHttpClient(sameClient); + try { + for (Where where : EnumSet.of(Where.BODY_HANDLER)) { + if (!sameClient || client == null) + client = newHttpClient(sameClient); + String uri = rootUri + "/" + requestCount.incrementAndGet(); + out.printf("\tsending request %s%n", uri); + HttpRequest req = request(uri); - String uri = rootUri + "/" + requestCount.incrementAndGet(); - out.printf("\tsending request %s%n", uri); - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) - .build(); - StallingPushPromiseHandler promiseHandler = - new StallingPushPromiseHandler<>(where, handlers, stallers); - BodyHandler handler = handlers.get(); - System.out.println("try stalling in " + where); - CompletableFuture> responseCF = - client.sendAsync(req, handler, promiseHandler); - // The body of the main response can be received before the body - // of the push promise handlers are received. - // The body of the main response doesn't stall, so the cf of - // the main response may be done here even for EAGER subscribers. - // We cannot make any assumption on the state of the main response - // cf here, so the only thing we can do is to call the finisher - // which will wait for them all. - finisher.finish(where, responseCF, promiseHandler, extractor); + StallingPushPromiseHandler promiseHandler = + new StallingPushPromiseHandler<>(where, handlers, stallers); + BodyHandler handler = handlers.get(); + System.out.println("try stalling in " + where); + CompletableFuture> responseCF = + client.sendAsync(req, handler, promiseHandler); + // The body of the main response can be received before the body + // of the push promise handlers are received. + // The body of the main response doesn't stall, so the cf of + // the main response may be done here even for EAGER subscribers. + // We cannot make any assumption on the state of the main response + // cf here, so the only thing we can do is to call the finisher + // which will wait for them all. + finisher.finish(where, responseCF, promiseHandler, extractor); + } + } finally { + if (client != null && !sameClient) { + client.close(); + } } } @@ -499,9 +537,7 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { httpStack.forEach(f -> System.out.printf("\t%s%n", f)); failed.set(new RuntimeException("Dependant action has unexpected frame in " + Thread.currentThread() + ": " + httpStack.get(0))); - } - return; } else { List httpStack = WALKER.walk(s -> s.filter(f -> f.getDeclaringClass() .getModule().equals(HttpClient.class.getModule())) @@ -670,31 +706,42 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { throw new AssertionError("Unexpected null sslContext"); // HTTP/2 - HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); - HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler(); + HttpTestHandler fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler chunkedHandler = new HTTP_ChunkedHandler(); http2TestServer = HttpTestServer.create(HTTP_2); - http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed"); - http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk"); + http2TestServer.addHandler(fixedLengthHandler, "/http2/fixed"); + http2TestServer.addHandler(chunkedHandler, "/http2/chunk"); http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/y"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/y"; https2TestServer = HttpTestServer.create(HTTP_2, sslContext); - https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); - https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); + https2TestServer.addHandler(fixedLengthHandler, "/https2/fixed"); + https2TestServer.addHandler(chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/y"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/y"; - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + + serverCount.addAndGet(3); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest public void teardown() throws Exception { + if (sharedClient != null) { + sharedClient.close(); + } sharedClient = null; http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static final BiPredicate ACCEPT_ALL = (x, y) -> true; @@ -712,15 +759,68 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); out.printf("TestServer: %s Pushing promise: %s%n", now(), promise); err.printf("TestServer: %s Pushing promise: %s%n", now(), promise); - HttpHeaders headers; + HttpHeaders reqHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; if (fixed) { String length = String.valueOf(promiseBytes.length); - headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), ACCEPT_ALL); } else { - headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty } - t.serverPush(promise, headers, promiseBytes); + t.serverPush(promise, reqHeaders, rspHeaders, promiseBytes); + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static long sendHttp3PushPromiseFrame(HttpTestExchange t, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending PushPromiseFrame: %s%n", now(), promise); + err.printf("TestServer: %s Pushing PushPromiseFrame: %s%n", now(), promise); + HttpHeaders reqHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); + long pushId = t.sendHttp3PushPromiseFrame(-1, promise, reqHeaders); + out.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + err.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + return pushId; + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static void sendHttp3PushResponse(HttpTestExchange t, + long pushId, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending push response pushId=%s: %s%n", now(), pushId, promise); + err.printf("TestServer: %s Pushing push response pushId=%s: %s%n", now(), pushId, promise); + HttpHeaders reqHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; + if (fixed) { + String length = String.valueOf(promiseBytes.length); + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + ACCEPT_ALL); + } else { + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + } + t.sendHttp3PushResponse(pushId, promise, reqHeaders, rspHeaders, new ByteArrayInputStream(promiseBytes)); } catch (URISyntaxException x) { throw new IOException(x.getMessage(), x); } @@ -739,14 +839,33 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { pushPromiseFor(t, requestURI, path, true); } byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8); - t.sendResponseHeaders(200, resp.length); //fixed content length + t.sendResponseHeaders(200, resp.length); + + //fixed content length + // With HTTP/3 fixed length we send a single DataFrame, + // therefore we can't interleave a PushPromiseFrame in + // the middle of the DataFrame, so we're going to send + // the PushPromiseFrame before the DataFrame, and then + // fulfill the promise later while sending the response + // body. + long[] pushIds = new long[2]; + if (t.getExchangeVersion() == HTTP_3) { + for (int i = 0; i < 2; i++) { + String path = requestURI.getPath() + "/after/promise-" + (i + 2); + pushIds[i] = sendHttp3PushPromiseFrame(t, requestURI, path, true); + } + } try (OutputStream os = t.getResponseBody()) { int bytes = resp.length/3; for (int i = 0; i<2; i++) { String path = requestURI.getPath() + "/after/promise-" + (i + 2); os.write(resp, i * bytes, bytes); os.flush(); - pushPromiseFor(t, requestURI, path, true); + if (t.getExchangeVersion() == HTTP_2) { + pushPromiseFor(t, requestURI, path, true); + } else if (t.getExchangeVersion() == HTTP_3) { + sendHttp3PushResponse(t, pushIds[i], requestURI, path, true); + } } os.write(resp, 2*bytes, resp.length - 2*bytes); } diff --git a/test/jdk/java/net/httpclient/DigestEchoClient.java b/test/jdk/java/net/httpclient/DigestEchoClient.java index 0aa70a26586..10add19cb94 100644 --- a/test/jdk/java/net/httpclient/DigestEchoClient.java +++ b/test/jdk/java/net/httpclient/DigestEchoClient.java @@ -33,6 +33,7 @@ import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; @@ -44,7 +45,6 @@ import java.util.List; import java.util.Optional; import java.util.Random; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -52,7 +52,9 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; + import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; import sun.net.NetProperties; import sun.net.www.HeaderParser; @@ -60,8 +62,9 @@ import sun.net.www.HeaderParser; import static java.lang.System.out; import static java.lang.System.err; import static java.lang.String.format; +import static java.net.http.HttpOption.H3_DISCOVERY; -/** +/* * @test * @summary this test verifies that a client may provides authorization * headers directly when connecting with a server. @@ -189,7 +192,11 @@ public class DigestEchoClient { static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; public HttpClient newHttpClient(DigestEchoServer server) { clientCount.incrementAndGet(); - HttpClient.Builder builder = HttpClient.newBuilder(); + HttpClient.Builder builder = switch (server.version()) { + case HTTP_3 -> HttpServerAdapters.createClientBuilderForH3() + .version(Version.HTTP_3); + default -> HttpClient.newBuilder(); + }; builder = builder.proxy(ProxySelector.of(null)); if (useSSL) { builder.sslContext(context); @@ -223,11 +230,11 @@ public class DigestEchoClient { } public static List serverVersions(Version clientVersion) { - if (clientVersion == Version.HTTP_1_1) { - return List.of(clientVersion); - } else { - return List.of(Version.values()); - } + return switch (clientVersion) { + case HTTP_1_1 -> List.of(clientVersion); + case HTTP_2 -> List.of(Version.HTTP_1_1, Version.HTTP_2); + case HTTP_3 -> List.of(Version.HTTP_2, Version.HTTP_3); + }; } public static List clientVersions() { @@ -273,6 +280,13 @@ public class DigestEchoClient { authType); for (Version clientVersion : clientVersions()) { for (Version serverVersion : serverVersions(clientVersion)) { + if (serverVersion == Version.HTTP_3) { + if (!useSSL) continue; + switch (authType) { + case PROXY, PROXY305: continue; + default: break; + } + } for (boolean expectContinue : expectContinue(serverVersion)) { for (boolean async : BOOLEANS) { for (boolean preemptive : BOOLEANS) { @@ -293,6 +307,13 @@ public class DigestEchoClient { authType); for (Version clientVersion : clientVersions()) { for (Version serverVersion : serverVersions(clientVersion)) { + if (serverVersion == Version.HTTP_3) { + if (!useSSL) continue; + switch (authType) { + case PROXY, PROXY305: continue; + default: break; + } + } for (boolean expectContinue : expectContinue(serverVersion)) { for (boolean async : BOOLEANS) { dec.testDigest(clientVersion, serverVersion, @@ -314,7 +335,7 @@ public class DigestEchoClient { throw t; } finally { Thread.sleep(100); - AssertionError trackFailed = TRACKER.check(500); + AssertionError trackFailed = TRACKER.check(Utils.adjustTimeout(1000)); EchoServers.stop(); System.out.println(" ---------------------------------------------------------- "); System.out.println(String.format("DigestEchoClient %s %s", useSSL ? "SSL" : "CLEAR", types)); @@ -370,6 +391,14 @@ public class DigestEchoClient { .isPresent(); } + static Http3DiscoveryMode serverConfig(int step, DigestEchoServer server) { + var config = server.serverConfig(); + return switch (config) { + case HTTP_3_URI_ONLY -> config; + default -> Http3DiscoveryMode.ALT_SVC; + }; + } + final static AtomicLong basics = new AtomicLong(); final static AtomicLong basicCount = new AtomicLong(); // @Test @@ -413,6 +442,7 @@ public class DigestEchoClient { + ",expectContinue=" + expectContinue + ",version=" + clientVersion); reqURI = URI.create(baseReq + ",basicCount=" + basicCount.get()); HttpRequest.Builder builder = HttpRequest.newBuilder(reqURI).version(clientVersion) + .setOption(H3_DISCOVERY, serverConfig(i, server)) .POST(reqBody).expectContinue(expectContinue); boolean isTunnel = isProxy(authType) && useSSL; if (addHeaders) { @@ -450,6 +480,16 @@ public class DigestEchoClient { out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); resp = client.send(request, BodyHandlers.ofLines()); } + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } } catch (Throwable t) { long stop = System.nanoTime(); synchronized (basicCount) { @@ -471,6 +511,7 @@ public class DigestEchoClient { reqURI = URI.create(baseReq + ",withAuthorization=" + authType + ",basicCount=" + basicCount.get()); request = HttpRequest.newBuilder(reqURI).version(clientVersion) + .setOption(H3_DISCOVERY, server.serverConfig()) .POST(reqBody).header(authorizationKey(authType), auth).build(); if (async) { out.printf("%s client.sendAsync(%s)%n", DigestEchoServer.now(), request); @@ -479,6 +520,16 @@ public class DigestEchoClient { out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); resp = client.send(request, BodyHandlers.ofLines()); } + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } } final List respLines; try { @@ -520,16 +571,19 @@ public class DigestEchoClient { failed = decorated; throw decorated; } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; System.gc(); - if (queue.remove(100) == ref) break; - } - var error = TRACKER.checkShutdown(900); - if (error != null) { - if (failed != null) error.addSuppressed(failed); - throw error; + while (!ref.refersTo(null)) { + System.gc(); + if (queue.remove(100) == ref) break; + } + var error = TRACKER.checkShutdown(tracker, Utils.adjustTimeout(900), false); + if (error != null) { + if (failed != null) error.addSuppressed(failed); + throw error; + } } } System.out.println("OK"); @@ -563,13 +617,13 @@ public class DigestEchoClient { server.getServerAddress(), "/foo/"); HttpClient client = newHttpClient(server); + ReferenceQueue queue = new ReferenceQueue<>(); + WeakReference ref = new WeakReference<>(client, queue); HttpResponse r; CompletableFuture> cf1; byte[] cnonce = new byte[16]; String cnonceStr = null; DigestEchoServer.DigestResponse challenge = null; - ReferenceQueue queue = new ReferenceQueue<>(); - WeakReference ref = new WeakReference<>(client, queue); URI reqURI = null; Throwable failed = null; try { @@ -584,6 +638,7 @@ public class DigestEchoClient { reqURI = URI.create(baseReq + ",digestCount=" + digestCount.get()); HttpRequest.Builder reqBuilder = HttpRequest .newBuilder(reqURI).version(clientVersion).POST(reqBody) + .setOption(H3_DISCOVERY, serverConfig(i, server)) .expectContinue(expectContinue); boolean isTunnel = isProxy(authType) && useSSL; @@ -612,6 +667,16 @@ public class DigestEchoClient { out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); resp = client.send(request, BodyHandlers.ofLines()); } + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } System.out.println(resp); assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407 : "challenge=" + challenge + ", resp=" + resp + ", test=[" + test + "]"; @@ -642,6 +707,7 @@ public class DigestEchoClient { reqURI = URI.create(baseReq + ",withAuth=" + authType + ",digestCount=" + digestCount.get()); try { request = HttpRequest.newBuilder(reqURI).version(clientVersion) + .setOption(H3_DISCOVERY, serverConfig(i, server)) .POST(reqBody).header(authorizationKey(authType), auth).build(); } catch (IllegalArgumentException x) { throw x; @@ -654,6 +720,16 @@ public class DigestEchoClient { resp = client.send(request, BodyHandlers.ofLines()); } System.out.println(resp); + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } } final List respLines; try { @@ -691,24 +767,27 @@ public class DigestEchoClient { failed = decorated; throw decorated; } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; System.gc(); - if (queue.remove(100) == ref) break; - } - var error = TRACKER.checkShutdown(900); - if (error != null) { - if (failed != null) { - error.addSuppressed(failed); + while (!ref.refersTo(null)) { + System.gc(); + if (queue.remove(100) == ref) break; + } + var error = TRACKER.checkShutdown(tracker, Utils.adjustTimeout(900), false); + if (error != null) { + if (failed != null) { + error.addSuppressed(failed); + } + throw error; } - throw error; } } System.out.println("OK"); } - // WARNING: This is not a full fledged implementation of DIGEST. + // WARNING: This is not a full-fledged implementation of DIGEST. // It does contain bugs and inaccuracy. static String digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce) throws NoSuchAlgorithmException { @@ -729,32 +808,34 @@ public class DigestEchoClient { } static String authenticateKey(DigestEchoServer.HttpAuthType authType) { - switch (authType) { - case SERVER: return "www-authenticate"; - case SERVER307: return "www-authenticate"; - case PROXY: return "proxy-authenticate"; - case PROXY305: return "proxy-authenticate"; - default: throw new InternalError("authType: " + authType); - } + return switch (authType) { + case SERVER -> "www-authenticate"; + case SERVER307 -> "www-authenticate"; + case PROXY -> "proxy-authenticate"; + case PROXY305 -> "proxy-authenticate"; + }; } static String authorizationKey(DigestEchoServer.HttpAuthType authType) { - switch (authType) { - case SERVER: return "authorization"; - case SERVER307: return "Authorization"; - case PROXY: return "Proxy-Authorization"; - case PROXY305: return "proxy-Authorization"; - default: throw new InternalError("authType: " + authType); - } + return switch (authType) { + case SERVER -> "authorization"; + case SERVER307 -> "Authorization"; + case PROXY -> "Proxy-Authorization"; + case PROXY305 -> "proxy-Authorization"; + }; } static boolean isProxy(DigestEchoServer.HttpAuthType authType) { - switch (authType) { - case SERVER: return false; - case SERVER307: return false; - case PROXY: return true; - case PROXY305: return true; - default: throw new InternalError("authType: " + authType); - } + return switch (authType) { + case SERVER, SERVER307 -> false; + case PROXY, PROXY305 -> true; + }; + } + + static boolean isRedirecting(DigestEchoServer.HttpAuthType authType) { + return switch (authType) { + case SERVER307, PROXY305 -> true; + case SERVER, PROXY -> false; + }; } } diff --git a/test/jdk/java/net/httpclient/DigestEchoClientSSL.java b/test/jdk/java/net/httpclient/DigestEchoClientSSL.java index 9a7f5acb88a..ecf043f58c8 100644 --- a/test/jdk/java/net/httpclient/DigestEchoClientSSL.java +++ b/test/jdk/java/net/httpclient/DigestEchoClientSSL.java @@ -31,10 +31,18 @@ * DigestEchoClient ReferenceTracker DigestEchoClientSSL * jdk.httpclient.test.lib.common.HttpServerAdapters * @run main/othervm/timeout=300 - * DigestEchoClientSSL SSL + * -Djdk.internal.httpclient.debug=err + * -Djdk.httpclient.HttpClient.log=headers + * DigestEchoClientSSL SSL SERVER307 + * @run main/othervm/timeout=300 + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers + * DigestEchoClientSSL SSL SERVER PROXY * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes= * -Djdk.http.auth.tunneling.disabledSchemes= + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers * DigestEchoClientSSL SSL PROXY * */ diff --git a/test/jdk/java/net/httpclient/DigestEchoServer.java b/test/jdk/java/net/httpclient/DigestEchoServer.java index 187951405c2..f9a2ec1e017 100644 --- a/test/jdk/java/net/httpclient/DigestEchoServer.java +++ b/test/jdk/java/net/httpclient/DigestEchoServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -47,6 +47,8 @@ import java.net.StandardSocketOptions; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.UnsupportedProtocolVersionException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -67,8 +69,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import jdk.httpclient.test.lib.common.HttpServerAdapters.AbstractHttpAuthFilter.HttpAuthMode; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import sun.net.www.HeaderParser; import java.net.http.HttpClient.Version; import jdk.httpclient.test.lib.common.HttpServerAdapters; @@ -159,15 +163,21 @@ public abstract class DigestEchoServer implements HttpServerAdapters { final String key; DigestEchoServer(String key, - HttpTestServer server, - DigestEchoServer target, - HttpTestHandler delegate) { + HttpTestServer server, + DigestEchoServer target, + HttpTestHandler delegate) { this.key = key; this.serverImpl = server; this.redirect = target; this.delegate = delegate; } + public Version version() { + if (serverImpl.canHandle(Version.HTTP_3)) + return Version.HTTP_3; + return serverImpl.getVersion(); + } + public static void main(String[] args) throws IOException { @@ -186,6 +196,17 @@ public abstract class DigestEchoServer implements HttpServerAdapters { } } + public Http3DiscoveryMode serverConfig() { + // If the client request is HTTP_3, but the server + // doesn't support HTTP/3, we don't want the client + // to attempt a direct HTTP/3 connection - so use + // ALT_SVC to prevent that + var config = serverImpl.h3DiscoveryConfig(); + return config == null + ? Http3DiscoveryMode.ALT_SVC + : config; + } + private static String toString(HttpTestRequestHeaders headers) { return headers.entrySet().stream() .map((e) -> e.getKey() + ": " + e.getValue()) @@ -249,6 +270,13 @@ public abstract class DigestEchoServer implements HttpServerAdapters { } } + private static String authority(InetSocketAddress address) { + String addressStr = address.getAddress().getHostAddress(); + if (addressStr.indexOf(':') >= 0) { + addressStr = "[" + addressStr + "]"; + } + return addressStr + ":" + address.getPort(); + } /** * The SocketBindableFactory ensures that the local port used by an HttpServer @@ -267,7 +295,8 @@ public abstract class DigestEchoServer implements HttpServerAdapters { for (int i = 1; i <= max; i++) { B bindable = createBindable(); InetSocketAddress address = getAddress(bindable); - String key = "localhost:" + address.getPort(); + + String key = authority(address); if (addresses.addIfAbsent(key)) { System.out.println("Socket bound to: " + key + " after " + i + " attempt(s)"); @@ -461,6 +490,15 @@ public abstract class DigestEchoServer implements HttpServerAdapters { return HttpTestServer.of(createHttp1Server(protocol)); case HTTP_2: return HttpTestServer.of(createHttp2Server(protocol)); + case HTTP_3: + try { + if (!"https".equalsIgnoreCase(protocol)) { + throw new UnsupportedProtocolVersionException("HTTP/3 requires https"); + } + return HttpTestServer.create(Version.HTTP_3, SSLContext.getDefault()); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } default: throw new InternalError("Unexpected version: " + version); } @@ -481,7 +519,7 @@ public abstract class DigestEchoServer implements HttpServerAdapters { static HttpsServer configure(HttpsServer server) throws IOException { try { SSLContext ctx = SSLContext.getDefault(); - server.setHttpsConfigurator(new Configurator(ctx)); + server.setHttpsConfigurator(new Configurator(server.getAddress().getAddress(), ctx)); } catch (NoSuchAlgorithmException ex) { throw new IOException(ex); } @@ -516,6 +554,16 @@ public abstract class DigestEchoServer implements HttpServerAdapters { Objects.requireNonNull(authType); Objects.requireNonNull(auth); + if (version == Version.HTTP_3) { + if (!"https".equalsIgnoreCase(protocol)) { + throw new UnsupportedProtocolVersionException("HTTP/3 requires TLS"); + } + switch (authType) { + case PROXY, PROXY305 -> + throw new UnsupportedProtocolVersionException("proxying not supported for HTTP/3"); + case SERVER, SERVER307 -> {} + } + } HttpTestServer impl = createHttpServer(version, protocol); String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s", ProcessHandle.current().pid(), @@ -545,6 +593,10 @@ public abstract class DigestEchoServer implements HttpServerAdapters { System.out.println("WARNING: can't use HTTP/1.1 proxy with unsecure HTTP/2 server"); version = Version.HTTP_1_1; } + if (version == Version.HTTP_3) { + System.out.println("WARNING: can't use HTTP/1.1 proxy with HTTP/3 server"); + version = Version.HTTP_2; + } HttpTestServer impl = createHttpServer(version, protocol); String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s", ProcessHandle.current().pid(), @@ -932,15 +984,11 @@ public abstract class DigestEchoServer implements HttpServerAdapters { @Override protected void requestAuthentication(HttpTestExchange he) throws IOException { - String separator; Version v = he.getExchangeVersion(); - if (v == Version.HTTP_1_1) { - separator = "\r\n "; - } else if (v == Version.HTTP_2) { - separator = " "; - } else { - throw new InternalError(String.valueOf(v)); - } + String separator = switch (v) { + case HTTP_1_1 -> "\r\n "; + case HTTP_2, HTTP_3 -> " "; + }; String headerName = getAuthenticate(); String headerValue = "Digest realm=\"" + auth.getRealm() + "\"," + separator + "qop=\"auth\"," @@ -1136,13 +1184,18 @@ public abstract class DigestEchoServer implements HttpServerAdapters { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } @Override public void configure (HttpsParameters params) { - params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } @@ -1553,8 +1606,8 @@ public abstract class DigestEchoServer implements HttpServerAdapters { port = port == -1 ? 443 : port; targetAddress = new InetSocketAddress(uri.getHost(), port); if (serverImpl != null) { - assert targetAddress.getHostString() - .equalsIgnoreCase(serverImpl.getAddress().getHostString()); + assert targetAddress.getAddress().getHostAddress() + .equalsIgnoreCase(serverImpl.getAddress().getAddress().getHostAddress()); assert targetAddress.getPort() == serverImpl.getAddress().getPort(); } } catch (Throwable x) { @@ -1716,15 +1769,16 @@ public abstract class DigestEchoServer implements HttpServerAdapters { public static URL url(String protocol, InetSocketAddress address, String path) throws MalformedURLException { - return new URL(protocol(protocol), - address.getHostString(), - address.getPort(), path); + try { + return uri(protocol, address, path).toURL(); + } catch (URISyntaxException e) { + throw new MalformedURLException(e.getMessage()); + } } public static URI uri(String protocol, InetSocketAddress address, String path) throws URISyntaxException { return new URI(protocol(protocol) + "://" + - address.getHostString() + ":" + - address.getPort() + path); + authority(address) + path); } } diff --git a/test/jdk/java/net/httpclient/EmptyAuthenticate.java b/test/jdk/java/net/httpclient/EmptyAuthenticate.java index ca5c41594e8..e445a974ad0 100644 --- a/test/jdk/java/net/httpclient/EmptyAuthenticate.java +++ b/test/jdk/java/net/httpclient/EmptyAuthenticate.java @@ -89,11 +89,13 @@ class EmptyAuthenticate { } static Stream args() { - return Stream - .of(Version.HTTP_1_1, Version.HTTP_2) - .flatMap(version -> Stream - .of(true, false) - .map(secure -> Arguments.of(version, secure))); + return Stream.concat( + Stream + .of(Version.HTTP_1_1, Version.HTTP_2) + .flatMap(version -> Stream + .of(true, false) + .map(secure -> Arguments.of(version, secure))), + Stream.of(Arguments.of(Version.HTTP_3, true))); } private static HttpTestServer createServer(Version version, boolean secure, String uriPath) @@ -128,7 +130,7 @@ class EmptyAuthenticate { } private static HttpClient createClient(Version version, boolean secure) { - HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(version).proxy(NO_PROXY); + HttpClient.Builder clientBuilder = HttpServerAdapters.createClientBuilderFor(version).proxy(NO_PROXY); if (secure) { clientBuilder.sslContext(SSL_CONTEXT); } diff --git a/test/jdk/java/net/httpclient/EncodedCharsInURI.java b/test/jdk/java/net/httpclient/EncodedCharsInURI.java index bcda1f32539..91bace42699 100644 --- a/test/jdk/java/net/httpclient/EncodedCharsInURI.java +++ b/test/jdk/java/net/httpclient/EncodedCharsInURI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -36,6 +36,7 @@ */ //* -Djdk.internal.httpclient.debug=true +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterTest; @@ -78,6 +79,8 @@ import static java.lang.System.err; import static java.lang.System.out; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -92,6 +95,7 @@ public class EncodedCharsInURI implements HttpServerAdapters { HttpTestServer https2TestServer; // HTTP/2 ( h2 ) DummyServer httpDummyServer; // HTTP/1.1 [ 2 servers ] DummyServer httpsDummyServer; // HTTPS/1.1 + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -100,6 +104,9 @@ public class EncodedCharsInURI implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; String httpDummy; String httpsDummy; @@ -169,6 +176,8 @@ public class EncodedCharsInURI implements HttpServerAdapters { return new String[] { httpDummy, httpsDummy, + http3URI_fixed, + http3URI_chunk, httpURI_fixed, httpURI_chunk, httpsURI_fixed, @@ -202,12 +211,39 @@ public class EncodedCharsInURI implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() .executor(executor) .proxy(NO_PROXY) .sslContext(sslContext) @@ -246,11 +282,14 @@ public class EncodedCharsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { BodyPublisher bodyPublisher = BodyPublishers.ofString(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .POST(bodyPublisher) .build(); BodyHandler handler = BodyHandlers.ofString(); @@ -314,15 +353,27 @@ public class EncodedCharsInURI implements HttpServerAdapters { httpDummy = "http://" + httpDummyServer.serverAuthority() + "/http1/dummy/x"; httpsDummy = "https://" + httpsDummyServer.serverAuthority() + "/https1/dummy/x"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + err.println(now() + "Starting servers"); - serverCount.addAndGet(6); + serverCount.addAndGet(7); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); httpDummyServer.start(); httpsDummyServer.start(); + http3TestServer.start(); out.println("HTTP/1.1 dummy server (http) listening at: " + httpDummyServer.serverAuthority()); out.println("HTTP/1.1 dummy server (TLS) listening at: " + httpsDummyServer.serverAuthority()); @@ -330,6 +381,11 @@ public class EncodedCharsInURI implements HttpServerAdapters { out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); out.println(now() + "setup done"); err.println(now() + "setup done"); @@ -342,6 +398,7 @@ public class EncodedCharsInURI implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); httpDummyServer.stopServer(); httpsDummyServer.stopServer(); } diff --git a/test/jdk/java/net/httpclient/EscapedOctetsInURI.java b/test/jdk/java/net/httpclient/EscapedOctetsInURI.java index 8a17cea78c4..d9d8ba1ddd7 100644 --- a/test/jdk/java/net/httpclient/EscapedOctetsInURI.java +++ b/test/jdk/java/net/httpclient/EscapedOctetsInURI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -50,6 +50,7 @@ import java.util.Arrays; import java.util.List; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -60,6 +61,8 @@ import static java.lang.System.err; import static java.lang.System.out; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -67,14 +70,17 @@ import static org.testng.Assert.assertEquals; public class EscapedOctetsInURI implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; + String http3URI_head; private volatile HttpClient sharedClient; @@ -106,6 +112,9 @@ public class EscapedOctetsInURI implements HttpServerAdapters { Arrays.asList(pathsAndQueryStrings).stream() .map(e -> new Object[] {https2URI + e[0] + e[1], sameClient}) .forEach(list::add); + Arrays.asList(pathsAndQueryStrings).stream() + .map(e -> new Object[] {http3URI + e[0] + e[1], sameClient}) + .forEach(list::add); } return list.stream().toArray(Object[][]::new); } @@ -126,12 +135,38 @@ public class EscapedOctetsInURI implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } private HttpClient makeNewClient() { - return HttpClient.newBuilder() + return newClientBuilderForH3() .proxy(NO_PROXY) .sslContext(sslContext) .build(); @@ -171,10 +206,13 @@ public class EscapedOctetsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); HttpResponse resp = client.send(request, BodyHandlers.ofString()); out.println("Got response: " + resp); @@ -200,10 +238,13 @@ public class EscapedOctetsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); client.sendAsync(request, BodyHandlers.ofString()) .thenApply(response -> { out.println("Got response: " + response); @@ -247,16 +288,28 @@ public class EscapedOctetsInURI implements HttpServerAdapters { https2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/https2/get"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/get"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new HttpASCIIUriStringHandler(), "/http3/get"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/get"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + err.println(now() + "Starting servers"); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); out.println(now() + "setup done"); err.println(now() + "setup done"); @@ -269,6 +322,7 @@ public class EscapedOctetsInURI implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } /** A handler that returns as its body the exact escaped request URI. */ diff --git a/test/jdk/java/net/httpclient/ExecutorShutdown.java b/test/jdk/java/net/httpclient/ExecutorShutdown.java index e829daa556b..7d4cbec6d92 100644 --- a/test/jdk/java/net/httpclient/ExecutorShutdown.java +++ b/test/jdk/java/net/httpclient/ExecutorShutdown.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, 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 @@ -39,12 +39,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -61,13 +61,8 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -79,6 +74,9 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -91,14 +89,19 @@ public class ExecutorShutdown implements HttpServerAdapters { static final Random RANDOM = RandomFactory.getRandom(); SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/2 ( h2+h3 ) + HttpTestServer h3TestServer; // HTTP/2 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h3URI; + String h2h3Head; static final String MESSAGE = "ExecutorShutdown message body"; static final int ITERATIONS = 3; @@ -106,10 +109,12 @@ public class ExecutorShutdown implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig() }, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig() }, + { httpURI, HTTP_1_1, null }, + { httpsURI, HTTP_1_1, null }, + { http2URI, HTTP_2, null }, + { https2URI, HTTP_2, null }, }; } @@ -134,6 +139,13 @@ public class ExecutorShutdown implements HttpServerAdapters { } else if (t instanceof ClosedChannelException) { out.println(what + ": Accepting ClosedChannelException as a valid cause: " + t); accepted = t; + } else if (t instanceof IOException io) { + var msg = io.getMessage(); + // Stream 0 cancelled should also be accepted + if (msg != null && msg.matches("Stream (0|([1-9][0-9]*)) cancelled")) { + out.println(what + ": Accepting Stream cancelled as a valid cause: " + io); + accepted = t; + } } t = t.getCause(); } @@ -147,12 +159,13 @@ public class ExecutorShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); ExecutorService executorService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .executor(executorService) .sslContext(sslContext) .build(); @@ -160,11 +173,19 @@ public class ExecutorShutdown implements HttpServerAdapters { assert client.executor().isPresent(); int step = RANDOM.nextInt(ITERATIONS); + int head = Math.min(1, step); + List>> responses = new ArrayList<>(); try { - List>> responses = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { + if (i == head && version == HTTP_3 && config != HTTP_3_URI_ONLY) { + // let's the first request go through whatever version, + // but ensure that the second will find an AltService + // record + headRequest(client); + } URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, config) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); @@ -189,6 +210,7 @@ public class ExecutorShutdown implements HttpServerAdapters { out.println(si + ": Got response: " + response); out.println(si + ": Got body Path: " + response.body()); assertEquals(response.statusCode(), 200); + if (si >= head) assertEquals(response.version(), version); assertEquals(response.body(), MESSAGE); return response; }).exceptionally((t) -> { @@ -207,12 +229,13 @@ public class ExecutorShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting (%s, %s, %s) ----%n%n", uriString, version, config); ExecutorService executorService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .executor(executorService) .sslContext(sslContext) .build(); @@ -225,8 +248,9 @@ public class ExecutorShutdown implements HttpServerAdapters { for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) - .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) - .build(); + .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) + .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; try { @@ -249,6 +273,7 @@ public class ExecutorShutdown implements HttpServerAdapters { out.println(si + ": Got response: " + response); out.println(si + ": Got body Path: " + response.body()); assertEquals(response.statusCode(), 200); + if (si > 0) assertEquals(response.version(), version); assertEquals(response.body(), MESSAGE); return response; }).handle((r,t) -> { @@ -274,6 +299,15 @@ public class ExecutorShutdown implements HttpServerAdapters { // -- Infrastructure + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @BeforeTest public void setup() throws Exception { out.println("\n**** Setup ****\n"); @@ -295,10 +329,21 @@ public class ExecutorShutdown implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -310,6 +355,8 @@ public class ExecutorShutdown implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/ExpectContinueTest.java b/test/jdk/java/net/httpclient/ExpectContinueTest.java index 50e43099255..a703fbfaacf 100644 --- a/test/jdk/java/net/httpclient/ExpectContinueTest.java +++ b/test/jdk/java/net/httpclient/ExpectContinueTest.java @@ -37,6 +37,7 @@ import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestServerConnection; +import jdk.httpclient.test.lib.http2.Http2TestServerConnection.ResponseHeaders; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.HeaderFrame; import org.testng.TestException; @@ -252,16 +253,25 @@ public class ExpectContinueTest implements HttpServerAdapters { static class ExpectContinueTestExchangeImpl extends Http2TestExchangeImpl { - public ExpectContinueTestExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { + public ExpectContinueTestExchangeImpl(int streamid, + String method, + HttpHeaders reqheaders, + HttpHeadersBuilder rspheadersBuilder, + URI uri, InputStream is, + SSLSession sslSession, + BodyOutputStream os, + Http2TestServerConnection conn, + boolean pushAllowed) { super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed); } private void sendEndStreamHeaders() throws IOException { this.responseLength = 0; - rspheadersBuilder.setHeader(":status", Integer.toString(100)); + HttpHeadersBuilder pseudoHeadersBuilder = new HttpHeadersBuilder(); + pseudoHeadersBuilder.setHeader(":status", Integer.toString(100)); + HttpHeaders pseudoHeaders = pseudoHeadersBuilder.build(); HttpHeaders headers = rspheadersBuilder.build(); - Http2TestServerConnection.ResponseHeaders response - = new Http2TestServerConnection.ResponseHeaders(headers); + ResponseHeaders response = new ResponseHeaders(pseudoHeaders, headers); response.streamid(streamid); response.setFlag(HeaderFrame.END_HEADERS); response.setFlag(HeaderFrame.END_STREAM); diff --git a/test/jdk/java/net/httpclient/FileChannelPublisherTest.java b/test/jdk/java/net/httpclient/FileChannelPublisherTest.java index 5b064efa078..4bc3c3ae315 100644 --- a/test/jdk/java/net/httpclient/FileChannelPublisherTest.java +++ b/test/jdk/java/net/httpclient/FileChannelPublisherTest.java @@ -531,9 +531,8 @@ class FileChannelPublisherTest { // Verifying the client failure LOGGER.log("Verifying the client failure"); - Exception requestFailure0 = assertThrows(ExecutionException.class, () -> responseFutureRef.get().get()); - Exception requestFailure1 = assertInstanceOf(UncheckedIOException.class, requestFailure0.getCause()); - assertInstanceOf(ClosedChannelException.class, requestFailure1.getCause()); + Exception requestFailure = assertThrows(ExecutionException.class, () -> responseFutureRef.get().get()); + assertInstanceOf(ClosedChannelException.class, requestFailure.getCause()); verifyServerIncompleteRead(pair, fileLength); @@ -578,9 +577,8 @@ class FileChannelPublisherTest { // Verifying the client failure LOGGER.log("Verifying the client failure"); Exception requestFailure0 = assertThrows(ExecutionException.class, responseFuture::get); - Exception requestFailure1 = assertInstanceOf(UncheckedIOException.class, requestFailure0.getCause()); - Exception requestFailure2 = assertInstanceOf(IOException.class, requestFailure1.getCause()); - String requestFailure2Message = requestFailure2.getMessage(); + Exception requestFailure1 = assertInstanceOf(IOException.class, requestFailure0.getCause()); + String requestFailure2Message = requestFailure1.getMessage(); assertTrue( requestFailure2Message.contains("Unexpected EOF"), "unexpected message: " + requestFailure2Message); diff --git a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java index 5d0935d7216..ddefd2a9aa7 100644 --- a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java +++ b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java @@ -27,9 +27,11 @@ import java.io.OutputStream; import java.net.URI; import java.net.http.HttpClient.Builder; import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Flow; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicBoolean; @@ -46,6 +48,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; + +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.util.stream.Collectors.joining; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpRequest.BodyPublishers.fromPublisher; @@ -61,20 +65,22 @@ import static org.testng.Assert.fail; * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm FlowAdapterPublisherTest + * @run testng/othervm -Djdk.internal.httpclient.debug=err FlowAdapterPublisherTest */ public class FlowAdapterPublisherTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; @DataProvider(name = "uris") public Object[][] variants() { @@ -83,6 +89,7 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { { httpsURI }, { http2URI }, { https2URI }, + { http3URI }, }; } @@ -94,16 +101,26 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { if (uri.contains("/https1/")) return Version.HTTP_1_1; if (uri.contains("/http2/")) return Version.HTTP_2; if (uri.contains("/https2/")) return Version.HTTP_2; + if (uri.contains("/http3/")) return Version.HTTP_3; return null; } private HttpClient newHttpClient(String uri) { - var builder = HttpClient.newBuilder(); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpClient.newBuilder() + : HttpServerAdapters.createClientBuilderForH3().version(Version.HTTP_3); return builder.sslContext(sslContext).proxy(Builder.NO_PROXY).build(); } private HttpRequest.Builder newRequestBuilder(String uri) { - return HttpRequest.newBuilder(URI.create(uri)); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpRequest.newBuilder(URI.create(uri)) + : HttpRequest.newBuilder(URI.create(uri)) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + return builder; } @Test @@ -378,10 +395,15 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -390,6 +412,7 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class HttpEchoHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java b/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java index 8319204f5c2..5de943278eb 100644 --- a/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java +++ b/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, 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 @@ -30,9 +30,11 @@ import java.lang.StackWalker.StackFrame; import java.net.URI; import java.net.http.HttpClient.Builder; import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.ByteBuffer; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -56,6 +58,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; + +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; @@ -74,14 +78,16 @@ import static org.testng.Assert.assertTrue; public class FlowAdapterSubscriberTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); @@ -102,6 +108,7 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { { httpsURI }, { http2URI }, { https2URI }, + { http3URI }, }; } @@ -112,16 +119,26 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { if (uri.contains("/https1/")) return Version.HTTP_1_1; if (uri.contains("/http2/")) return Version.HTTP_2; if (uri.contains("/https2/")) return Version.HTTP_2; + if (uri.contains("/http3/")) return Version.HTTP_3; return null; } private HttpClient newHttpClient(String uri) { - var builder = HttpClient.newBuilder(); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpClient.newBuilder() + : HttpServerAdapters.createClientBuilderForH3().version(Version.HTTP_3); return builder.sslContext(sslContext).proxy(Builder.NO_PROXY).build(); } private HttpRequest.Builder newRequestBuilder(String uri) { - return HttpRequest.newBuilder(URI.create(uri)); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpRequest.newBuilder(URI.create(uri)) + : HttpRequest.newBuilder(URI.create(uri)) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + return builder; } @Test @@ -636,10 +653,15 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -648,6 +670,7 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class HttpEchoHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/ForbiddenHeadTest.java b/test/jdk/java/net/httpclient/ForbiddenHeadTest.java index 1498aa118b3..2a50d03b365 100644 --- a/test/jdk/java/net/httpclient/ForbiddenHeadTest.java +++ b/test/jdk/java/net/httpclient/ForbiddenHeadTest.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 @@ -35,9 +35,6 @@ * ForbiddenHeadTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -53,7 +50,6 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Proxy; @@ -76,12 +72,14 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.err; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -93,12 +91,14 @@ public class ForbiddenHeadTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) DigestEchoServer.TunnelingProxy proxy; DigestEchoServer.TunnelingProxy authproxy; String httpURI; String httpsURI; String http2URI; String https2URI; + String https3URI; HttpClient authClient; HttpClient noAuthClient; @@ -218,6 +218,8 @@ public class ForbiddenHeadTest implements HttpServerAdapters { for (var uri : List.of(httpURI, httpsURI, http2URI, https2URI)) { result.add(new Object[]{uri + srv + auth, pcode, async, useAuth}); } + if (code == PROXY_UNAUTHORIZED) continue; // no HTTP/3 with proxy + result.add(new Object[] {https3URI + srv + auth, pcode, async, useAuth}); } } } @@ -275,6 +277,10 @@ public class ForbiddenHeadTest implements HttpServerAdapters { .newBuilder(uri) .method("HEAD", HttpRequest.BodyPublishers.noBody()); + if (uriString.contains("/http3/")) { + requestBuilder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + HttpRequest request = requestBuilder.build(); out.println("Initial request: " + request.uri()); @@ -349,10 +355,14 @@ public class ForbiddenHeadTest implements HttpServerAdapters { https2TestServer.addHandler(new UnauthorizedHandler(), "/https2/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new UnauthorizedHandler(), "/http3/"); + https3URI = "https://" + http3TestServer.serverAuthority() + "/http3"; + proxy = DigestEchoServer.createHttpsProxyTunnel(DigestEchoServer.HttpAuthSchemeType.NONE); authproxy = DigestEchoServer.createHttpsProxyTunnel(DigestEchoServer.HttpAuthSchemeType.BASIC); - authClient = TRACKER.track(HttpClient.newBuilder() + authClient = TRACKER.track(newClientBuilderForH3() .proxy(TestProxySelector.of(proxy, authproxy, httpTestServer)) .sslContext(sslContext) .executor(executor) @@ -360,7 +370,7 @@ public class ForbiddenHeadTest implements HttpServerAdapters { .build()); clientCount.incrementAndGet(); - noAuthClient = TRACKER.track(HttpClient.newBuilder() + noAuthClient = TRACKER.track(newClientBuilderForH3() .proxy(TestProxySelector.of(proxy, authproxy, httpTestServer)) .sslContext(sslContext) .executor(executor) @@ -375,6 +385,8 @@ public class ForbiddenHeadTest implements HttpServerAdapters { serverCount.incrementAndGet(); https2TestServer.start(); serverCount.incrementAndGet(); + http3TestServer.start(); + serverCount.incrementAndGet(); } @AfterTest @@ -389,6 +401,7 @@ public class ForbiddenHeadTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/GZIPInputStreamTest.java b/test/jdk/java/net/httpclient/GZIPInputStreamTest.java index 1215fc6c0a0..a8ce8fb2de5 100644 --- a/test/jdk/java/net/httpclient/GZIPInputStreamTest.java +++ b/test/jdk/java/net/httpclient/GZIPInputStreamTest.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 @@ -31,8 +31,6 @@ */ import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -45,10 +43,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -62,11 +60,13 @@ import java.util.function.Supplier; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -77,10 +77,12 @@ public class GZIPInputStreamTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer https3TestServer; // HTTP/3 String httpURI; String httpsURI; String http2URI; String https2URI; + String https3URI; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -163,26 +165,28 @@ public class GZIPInputStreamTest implements HttpServerAdapters { { http2URI, true }, { https2URI, false }, { https2URI, true }, + { https3URI, false }, + { https3URI, true } }; } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .executor(executor) .sslContext(sslContext) .build()); } HttpClient newSingleThreadClient() { - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .executor(singleThreadExecutor) .sslContext(sslContext) .build()); } HttpClient newInLineClient() { - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .executor((r) -> r.run() ) .sslContext(sslContext) .build()); @@ -198,18 +202,10 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri +"/txt/LoremIpsum.txt")) - .build(); + HttpRequest req = buildRequest(URI.create(uri +"/txt/LoremIpsum.txt")); BodyHandler handler = BodyHandlers.ofString(UTF_8); HttpResponse response = client.send(req, handler); - String lorem = response.body(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(response.body()); } } @@ -222,18 +218,10 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/txt/LoremIpsum.txt")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/txt/LoremIpsum.txt")); BodyHandler handler = BodyHandlers.ofInputStream(); HttpResponse response = client.send(req, handler); - String lorem = new String(response.body().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().readAllBytes(), UTF_8)); } } @@ -247,19 +235,11 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); BodyHandler handler = BodyHandlers.ofInputStream(); HttpResponse response = client.send(req, handler); GZIPInputStream gz = new GZIPInputStream(response.body()); - String lorem = new String(gz.readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(gz.readAllBytes(), UTF_8)); } } @@ -273,20 +253,12 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous, because the finisher will block. // We support this, but the executor must have enough threads. BodyHandler handler = new GZIPBodyHandler(); HttpResponse response = client.send(req, handler); - String lorem = new String(response.body().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().readAllBytes(), UTF_8)); } } @@ -301,8 +273,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous, because the finisher will block. // We support this, but the executor must have enough threads. BodyHandler> handler = new BodyHandler>() { @@ -329,14 +300,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { } }; HttpResponse> response = client.send(req, handler); - String lorem = new String(response.body().get().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().get().readAllBytes(), UTF_8)); } } @@ -350,27 +314,20 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/txt/LoremIpsum.txt")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/txt/LoremIpsum.txt")); BodyHandler handler = BodyHandlers.ofInputStream(); CompletableFuture> responseCF = client.sendAsync(req, handler); // This is dangerous. Blocking in the mapping function can wedge the // response. We do support it provided that there enough threads in // the executor. - String lorem = responseCF.thenApply((r) -> { + String responseBody = responseCF.thenApply((r) -> { try { return new String(r.body().readAllBytes(), UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); } }).join(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(responseBody); } } @@ -384,27 +341,20 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); BodyHandler handler = new GZIPBodyHandler(); CompletableFuture> responseCF = client.sendAsync(req, handler); // This is dangerous - we support this, but it can block // if there are not enough threads available. // Correct custom code should use thenApplyAsync instead. - String lorem = responseCF.thenApply((r) -> { + String responseBody = responseCF.thenApply((r) -> { try { return new String(r.body().readAllBytes(), UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); } }).join(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(responseBody); } } @@ -419,8 +369,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous. Blocking in the mapping function can wedge the // response. We do support it provided that there enough thread in // the executor. @@ -433,14 +382,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { } }); HttpResponse response = client.send(req, handler); - String lorem = response.body(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(response.body()); } } @@ -455,8 +397,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newInLineClient(); // should even work with no threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous, because the finisher will block. // We support this, but the executor must have enough threads. BodyHandler> handler = new BodyHandler>() { @@ -483,17 +424,29 @@ public class GZIPInputStreamTest implements HttpServerAdapters { } }; HttpResponse> response = client.send(req, handler); - String lorem = new String(response.body().get().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().get().readAllBytes(), UTF_8)); } } + private void verifyResponse(String responseBody) { + if (!LOREM_IPSUM.equals(responseBody)) { + out.println("Response doesn't match"); + out.println("[" + LOREM_IPSUM + "] != [" + responseBody + "]"); + assertEquals(LOREM_IPSUM, responseBody); + } else { + out.println("Received expected response."); + } + } + + private HttpRequest buildRequest(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getPath().contains("/https3/")) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder.build(); + } + static final class GZIPBodyHandler implements BodyHandler { @Override public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) { @@ -565,10 +518,16 @@ public class GZIPInputStreamTest implements HttpServerAdapters { https2TestServer.addHandler(gzipHandler, "/https2/chunk/gz"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + https3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + https3TestServer.addHandler(plainHandler, "/https3/chunk/txt"); + https3TestServer.addHandler(gzipHandler, "/https3/chunk/gz"); + https3URI = "https://" + https3TestServer.serverAuthority() + "/https3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + https3TestServer.start(); } @AfterTest @@ -580,6 +539,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + https3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/HandshakeFailureTest.java b/test/jdk/java/net/httpclient/HandshakeFailureTest.java index a6db6be817a..e07dfacbd85 100644 --- a/test/jdk/java/net/httpclient/HandshakeFailureTest.java +++ b/test/jdk/java/net/httpclient/HandshakeFailureTest.java @@ -142,6 +142,7 @@ public class HandshakeFailureTest { SSLParameters params = new SSLParameters(); params.setProtocols(new String[] { tlsProtocol }); return HttpClient.newBuilder() + .version(Version.HTTP_2) .sslParameters(params) .build(); } diff --git a/test/jdk/java/net/httpclient/HeadTest.java b/test/jdk/java/net/httpclient/HeadTest.java index 5b3b1671d43..e14339a1121 100644 --- a/test/jdk/java/net/httpclient/HeadTest.java +++ b/test/jdk/java/net/httpclient/HeadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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,17 +24,12 @@ /* * @test * @bug 8203433 8276559 - * @summary (httpclient) Add tests for HEAD and 304 responses. + * @summary Tests Client handles HEAD and 304 responses correctly. * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm - * -Djdk.httpclient.HttpClient.log=trace,headers,requests - * HeadTest + * @build jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=trace,headers,requests HeadTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -44,20 +39,22 @@ import org.testng.annotations.Test; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; +import java.io.PrintStream; import java.net.URI; -import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import static java.lang.System.out; -import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.httpclient.test.lib.common.HttpServerAdapters.createClientBuilderForH3; import static org.testng.Assert.assertEquals; public class HeadTest implements HttpServerAdapters { @@ -67,10 +64,10 @@ public class HeadTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) - String httpURI; - String httpsURI; - String http2URI; - String https2URI; + HttpTestServer https3TestServer; // HTTP/3 + String httpURI, httpsURI; + String http2URI, https2URI; + String https3URI; static final String CONTENT_LEN = "300"; @@ -81,7 +78,7 @@ public class HeadTest implements HttpServerAdapters { */ static final int HTTP_NOT_MODIFIED = 304; static final int HTTP_OK = 200; - + static final PrintStream out = System.out; @DataProvider(name = "positive") public Object[][] positive() { @@ -96,28 +93,33 @@ public class HeadTest implements HttpServerAdapters { { httpURI + "transfer/", "HEAD", HTTP_OK, HTTP_1_1 }, { httpsURI + "transfer/", "HEAD", HTTP_OK, HTTP_1_1 }, // HTTP/2 - { http2URI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 }, - { https2URI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 }, - { http2URI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 }, - { https2URI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 }, - // HTTP2 forbids transfer-encoding + { http2URI, "GET", HTTP_NOT_MODIFIED, HTTP_2 }, + { https2URI, "GET", HTTP_NOT_MODIFIED, HTTP_2 }, + { http2URI, "HEAD", HTTP_OK, HTTP_2 }, + { https2URI, "HEAD", HTTP_OK, HTTP_2 }, + // HTTP/3 + { https3URI, "GET", HTTP_NOT_MODIFIED, HTTP_3 }, + { https3URI, "HEAD", HTTP_OK, HTTP_3 }, }; } @Test(dataProvider = "positive") void test(String uriString, String method, - int expResp, HttpClient.Version version) throws Exception { + int expResp, Version version) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); URI uri = URI.create(uriString); + Http3DiscoveryMode config = version.equals(HTTP_3) ? HTTP_3_URI_ONLY : null; HttpRequest.Builder requestBuilder = HttpRequest .newBuilder(uri) .version(version) + .setOption(H3_DISCOVERY, config) .method(method, HttpRequest.BodyPublishers.noBody()); doTest(requestBuilder.build(), expResp); // repeat the test this time by building the request using convenience // GET and HEAD methods requestBuilder = HttpRequest.newBuilder(uri) - .version(version); + .version(version) + .setOption(H3_DISCOVERY, config); switch (method) { case "GET" -> requestBuilder.GET(); case "HEAD" -> requestBuilder.HEAD(); @@ -128,32 +130,27 @@ public class HeadTest implements HttpServerAdapters { // issue a request with no body and verify the response code is the expected response code private void doTest(HttpRequest request, int expResp) throws Exception { - HttpClient client = HttpClient.newBuilder() - .followRedirects(Redirect.ALWAYS) - .sslContext(sslContext) - .build(); - out.println("Initial request: " + request.uri()); + try (var client = createClientBuilderForH3().followRedirects(Redirect.ALWAYS).sslContext(sslContext).build()) { + out.println("Initial request: " + request.uri()); + HttpResponse response = client.send(request, BodyHandlers.ofString()); - HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println(" Got response: " + response); - out.println(" Got response: " + response); - - assertEquals(response.statusCode(), expResp); - assertEquals(response.body(), ""); - assertEquals(response.headers().firstValue("Content-length").get(), CONTENT_LEN); - assertEquals(response.version(), request.version().get()); + assertEquals(response.statusCode(), expResp); + assertEquals(response.body(), ""); + assertEquals(response.headers().firstValue("Content-length").get(), CONTENT_LEN); + assertEquals(response.version(), request.version().get()); + } } // -- Infrastructure - + // TODO: See if test performs better with Vthreads, see H3SimplePost and H3SimpleGet @BeforeTest public void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); - InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); - httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new HeadHandler(), "/"); httpURI = "http://" + httpTestServer.serverAuthority() + "/"; @@ -168,11 +165,16 @@ public class HeadTest implements HttpServerAdapters { https2TestServer.addHandler(new HeadHandler(), "/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/"; + https3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + https3TestServer.addHandler(new HeadHandler(), "/"); + https3URI = "https://" + https3TestServer.serverAuthority() + "/"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + https3TestServer.start(); } @AfterTest @@ -181,6 +183,7 @@ public class HeadTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + https3TestServer.stop(); } static class HeadHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/HeadersLowerCaseTest.java b/test/jdk/java/net/httpclient/HeadersLowerCaseTest.java new file mode 100644 index 00000000000..26c791eaca8 --- /dev/null +++ b/test/jdk/java/net/httpclient/HeadersLowerCaseTest.java @@ -0,0 +1,231 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @summary Verify that the request/response headers of HTTP/2 and HTTP/3 + * are sent and received in lower case + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm -Djdk.internal.httpclient.debug=true HeadersLowerCaseTest + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class HeadersLowerCaseTest implements HttpServerAdapters { + + private static Set REQUEST_HEADERS; + + private HttpTestServer h2server; + private HttpTestServer h3server; + private String h2ReqURIBase; + private String h3ReqURIBase; + private SSLContext sslContext; + + @BeforeAll + public void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + h2server = HttpTestServer.create(HTTP_2, sslContext); + h2server.start(); + h2ReqURIBase = "https://" + h2server.serverAuthority(); + h2server.addHandler(new ReqHeadersVerifier(), "/h2verifyReqHeaders"); + System.out.println("HTTP/2 server listening on " + h2server.getAddress()); + + + h3server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3server.start(); + h3ReqURIBase = "https://" + h3server.serverAuthority(); + h3server.addHandler(new ReqHeadersVerifier(), "/h3verifyReqHeaders"); + System.out.println("HTTP/3 server listening on " + h3server.getAddress()); + + REQUEST_HEADERS = new HashSet<>(); + REQUEST_HEADERS.add("AbCdeFgh"); + REQUEST_HEADERS.add("PQRSTU"); + REQUEST_HEADERS.add("xyz"); + REQUEST_HEADERS.add("A1243Bde2"); + REQUEST_HEADERS.add("123243"); + REQUEST_HEADERS.add("&1bacd*^d"); + REQUEST_HEADERS.add("~!#$%^&*_+"); + } + + @AfterAll + public void afterAll() throws Exception { + if (h2server != null) { + h2server.stop(); + } + if (h3server != null) { + h3server.stop(); + } + } + + /** + * Handler which verifies that the request header names are lowercase (as mandated by the spec) + */ + private static final class ReqHeadersVerifier implements HttpTestHandler { + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + System.out.println("Verifying request headers for " + exchange.getRequestURI()); + final Set missing = new HashSet<>(REQUEST_HEADERS); + final HttpTestRequestHeaders headers = exchange.getRequestHeaders(); + for (final Map.Entry> e : headers.entrySet()) { + final String header = e.getKey(); + // check validity of (non-pseudo) header names + if (!header.startsWith(":") && !Utils.isValidLowerCaseName(header)) { + System.err.println("Header name " + header + " is not valid"); + sendResponse(exchange, 500); + return; + } + final List headerVals = e.getValue(); + if (headerVals.isEmpty()) { + System.err.println("Header " + header + " is missing value"); + sendResponse(exchange, 500); + return; + } + // the header value represents the original form of the header key held in the + // REQUEST_HEADERS set + final String originalForm = headerVals.get(0); + missing.remove(originalForm); + } + if (!missing.isEmpty()) { + System.err.println("Missing headers in request: " + missing); + sendResponse(exchange, 500); + return; + } + System.out.println("All expected headers received in lower case for " + exchange.getRequestURI()); + sendResponse(exchange, 200); + } + + private static void sendResponse(final HttpTestExchange exchange, final int statusCode) throws IOException { + final HttpTestResponseHeaders respHeaders = exchange.getResponseHeaders(); + // we just send the pre-defined (request) headers back as the response headers + for (final String k : REQUEST_HEADERS) { + respHeaders.addHeader(k, k); + } + exchange.sendResponseHeaders(statusCode, 0); + } + } + + private Stream params() throws Exception { + return Stream.of( + Arguments.of(HTTP_2, new URI(h2ReqURIBase + "/h2verifyReqHeaders")), + Arguments.of(Version.HTTP_3, new URI(h3ReqURIBase + "/h3verifyReqHeaders"))); + } + + /** + * Issues a HTTP/2 or HTTP/3 request with header names of varying case (some in lower, + * some mixed, some upper case) and expects that the client internally converts them + * to lower case before encoding and sending to the server. The server side handler verifies + * that it receives the header names in lower case and if it doesn't then it returns a + * non-200 response + */ + @ParameterizedTest + @MethodSource("params") + public void testRequestHeaders(final Version version, final URI requestURI) throws Exception { + try (final HttpClient client = newClientBuilderForH3() + .version(version) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY).build()) { + Http3DiscoveryMode config = switch (version) { + case HTTP_3 -> HTTP_3_URI_ONLY; + default -> ALT_SVC; + }; + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(requestURI) + .setOption(H3_DISCOVERY, config) + .version(version); + for (final String k : REQUEST_HEADERS) { + reqBuilder.header(k, k); + } + final HttpRequest req = reqBuilder.build(); + System.out.println("Issuing " + version + " request to " + requestURI); + final HttpResponse resp = client.send(req, BodyHandlers.discarding()); + assertEquals(resp.version(), version, "Unexpected HTTP version in response"); + assertEquals(resp.statusCode(), 200, "Unexpected response code"); + // now try with async + System.out.println("Issuing (async) request to " + requestURI); + final CompletableFuture> futureResp = client.sendAsync(req, + BodyHandlers.discarding()); + final HttpResponse asyncResp = futureResp.get(); + assertEquals(asyncResp.version(), version, "Unexpected HTTP version in response"); + assertEquals(asyncResp.statusCode(), 200, "Unexpected response code"); + } + } + + /** + * Verifies that when a HTTP/2 or HTTP/3 request is being built using + * {@link HttpRequest.Builder}, only valid header names are allowed to be added to the request + */ + @ParameterizedTest + @MethodSource("params") + public void testInvalidHeaderName(final Version version, final URI requestURI) throws Exception { + Http3DiscoveryMode config = switch (version) { + case HTTP_3 -> HTTP_3_URI_ONLY; + default -> ALT_SVC; + }; + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(requestURI) + .setOption(H3_DISCOVERY, config) + .version(version); + final String copyrightSign = new String(Character.toChars(0x00A9)); // copyright sign + final String invalidHeaderName = "abcd" + copyrightSign; + System.out.println("Adding header name " + invalidHeaderName + " to " + version + " request"); + // Field names are strings containing a subset of ASCII characters. + // This header name contains a unicode character, so it should fail + assertThrows(IllegalArgumentException.class, + () -> reqBuilder.header(invalidHeaderName, "something")); + } +} diff --git a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java index 451221c6e23..596320d49e6 100644 --- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2024, 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 @@ -57,7 +57,7 @@ import static org.testng.Assert.*; /* * @test - * @bug 8209137 8326233 + * @bug 8209137 8326233 8367112 * @summary HttpClient[.Builder] API and behaviour checks * @library /test/lib * @build jdk.test.lib.net.SimpleSSLContext @@ -271,6 +271,20 @@ public class HttpClientBuilderTest { try (var closer = closeable(builder)) { assertTrue(closer.build().sslParameters().getProtocols()[0].equals("C")); } + SSLParameters d = new SSLParameters(); + d.setSignatureSchemes(new String[] { "C" }); + builder.sslParameters(d); + d.setSignatureSchemes(new String[] { "D" }); + try (var closer = closeable(builder)) { + assertTrue(closer.build().sslParameters().getSignatureSchemes()[0].equals("C")); + } + SSLParameters e = new SSLParameters(); + e.setNamedGroups(new String[] { "C" }); + builder.sslParameters(e); + e.setNamedGroups(new String[] { "D" }); + try (var closer = closeable(builder)) { + assertTrue(closer.build().sslParameters().getNamedGroups()[0].equals("C")); + } // test defaults for needClientAuth and wantClientAuth builder.sslParameters(new SSLParameters()); try (var closer = closeable(builder)) { @@ -343,6 +357,13 @@ public class HttpClientBuilderTest { @Test public void testVersion() { HttpClient.Builder builder = HttpClient.newBuilder(); + try (var closer = closeable(builder)) { + assertTrue(closer.build().version() == Version.HTTP_2); + } + builder.version(Version.HTTP_3); + try (var closer = closeable(builder)) { + assertTrue(closer.build().version() == Version.HTTP_3); + } builder.version(Version.HTTP_2); try (var closer = closeable(builder)) { assertTrue(closer.build().version() == Version.HTTP_2); @@ -352,6 +373,10 @@ public class HttpClientBuilderTest { assertTrue(closer.build().version() == Version.HTTP_1_1); } assertThrows(NPE, () -> builder.version(null)); + builder.version(Version.HTTP_3); + try (var closer = closeable(builder)) { + assertTrue(closer.build().version() == Version.HTTP_3); + } builder.version(Version.HTTP_2); try (var closer = closeable(builder)) { assertTrue(closer.build().version() == Version.HTTP_2); diff --git a/test/jdk/java/net/httpclient/HttpClientClose.java b/test/jdk/java/net/httpclient/HttpClientClose.java index 0425d27a25e..f761396487c 100644 --- a/test/jdk/java/net/httpclient/HttpClientClose.java +++ b/test/jdk/java/net/httpclient/HttpClientClose.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 @@ -46,7 +46,9 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; @@ -57,13 +59,11 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Flow; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.Flow.Subscriber; import java.util.concurrent.Flow.Subscription; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; import javax.net.ssl.SSLContext; @@ -75,12 +75,14 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.lang.System.err; -import static java.lang.System.in; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -100,10 +102,15 @@ public class HttpClientClose implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "HttpClientClose message body"; static final int ITERATIONS = 3; @@ -111,10 +118,12 @@ public class HttpClientClose implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -160,19 +169,39 @@ public class HttpClientClose implements HttpServerAdapters { } } - private record ExchangeResult(int step, HttpResponse response) { - public static ExchangeResult ofStep(int step) { - return new ExchangeResult(step, null); + record ExchangeResult(int step, + Version version, + Http3DiscoveryMode config, + HttpResponse response, + boolean firstVersionMayNotMatch) { + + static ExchangeResult afterHead(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, false); } + + static ExchangeResult ofSequential(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, true); + } + ExchangeResult withResponse(HttpResponse response) { - return new ExchangeResult(step, response); + return new ExchangeResult(step(), version(), config(), response, firstVersionMayNotMatch()); } + + // Ensures that the input stream gets closed in case of assertion ExchangeResult assertResponseState() { + out.println(step + ": Got response: " + response); try { - out.println(step + ": Got response: " + response); + out.printf("%s: expect status 200 and version %s (%s) for %s%n", step, version, config, + response.request().uri()); assertEquals(response.statusCode(), 200); + if (step == 0 && version == HTTP_3 && firstVersionMayNotMatch) { + out.printf("%s: version not checked%n", step); + } else { + assertEquals(response.version(), version); + out.printf("%s: got expected version %s%n", step, response.version()); + } } catch (AssertionError error) { - out.printf("%s: Closing body due to assertion - %s", error); + out.printf("%s: Closing body due to assertion - %s", step, error); ensureClosed(this); throw error; } @@ -180,35 +209,62 @@ public class HttpClientClose implements HttpServerAdapters { } } + static String readBody(int i, HttpResponse resp) { + try (var in = resp.body()) { + out.println(i + ": reading body for " + resp.request().uri()); + var body = new String(in.readAllBytes(), StandardCharsets.UTF_8); + out.println(i + ": got body " + body); + return body; + } catch (IOException io) { + out.println(i + ": failed to read body"); + throw new UncheckedIOException(io); + } + } + + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting concurrent (%s) ----%n%n", uriString); + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting concurrent (%s, %s, %s) ----%n%n", uriString, version, config); Throwable failed = null; HttpClient toCheck = null; List> bodies = new ArrayList<>(); - try (HttpClient client = toCheck = HttpClient.newBuilder() + try (HttpClient client = toCheck = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build()) { TRACKER.track(client); + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; CompletableFuture bodyCF; final int si = i; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.afterHead(i, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) .thenApply(result::withResponse) .thenApplyAsync(ExchangeResult::assertResponseState, readerService) .thenApply(ExchangeResult::response); - bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) - .thenApply(HttpClientClose::readBody) + bodyCF = responseCF + .thenApplyAsync((resp) -> readBody(si, resp), readerService) .thenApply((s) -> { assertEquals(s, MESSAGE); return s; @@ -223,18 +279,25 @@ public class HttpClientClose implements HttpServerAdapters { } } assertTrue(toCheck.isTerminated()); - // assert all operations eventually terminate + + // Ensure all CF are eventually completed + out.printf("waiting for requests to complete%n"); CompletableFuture.allOf(bodies.toArray(new CompletableFuture[0])).get(); + out.printf("all requests completed%n"); + out.printf("%n---- end concurrent (%s, %s, %s): %s ----%n", + uriString, version, config, + failed == null ? "done" : failed.toString()); } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting sequential (%s) ----%n%n", uriString); + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting sequential (%s, %s, %s) ----%n%n", uriString, version, config); Throwable failed = null; HttpClient toCheck = null; - try (HttpClient client = toCheck = HttpClient.newBuilder() + try (HttpClient client = toCheck = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build()) { TRACKER.track(client); @@ -243,10 +306,11 @@ public class HttpClientClose implements HttpServerAdapters { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); final int si = i; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.ofSequential(si, version, config); CompletableFuture> responseCF; CompletableFuture bodyCF; responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) @@ -283,6 +347,7 @@ public class HttpClientClose implements HttpServerAdapters { if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); readerService = Executors.newCachedThreadPool(); + httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new ServerRequestHandler(), "/http1/exec/"); httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/exec/retry"; @@ -297,10 +362,21 @@ public class HttpClientClose implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -313,6 +389,8 @@ public class HttpClientClose implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/HttpClientShutdown.java b/test/jdk/java/net/httpclient/HttpClientShutdown.java index 3c77881c8aa..192abcba39d 100644 --- a/test/jdk/java/net/httpclient/HttpClientShutdown.java +++ b/test/jdk/java/net/httpclient/HttpClientShutdown.java @@ -47,7 +47,9 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -77,11 +79,14 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -101,10 +106,15 @@ public class HttpClientShutdown implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "HttpClientShutdown message body"; static final int ITERATIONS = 3; @@ -112,10 +122,12 @@ public class HttpClientShutdown implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -180,17 +192,37 @@ public class HttpClientShutdown implements HttpServerAdapters { } } - private record ExchangeResult(int step, HttpResponse response) { - public static ExchangeResult ofStep(int step) { - return new ExchangeResult(step, null); + record ExchangeResult(int step, + Version version, + Http3DiscoveryMode config, + HttpResponse response, + boolean firstVersionMayNotMatch) { + + static ExchangeResult afterHead(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, false); } + + static ExchangeResult ofSequential(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, true); + } + ExchangeResult withResponse(HttpResponse response) { - return new ExchangeResult(step, response); + return new ExchangeResult(step(), version(), config(), response, firstVersionMayNotMatch()); } + + // Ensures that the input stream gets closed in case of assertion ExchangeResult assertResponseState() { + out.println(now() + step + ": Got response: " + response); try { - out.println(now() + step + ": Got response: " + response); + out.printf(now() + "%s: expect status 200 and version %s (%s) for %s%n", step, version, config, + response.request().uri()); assertEquals(response.statusCode(), 200); + if (step == 0 && version == HTTP_3 && firstVersionMayNotMatch) { + out.printf(now() + "%s: version not checked%n", step); + } else { + assertEquals(response.version(), version); + out.printf(now() + "%s: got expected version %s%n", step, response.version()); + } } catch (AssertionError error) { out.printf(now() + "%s: Closing body due to assertion - %s", step, error); ensureClosed(this); @@ -200,6 +232,15 @@ public class HttpClientShutdown implements HttpServerAdapters { } } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + static boolean hasExpectedMessage(IOException io) { String message = io.getMessage(); if (message == null) return false; @@ -232,11 +273,13 @@ public class HttpClientShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- %sstarting concurrent (%s) ----%n%n", now(), uriString); - HttpClient client = HttpClient.newBuilder() + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- %sstarting concurrent (%s, %s, %s) ----%n%n", + now(), uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -245,19 +288,24 @@ public class HttpClientShutdown implements HttpServerAdapters { Throwable failed = null; List> bodies = new ArrayList<>(); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf(now() + "Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; CompletableFuture bodyCF; final int si = i; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.afterHead(si, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) .thenApply(result::withResponse) - .thenApplyAsync(ExchangeResult::assertResponseState, readerService) + .thenApplyAsync(ExchangeResult::assertResponseState) .thenApply(ExchangeResult::response); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(HttpClientShutdown::readBody) @@ -328,11 +376,13 @@ public class HttpClientShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- %sstarting sequential (%s) ----%n%n", now(), uriString); - HttpClient client = HttpClient.newBuilder() + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- %sstarting sequential (%s, %s, %s) ----%n%n", + now(), uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -345,12 +395,13 @@ public class HttpClientShutdown implements HttpServerAdapters { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf(now() + "Iteration %d request: %s%n", i, request.uri()); final int si = i; CompletableFuture> responseCF; CompletableFuture bodyCF; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.ofSequential(si, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) .thenApply(result::withResponse) .thenApplyAsync(ExchangeResult::assertResponseState, readerService) @@ -432,10 +483,21 @@ public class HttpClientShutdown implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); start = System.nanoTime(); } @@ -449,6 +511,8 @@ public class HttpClientShutdown implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java b/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java index baa64356fa7..4ddc0bdfdc2 100644 --- a/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java +++ b/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; @@ -30,6 +31,8 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; import java.net.http.HttpResponse; import java.time.Duration; import java.util.List; @@ -47,20 +50,29 @@ import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; + +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 java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /* * @test * @bug 8316580 - * @library /test/lib + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build HttpGetInCancelledFuture ReferenceTracker * @run junit/othervm -DuseReferenceTracker=false * HttpGetInCancelledFuture * @run junit/othervm -DuseReferenceTracker=true @@ -82,7 +94,9 @@ public class HttpGetInCancelledFuture { static ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; HttpClient makeClient(URI uri, Version version, Executor executor) { - var builder = HttpClient.newBuilder(); + var builder = version == HTTP_3 + ? HttpServerAdapters.createClientBuilderForH3() + : HttpClient.newBuilder(); if (uri.getScheme().equalsIgnoreCase("https")) { try { builder.sslContext(new SimpleSSLContext().get()); @@ -96,9 +110,19 @@ public class HttpGetInCancelledFuture { .build(); } - record TestCase(String url, int reqCount, Version version) {} + record TestCase(String url, int reqCount, Version version, Http3DiscoveryMode config) { + TestCase(String url, int reqCount, Version version) { + this(url, reqCount, version, null); + } + TestCase(String url, int reqCount, Http3DiscoveryMode config) { + this(url, reqCount, HTTP_3, null); + } + } + + // A server that doesn't accept - static volatile ServerSocket NOT_ACCEPTING; + static volatile ServerSocket NOT_ACCEPTING; + static volatile DatagramSocket NOT_RESPONDING; static List parameters() { ServerSocket ss = NOT_ACCEPTING; @@ -116,6 +140,28 @@ public class HttpGetInCancelledFuture { } } } + + DatagramSocket ds = NOT_RESPONDING; + boolean sameport = false; + if (ds == null) { + synchronized (HttpGetInCancelledFuture.class) { + if ((ds = NOT_RESPONDING) == null) { + try { + var loopback = InetAddress.getLoopbackAddress(); + try { + ds = new DatagramSocket(new InetSocketAddress(loopback, ss.getLocalPort())); + sameport = true; + } catch (IOException io) { + ds = new DatagramSocket(new InetSocketAddress(loopback,0)); + } + NOT_RESPONDING = ds; + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + } + } + URI http = URIBuilder.newBuilder() .loopback() .scheme("http") @@ -128,13 +174,25 @@ public class HttpGetInCancelledFuture { .port(ss.getLocalPort()) .path("/not-accepting/") .buildUnchecked(); + URI https3 = URIBuilder.newBuilder() + .loopback() + .scheme("https") + .port(ds.getLocalPort()) + .path("/not-responding/") + .buildUnchecked(); // use all HTTP versions, without and with TLS - return List.of( - new TestCase(http.toString(), 200, Version.HTTP_2), - new TestCase(http.toString(), 200, Version.HTTP_1_1), - new TestCase(https.toString(), 200, Version.HTTP_2), - new TestCase(https.toString(), 200, Version.HTTP_1_1) + var def = Stream.of( + new TestCase(https3.toString(), 200, HTTP_3_URI_ONLY), + new TestCase(http.toString(), 200, HTTP_2), + new TestCase(http.toString(), 200, HTTP_1_1), + new TestCase(https.toString(), 200, HTTP_2), + new TestCase(https.toString(), 200, HTTP_1_1) ); + var first = sameport + ? Stream.of(new TestCase(https3.toString(), 200, ANY)) + : Stream.empty(); + var cases= Stream.concat(first, def); + return cases.toList(); } @ParameterizedTest @@ -251,7 +309,7 @@ public class HttpGetInCancelledFuture { Throwable failed = null; try { try (final var scope = new TestTaskScope.ShutdownOnFailure()) { - launchAndProcessRequests(scope, httpClient, reqCount, dest); + launchAndProcessRequests(scope, httpClient, reqCount, version, dest); } finally { System.out.printf("StructuredTaskScope closed: STARTED=%s, SUCCESS=%s, INTERRUPT=%s, FAILED=%s%n", STARTED.get(), SUCCESS.get(), INTERRUPT.get(), FAILED.get()); @@ -311,10 +369,11 @@ public class HttpGetInCancelledFuture { TestTaskScope.ShutdownOnFailure scope, HttpClient httpClient, int reqCount, + Version version, URI dest) { for (int counter = 0; counter < reqCount; counter++) { scope.fork(() -> - getAndCheck(httpClient, dest) + getAndCheck(httpClient, dest, version) ); } try { @@ -335,19 +394,21 @@ public class HttpGetInCancelledFuture { final AtomicLong FAILED = new AtomicLong(); final AtomicLong STARTED = new AtomicLong(); final CopyOnWriteArrayList EXCEPTIONS = new CopyOnWriteArrayList<>(); - private String getAndCheck(HttpClient httpClient, URI url) { + private String getAndCheck(HttpClient httpClient, URI url, Version version) { STARTED.incrementAndGet(); - final var response = sendRequest(httpClient, url); + final var response = sendRequest(httpClient, url, version); String res = response.body(); int statusCode = response.statusCode(); assertEquals(200, statusCode); return res; } - private HttpResponse sendRequest(HttpClient httpClient, URI url) { + private HttpResponse sendRequest(HttpClient httpClient, URI url, Version version) { var id = ID.incrementAndGet(); try { - var request = HttpRequest.newBuilder(url).GET().build(); + var builder = HttpRequest.newBuilder(url).version(version).GET(); + if (version == HTTP_3) builder.setOption(HttpOption.H3_DISCOVERY, HTTP_3_URI_ONLY); + var request = builder.build(); var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // System.out.println("Got response for " + id + ": " + response); SUCCESS.incrementAndGet(); @@ -372,16 +433,18 @@ public class HttpGetInCancelledFuture { if (error != null) throw error; } finally { ServerSocket ss; + DatagramSocket ds; synchronized (HttpGetInCancelledFuture.class) { ss = NOT_ACCEPTING; NOT_ACCEPTING = null; + ds = NOT_RESPONDING; + NOT_RESPONDING = null; } - if (ss != null) { - try { - ss.close(); - } catch (IOException io) { - throw new UncheckedIOException(io); - } + try (var ss1 = ss; var ds1 = ds;) { + System.out.printf("Cleaning up: ss=%s, ds=%s%n", + ss1.getLocalSocketAddress(), ds1.getLocalSocketAddress()); + } catch (IOException io) { + throw new UncheckedIOException(io); } } } diff --git a/test/jdk/java/net/httpclient/HttpRedirectTest.java b/test/jdk/java/net/httpclient/HttpRedirectTest.java index dedcc36dda7..358b908a03c 100644 --- a/test/jdk/java/net/httpclient/HttpRedirectTest.java +++ b/test/jdk/java/net/httpclient/HttpRedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, 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 @@ -29,6 +29,9 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.*; import javax.net.ssl.SSLContext; @@ -93,12 +96,14 @@ public class HttpRedirectTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -111,8 +116,7 @@ public class HttpRedirectTest implements HttpServerAdapters { TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // Used by the client public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .followRedirects(HttpClient.Redirect.ALWAYS) @@ -123,6 +127,7 @@ public class HttpRedirectTest implements HttpServerAdapters { @DataProvider(name="uris") Object[][] testURIs() throws URISyntaxException { List uris = List.of( + http3URI.resolve("direct/orig/"), http1URI.resolve("direct/orig/"), https1URI.resolve("direct/orig/"), https1URI.resolve("proxy/orig/"), @@ -195,6 +200,13 @@ public class HttpRedirectTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/HttpRedirectTest/https2/"); + // HTTPS/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestRedirectHandler("https", http3Server), + "/HttpRedirectTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/HttpRedirectTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -255,10 +267,18 @@ public class HttpRedirectTest implements HttpServerAdapters { } } + private HttpRequest.Builder newRequestBuilder(URI u) { + var builder = HttpRequest.newBuilder(u); + if (u.getRawPath().contains("/http3/")) { + builder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "uris") public void testPOST(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .POST(HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); // POST is not considered idempotent. testNonIdempotent(u, request, code, method); @@ -268,7 +288,7 @@ public class HttpRedirectTest implements HttpServerAdapters { public void testPUT(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); System.out.println("Testing with " + u); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .PUT(HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); // PUT is considered idempotent. testIdempotent(u, request, code, method); @@ -278,7 +298,7 @@ public class HttpRedirectTest implements HttpServerAdapters { public void testFoo(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); System.out.println("Testing with " + u); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .method("FOO", HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); // FOO is considered idempotent. @@ -289,7 +309,7 @@ public class HttpRedirectTest implements HttpServerAdapters { public void testGet(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); System.out.println("Testing with " + u); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .method("GET", HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); CompletableFuture> respCf = @@ -320,6 +340,7 @@ public class HttpRedirectTest implements HttpServerAdapters { https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java new file mode 100644 index 00000000000..29276dc7b60 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java @@ -0,0 +1,91 @@ +/* + * 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. + */ + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public final class ByteBufferUtils { + + private ByteBufferUtils() {} + + public static void assertEquals(ByteBuffer expectedBuffer, ByteBuffer actualBuffer, String message) { + assertEquals(bytes(expectedBuffer), bytes(actualBuffer), message); + } + + public static void assertEquals(byte[] expectedBytes, ByteBuffer actualBuffer, String message) { + assertEquals(expectedBytes, bytes(actualBuffer), message); + } + + public static void assertEquals(byte[] expectedBytes, byte[] actualBytes, String message) { + Objects.requireNonNull(expectedBytes); + Objects.requireNonNull(actualBytes); + int mismatchIndex = Arrays.mismatch(expectedBytes, actualBytes); + if (mismatchIndex >= 0) { + Byte expectedByte = mismatchIndex >= expectedBytes.length ? null : expectedBytes[mismatchIndex]; + Byte actualByte = mismatchIndex >= actualBytes.length ? null : actualBytes[mismatchIndex]; + String extendedMessage = String.format( + "%s" + + "array contents differ at index [%s], expected: <%s> but was: <%s>%n" + + "expected: %s%n" + + "actual: %s%n", + message == null ? "" : (message + ": "), + mismatchIndex, expectedByte, actualByte, + prettyPrintBytes(expectedBytes), + prettyPrintBytes(actualBytes)); + throw new AssertionError(extendedMessage); + } + } + + private static byte[] bytes(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + return bytes; + } + + private static String prettyPrintBytes(byte[] bytes) { + return IntStream.range(0, bytes.length) + .mapToObj(i -> "" + bytes[i]) + .collect(Collectors.joining(", ", "[", "]")); + } + + public static int findLengthExceedingMaxMemory() { + long memoryLength = Runtime.getRuntime().maxMemory(); + double length = Math.ceil(1.5D * memoryLength); + if (length < 1 || length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Bogus or excessive memory: " + memoryLength); + } + return (int) length; + } + + public static byte[] byteArrayOfLength(int length) { + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + bytes[i] = (byte) i; + } + return bytes; + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java new file mode 100644 index 00000000000..f05eab0a2e5 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java @@ -0,0 +1,100 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.http.HttpRequest; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::fromPublisher` behavior + * @build RecordingSubscriber + * @run junit FromPublisherTest + */ + +class FromPublisherTest { + + @Test + void testNullPublisher() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.fromPublisher(null)); + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.fromPublisher(null, 1)); + } + + @ParameterizedTest + @ValueSource(longs = {0L, -1L, Long.MIN_VALUE}) + void testInvalidContentLength(long contentLength) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> HttpRequest.BodyPublishers.fromPublisher(null, contentLength)); + String exceptionMessage = exception.getMessage(); + assertTrue( + exceptionMessage.contains("non-positive contentLength"), + "Unexpected exception message: " + exceptionMessage); + } + + @ParameterizedTest + @ValueSource(longs = {1, 2, 3, 4}) + void testValidContentLength(long contentLength) { + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.noBody(), contentLength); + assertEquals(contentLength, publisher.contentLength()); + } + + @Test + void testNoContentLength() { + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.noBody()); + assertEquals(-1, publisher.contentLength()); + } + + @Test + void testNullSubscriber() { + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.noBody()); + assertThrows(NullPointerException.class, () -> publisher.subscribe(null)); + } + + @Test + void testDelegation() throws InterruptedException { + BlockingQueue publisherInvocations = new LinkedBlockingQueue<>(); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.fromPublisher(subscriber -> { + publisherInvocations.add("subscribe"); + publisherInvocations.add(subscriber); + }); + RecordingSubscriber subscriber = new RecordingSubscriber(); + publisher.subscribe(subscriber); + assertEquals("subscribe", publisherInvocations.take()); + assertEquals(subscriber, publisherInvocations.take()); + assertTrue(subscriber.invocations.isEmpty()); + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java new file mode 100644 index 00000000000..f58e9505c9a --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java @@ -0,0 +1,57 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.Test; + +import java.net.http.HttpRequest; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::noBody` behavior + * @build RecordingSubscriber + * @run junit NoBodyTest + */ + +class NoBodyTest { + + @Test + void test() throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.noBody(); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, 0); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + assertEquals("onComplete", subscriber.invocations.take()); + + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java new file mode 100644 index 00000000000..9973272b435 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java @@ -0,0 +1,134 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofByteArray` behavior + * @build RecordingSubscriber + * @run junit OfByteArrayTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Djdk.httpclient.bufsize=-1 OfByteArrayTest testInvalidBufferSize + * @run main/othervm -Djdk.httpclient.bufsize=0 OfByteArrayTest testInvalidBufferSize + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking "" 0 0 "" + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 0 0 "" + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 1 0 "" + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 0 1 a + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking ab 0 1 a + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking ab 1 1 b + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking ab 0 2 ab + * @run main/othervm -Djdk.httpclient.bufsize=1 OfByteArrayTest testChunking abc 0 3 a:b:c + * @run main/othervm -Djdk.httpclient.bufsize=2 OfByteArrayTest testChunking abc 0 3 ab:c + * @run main/othervm -Djdk.httpclient.bufsize=2 OfByteArrayTest testChunking abcdef 2 4 cd:ef + */ + +public class OfByteArrayTest { + + private static final Charset CHARSET = StandardCharsets.US_ASCII; + + @Test + void testNullContent() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofByteArray(null)); + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofByteArray(null, 1, 2)); + } + + @ParameterizedTest + @CsvSource({ + "abc,-1,1", // Negative offset + "abc,1,-1", // Negative length + "'',1,1", // Offset overflow on empty string + "a,2,1", // Offset overflow + "'',0,1", // Length overflow on empty string + "a,0,2", // Length overflow + }) + void testInvalidOffsetOrLength(String contentText, int offset, int length) { + byte[] content = contentText.getBytes(CHARSET); + assertThrows( + IndexOutOfBoundsException.class, + () -> HttpRequest.BodyPublishers.ofByteArray(content, offset, length)); + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws InterruptedException { + switch (args[0]) { + case "testInvalidBufferSize" -> testInvalidBufferSize(); + case "testChunking" -> testChunking( + parseStringArg(args[1]), + Integer.parseInt(args[2]), + Integer.parseInt(args[3]), + parseStringArg(args[4])); + default -> throw new IllegalArgumentException("Unexpected arguments: " + List.of(args)); + } + } + + private static String parseStringArg(String arg) { + return arg == null || arg.trim().equals("\"\"") ? "" : arg; + } + + private static void testInvalidBufferSize() { + assertThrows(IllegalArgumentException.class, () -> HttpRequest.BodyPublishers.ofByteArray(new byte[1])); + } + + private static void testChunking( + String contentText, int offset, int length, String expectedBuffersText) + throws InterruptedException { + + // Create the publisher + byte[] content = contentText.getBytes(CHARSET); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArray(content, offset, length); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, length); + + // Verify the state after `request()` + String[] expectedBuffers = expectedBuffersText.isEmpty() ? new String[0] : expectedBuffersText.split(":"); + subscription.request(Long.MAX_VALUE); + for (int bufferIndex = 0; bufferIndex < expectedBuffers.length; bufferIndex++) { + assertEquals("onNext", subscriber.invocations.take()); + String actualBuffer = CHARSET.decode((ByteBuffer) subscriber.invocations.take()).toString(); + String expectedBuffer = expectedBuffers[bufferIndex]; + assertEquals(expectedBuffer, actualBuffer, "buffer mismatch at index " + bufferIndex); + } + assertEquals("onComplete", subscriber.invocations.take()); + + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java new file mode 100644 index 00000000000..6b852407907 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java @@ -0,0 +1,317 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Flow; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofByteArrays` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfByteArraysTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Xmx64m OfByteArraysTest testOOM + */ + +public class OfByteArraysTest { + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testIteratorOfLength(int length) throws InterruptedException { + + // Create the publisher + List buffers = IntStream + .range(0, length) + .mapToObj(i -> new byte[]{(byte) i}) + .toList(); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(buffers::iterator); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + for (int bufferIndex = 0; bufferIndex < length; bufferIndex++) { + assertEquals("onNext", subscriber.invocations.take()); + byte[] expectedBuffer = buffers.get(bufferIndex); + ByteBuffer actualBuffer = (ByteBuffer) subscriber.invocations.take(); + ByteBufferUtils.assertEquals(expectedBuffer, actualBuffer, "buffer mismatch at index " + bufferIndex); + } + assertEquals("onComplete", subscriber.invocations.take()); + + } + + @Test + void testDifferentIterators() throws InterruptedException { + + // Create a publisher using an iterable that returns a different iterator at each invocation + byte[] buffer1 = ByteBufferUtils.byteArrayOfLength(9); + byte[] buffer2 = ByteBufferUtils.byteArrayOfLength(9); + int[] iteratorRequestCount = {0}; + Iterable iterable = () -> switch (++iteratorRequestCount[0]) { + case 1 -> List.of(buffer1).iterator(); + case 2 -> List.of(buffer2).iterator(); + default -> throw new AssertionError(); + }; + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(iterable); + + // Subscribe twice (to force two `Iterable::iterator` invocations) + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription1 = subscriber.verifyAndSubscribe(publisher, -1); + Flow.Subscription subscription2 = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the content + byte[] actualBuffer1 = subscriber.drainToByteArray(subscription1, Long.MAX_VALUE); + byte[] actualBuffer2 = subscriber.drainToByteArray(subscription2, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(buffer1, actualBuffer1, null); + ByteBufferUtils.assertEquals(buffer2, actualBuffer2, null); + + } + + @Test + void testNullIterable() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofByteArrays(null)); + } + + @Test + void testNullIterator() throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(() -> null); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the NPE + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + assertInstanceOf(NullPointerException.class, subscriber.invocations.take()); + + } + + @Test + void testNullArray() throws InterruptedException { + + // Create the publisher + List iterable = new ArrayList<>(); + iterable.add(null); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(iterable); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the NPE + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + assertInstanceOf(NullPointerException.class, subscriber.invocations.take()); + + } + + @Test + void testThrowingIterable() throws InterruptedException { + + // Create the publisher + RuntimeException exception = new RuntimeException("failure for `testIteratorCreationException`"); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(() -> { + throw exception; + }); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the failure + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + Exception actualException = (Exception) subscriber.invocations.take(); + assertSame(exception, actualException); + + } + + static Stream testThrowingIteratorArgs() { + RuntimeException hasNextException = new RuntimeException("failure for `hasNext`"); + RuntimeException nextException = new RuntimeException("failure for `next`"); + return Stream.of( + Arguments.of(0, hasNextException, null, hasNextException), + Arguments.of(0, hasNextException, nextException, hasNextException), + Arguments.of(1, hasNextException, null, hasNextException), + Arguments.of(1, hasNextException, nextException, hasNextException), + Arguments.of(1, null, nextException, nextException)); + } + + @ParameterizedTest + @MethodSource("testThrowingIteratorArgs") + void testThrowingIterator( + int exceptionIndex, RuntimeException hasNextException, RuntimeException nextException, Exception expectedException) + throws InterruptedException { + + // Create the publisher + IteratorThrowingAtEnd iterator = + new IteratorThrowingAtEnd(exceptionIndex, hasNextException, nextException); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(() -> iterator); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain successful emissions + subscription.request(Long.MAX_VALUE); + for (int itemIndex = 0; itemIndex < exceptionIndex; itemIndex++) { + assertEquals("onNext", subscriber.invocations.take()); + ByteBuffer actualBuffer = (ByteBuffer) subscriber.invocations.take(); + ByteBuffer expectedBuffer = ByteBuffer.wrap(iterator.content, itemIndex, 1); + ByteBufferUtils.assertEquals(expectedBuffer, actualBuffer, null); + } + + // Verify the result + if (expectedException == null) { + assertEquals("onComplete", subscriber.invocations.take()); + } else { + assertEquals("onError", subscriber.invocations.take()); + Exception actualException = (Exception) subscriber.invocations.take(); + assertSame(expectedException, actualException); + } + + } + + private static final class IteratorThrowingAtEnd implements Iterator { + + private final byte[] content; + + private final RuntimeException hasNextException; + + private final RuntimeException nextException; + + private int position; + + private IteratorThrowingAtEnd( + int length, + RuntimeException hasNextException, + RuntimeException nextException) { + this.content = ByteBufferUtils.byteArrayOfLength(length); + this.hasNextException = hasNextException; + this.nextException = nextException; + } + + @Override + public synchronized boolean hasNext() { + if (position >= content.length && hasNextException != null) { + throw hasNextException; + } + // We always instruct to proceed, so `next()` can throw + return true; + } + + @Override + public synchronized byte[] next() { + if (position < content.length) { + return new byte[]{content[position++]}; + } + assertNotNull(nextException); + throw nextException; + } + + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws Exception { + if ("testOOM".equals(args[0])) { + testOOM(); + } else { + throw new IllegalArgumentException("Unknown arguments: " + List.of(args)); + } + } + + private static void testOOM() throws Exception { + + // Create the publisher + int length = ByteBufferUtils.findLengthExceedingMaxMemory(); + Iterable iterable = createIterableOfLength(length); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(iterable); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the received content length + final int[] readLength = {0}; + subscriber.drainToAccumulator(subscription, 1, buffer -> readLength[0] += buffer.limit()); + assertEquals(length, readLength[0]); + + } + + private static Iterable createIterableOfLength(int length) { + return () -> new Iterator<>() { + + // Instead of emitting `length` at once, doing it gradually using a buffer to avoid OOM. + private final byte[] buffer = new byte[8192]; + + private volatile int remainingLength = length; + + @Override + public boolean hasNext() { + return remainingLength > 0; + } + + @Override + public synchronized byte[] next() { + if (remainingLength >= buffer.length) { + remainingLength -= buffer.length; + return buffer; + } else { + byte[] remainingBuffer = new byte[remainingLength]; + remainingLength = 0; + return remainingBuffer; + } + } + + }; + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java new file mode 100644 index 00000000000..7705faef109 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java @@ -0,0 +1,290 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Flow; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofFile` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfFileTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Xmx64m OfFileTest testOOM + */ + +public class OfFileTest { + + private static final Path DEFAULT_FS_DIR = Path.of(System.getProperty("user.dir", ".")); + + private static final FileSystem ZIP_FS = zipFs(); + + private static final Path ZIP_FS_DIR = ZIP_FS.getRootDirectories().iterator().next(); + + private static final List PARENT_DIRS = List.of(DEFAULT_FS_DIR, ZIP_FS_DIR); + + private static FileSystem zipFs() { + try { + Path zipFile = DEFAULT_FS_DIR.resolve("file.zip"); + return FileSystems.newFileSystem(zipFile, Map.of("create", "true")); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + @AfterAll + static void closeZipFs() throws IOException { + ZIP_FS.close(); + } + + static List parentDirs() { + return PARENT_DIRS; + } + + @Test + void testNullPath() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofFile(null)); + } + + @ParameterizedTest + @MethodSource("parentDirs") + void testNonExistentPath(Path parentDir) { + Path nonExistentPath = createFilePath(parentDir, "testNonExistentPath"); + assertThrows(FileNotFoundException.class, () -> HttpRequest.BodyPublishers.ofFile(nonExistentPath)); + } + + @ParameterizedTest + @MethodSource("parentDirs") + void testNonExistentPathAtSubscribe(Path parentDir) throws Exception { + + // Create the publisher + byte[] fileBytes = ByteBufferUtils.byteArrayOfLength(3); + Path filePath = createFile(parentDir, "testNonExistentPathAtSubscribe", fileBytes); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Delete the file + Files.delete(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileBytes.length); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + FileNotFoundException actualException = (FileNotFoundException) subscriber.invocations.take(); + String actualExceptionMessage = actualException.getMessage(); + assertTrue( + actualExceptionMessage.contains("Not a regular file"), + "Unexpected message: " + actualExceptionMessage); + + } + + @ParameterizedTest + @MethodSource("parentDirs") + void testIrregularFile(Path parentDir) throws Exception { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(parentDir); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, Files.size(parentDir)); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + FileNotFoundException actualException = (FileNotFoundException) subscriber.invocations.take(); + String actualExceptionMessage = actualException.getMessage(); + assertTrue( + actualExceptionMessage.contains("Not a regular file"), + "Unexpected message: " + actualExceptionMessage); + + } + + /** + * A big enough file length to observe the effects of file + * modification whilst the file is getting read. + */ + private static final int BIG_FILE_LENGTH = 8 * 1024 * 1024; // 8 MiB + + @ParameterizedTest + @MethodSource("parentDirs") + void testFileModificationWhileReading(Path parentDir) throws Exception { + + // ZIP file system (sadly?) consumes the entire content at open. + // Hence, we cannot observe the effect of file modification while reading. + if (parentDir == ZIP_FS_DIR) { + return; + } + + // Create the publisher + byte[] fileBytes = ByteBufferUtils.byteArrayOfLength(BIG_FILE_LENGTH); + Path filePath = createFile(parentDir, "testFileModificationWhileReading", fileBytes); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileBytes.length); + + // Verify the state after the 1st `request()` + subscription.request(1); + assertEquals("onNext", subscriber.invocations.take()); + ByteBuffer buffer1 = (ByteBuffer) subscriber.invocations.take(); + assertTrue(buffer1.limit() > 0, "unexpected empty buffer"); + List buffers = new ArrayList<>(); + buffers.add(buffer1); + + // Truncate the file + Files.write(filePath, new byte[0]); + + // Drain emissions until completion, and verify the content + byte[] readBytes = subscriber.drainToByteArray(subscription, Long.MAX_VALUE, buffers); + assertTrue( + readBytes.length < fileBytes.length, + "was expecting less than the total amount (%s bytes), found: %s".formatted( + fileBytes.length, readBytes.length)); + ByteBuffer expectedReadBytes = ByteBuffer.wrap(fileBytes, 0, readBytes.length); + ByteBufferUtils.assertEquals(expectedReadBytes, ByteBuffer.wrap(readBytes), null); + + } + + static Stream testFileOfLengthParams() { + return PARENT_DIRS + .stream() + .flatMap(parentDir -> Stream + .of(0, 1, 2, 3, BIG_FILE_LENGTH) + .map(fileLength -> Arguments.of(parentDir, fileLength))); + } + + @ParameterizedTest + @MethodSource("testFileOfLengthParams") + void testFileOfLength(Path parentDir, int fileLength) throws Exception { + + // Create the publisher + byte[] fileBytes = ByteBufferUtils.byteArrayOfLength(fileLength); + Path filePath = createFile(parentDir, "testFileOfLength", fileBytes); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileBytes.length); + + // Drain emissions until completion, and verify the received content + byte[] readBytes = subscriber.drainToByteArray(subscription, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(fileBytes, readBytes, null); + + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws Exception { + if ("testOOM".equals(args[0])) { + testOOM(); + } else { + throw new IllegalArgumentException("Unknown arguments: " + List.of(args)); + } + } + + private static void testOOM() { + for (Path parentDir : PARENT_DIRS) { + try { + testOOM(parentDir); + } catch (Exception exception) { + throw new AssertionError("failed for parent directory: " + parentDir, exception); + } + } + } + + private static void testOOM(Path parentDir) throws Exception { + + // Create the publisher + int fileLength = ByteBufferUtils.findLengthExceedingMaxMemory(); + Path filePath = createFileOfLength(parentDir, "testOOM", fileLength); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileLength); + + // Drain emissions until completion, and verify the received content length + final int[] readLength = {0}; + subscriber.drainToAccumulator(subscription, 1, buffer -> readLength[0] += buffer.limit()); + assertEquals(fileLength, readLength[0]); + + } + + private static Path createFileOfLength(Path parentDir, String identifier, int fileLength) throws IOException { + Path filePath = createFilePath(parentDir, identifier); + try (OutputStream fileStream = Files.newOutputStream(filePath)) { + byte[] buffer = ByteBufferUtils.byteArrayOfLength(8192); + for (int writtenLength = 0; writtenLength < fileLength; writtenLength += buffer.length) { + int remainingLength = fileLength - writtenLength; + byte[] effectiveBuffer = remainingLength < buffer.length + ? ByteBufferUtils.byteArrayOfLength(remainingLength) + : buffer; + fileStream.write(effectiveBuffer); + } + } + return filePath; + } + + private static Path createFile(Path parentDir, String identifier, byte[] fileBytes) throws IOException { + Path filePath = createFilePath(parentDir, identifier); + Files.write(filePath, fileBytes); + return filePath; + } + + private static Path createFilePath(Path parentDir, String identifier) { + String fileName = identifier.replaceAll("\\W*", ""); + return parentDir.resolve(fileName); + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java new file mode 100644 index 00000000000..7688c1674ee --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java @@ -0,0 +1,226 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpRequest; +import java.util.List; +import java.util.concurrent.Flow; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofInputStream` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfInputStreamTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Xmx64m OfInputStreamTest testOOM + */ + +public class OfInputStreamTest { + + @Test + void testNullInputStreamSupplier() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofInputStream(null)); + } + + @Test + void testThrowingInputStreamSupplier() throws InterruptedException { + + // Create the publisher + RuntimeException exception = new RuntimeException(); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> { throw exception; }); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + IOException actualException = (IOException) subscriber.invocations.take(); + assertEquals("Stream supplier has failed", actualException.getMessage()); + assertSame(exception, actualException.getCause()); + + } + + @Test + void testNullInputStream() throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> null); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + IOException actualException = (IOException) subscriber.invocations.take(); + assertEquals("Stream supplier returned null", actualException.getMessage()); + + } + + @Test + void testInputStreamSupplierInvocations() throws InterruptedException { + + // Create a publisher from an `InputStream` supplier returning a different instance at each invocation + byte[] buffer1 = ByteBufferUtils.byteArrayOfLength(10); + byte[] buffer2 = ByteBufferUtils.byteArrayOfLength(10); + int[] inputStreamSupplierInvocationCount = {0}; + Supplier inputStreamSupplier = () -> + switch (++inputStreamSupplierInvocationCount[0]) { + case 1 -> new ByteArrayInputStream(buffer1); + case 2 -> new ByteArrayInputStream(buffer2); + default -> throw new AssertionError(); + }; + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(inputStreamSupplier); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription1 = subscriber.verifyAndSubscribe(publisher, -1); + Flow.Subscription subscription2 = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain each subscription and verify the received content + byte[] actualBuffer1 = subscriber.drainToByteArray(subscription1, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(buffer1, actualBuffer1, null); + byte[] actualBuffer2 = subscriber.drainToByteArray(subscription2, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(buffer2, actualBuffer2, null); + + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testInputStreamOfLength(int length) throws InterruptedException { + + // Create the publisher + byte[] content = ByteBufferUtils.byteArrayOfLength(length); + InputStream inputStream = new ByteArrayInputStream(content); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the received content + byte[] actualContent = subscriber.drainToByteArray(subscription, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(content, actualContent, null); + + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testThrowingInputStream(int exceptionIndex) throws InterruptedException { + + // Create the publisher + RuntimeException exception = new RuntimeException("failure for `read`"); + InputStream inputStream = new InputStreamThrowingOnCompletion(exceptionIndex, exception); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the failure + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + Exception actualException = (Exception) subscriber.invocations.take(); + assertSame(exception, actualException); + + } + + private static final class InputStreamThrowingOnCompletion extends InputStream { + + private final int length; + + private final RuntimeException exception; + + private int position; + + private InputStreamThrowingOnCompletion(int length, RuntimeException exception) { + this.length = length; + this.exception = exception; + } + + @Override + public synchronized int read() { + if (position < length) { + return position++ & 0xFF; + } + throw exception; + } + + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws Exception { + if ("testOOM".equals(args[0])) { + testOOM(); + } else { + throw new IllegalArgumentException("Unknown arguments: " + List.of(args)); + } + } + + private static void testOOM() throws InterruptedException { + + // Create the publisher using an `InputStream` that emits content exceeding the maximum memory + int length = ByteBufferUtils.findLengthExceedingMaxMemory(); + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.ofInputStream(() -> new InputStream() { + + private int position; + + @Override + public synchronized int read() { + return position < length ? (position++ & 0xFF) : -1; + } + + }); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the received content length + final int[] readLength = {0}; + subscriber.drainToAccumulator(subscription, 1, buffer -> readLength[0] += buffer.limit()); + assertEquals(length, readLength[0]); + + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java new file mode 100644 index 00000000000..143b5ce17da --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java @@ -0,0 +1,119 @@ +/* + * 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. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofString` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfStringTest + */ + +class OfStringTest { + + private static final Charset CHARSET = StandardCharsets.US_ASCII; + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testContentOfLength(int length) throws InterruptedException { + + // Create the publisher + char[] contentChars = new char[length]; + for (int i = 0; i < length; i++) { + contentChars[i] = (char) ('a' + i); + } + String content = new String(contentChars); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(content, CHARSET); + + // Subscribe + assertEquals(length, publisher.contentLength()); + RecordingSubscriber subscriber = new RecordingSubscriber(); + publisher.subscribe(subscriber); + assertEquals("onSubscribe", subscriber.invocations.take()); + Flow.Subscription subscription = (Flow.Subscription) subscriber.invocations.take(); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + if (length > 0) { + assertEquals("onNext", subscriber.invocations.take()); + String actualContent = CHARSET.decode((ByteBuffer) subscriber.invocations.take()).toString(); + assertEquals(content, actualContent); + } + assertEquals("onComplete", subscriber.invocations.take()); + + } + + @ParameterizedTest + @CsvSource({ + "a,UTF-8", + "b,UTF-16", + "ı,ISO-8859-9" + }) + void testCharset(String content, Charset charset) throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(content, charset); + + // Subscribe + ByteBuffer expectedBuffer = charset.encode(content); + assertEquals(expectedBuffer.limit(), publisher.contentLength()); + RecordingSubscriber subscriber = new RecordingSubscriber(); + publisher.subscribe(subscriber); + assertEquals("onSubscribe", subscriber.invocations.take()); + Flow.Subscription subscription = (Flow.Subscription) subscriber.invocations.take(); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + assertEquals("onNext", subscriber.invocations.take()); + ByteBuffer actualBuffer = (ByteBuffer) subscriber.invocations.take(); + ByteBufferUtils.assertEquals(expectedBuffer, actualBuffer, null); + assertEquals("onComplete", subscriber.invocations.take()); + + } + + @Test + void testNullContent() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofString(null, CHARSET)); + } + + @Test + void testNullCharset() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofString("foo", null)); + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java new file mode 100644 index 00000000000..0cd43c635ae --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java @@ -0,0 +1,136 @@ +/* + * 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. + */ + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Flow; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test subscriber recording received invocations. + */ +public final class RecordingSubscriber implements Flow.Subscriber { + + public final BlockingQueue invocations = new LinkedBlockingQueue<>(); + + @Override + public void onSubscribe(Flow.Subscription subscription) { + invocations.add("onSubscribe"); + invocations.add(subscription); + } + + @Override + public void onNext(ByteBuffer item) { + invocations.add("onNext"); + invocations.add(item); + } + + @Override + public synchronized void onError(Throwable throwable) { + invocations.add("onError"); + invocations.add(throwable); + } + + @Override + public synchronized void onComplete() { + invocations.add("onComplete"); + } + + /** + * Verifies the content length of the given publisher and subscribes to it. + */ + public Flow.Subscription verifyAndSubscribe(HttpRequest.BodyPublisher publisher, long contentLength) + throws InterruptedException { + assertEquals(contentLength, publisher.contentLength()); + publisher.subscribe(this); + assertEquals("onSubscribe", invocations.take()); + return (Flow.Subscription) invocations.take(); + } + + /** + * {@return the byte sequence collected by draining all emissions until completion} + * + * @param subscription a subscription to drain from + * @param itemCount the number of items to request per iteration + */ + public byte[] drainToByteArray(Flow.Subscription subscription, long itemCount) throws InterruptedException { + return drainToByteArray(subscription, itemCount, new ArrayList<>()); + } + + /** + * {@return the byte sequence collected by draining all emissions until completion} + * + * @param subscription a subscription to drain from + * @param itemCount the number of items to request per iteration + * @param buffers a list to accumulate the received content in + */ + public byte[] drainToByteArray(Flow.Subscription subscription, long itemCount, List buffers) + throws InterruptedException { + drainToAccumulator(subscription, itemCount, buffers::add); + return flattenBuffers(buffers); + } + + /** + * Drains all emissions until completion to the given {@code accumulator}. + * + * @param subscription a subscription to drain from + * @param itemCount the number of items to request per iteration + * @param accumulator an accumulator to pass the received content to + */ + public void drainToAccumulator( + Flow.Subscription subscription, long itemCount, Consumer accumulator) + throws InterruptedException { + boolean completed = false; + while (!completed) { + subscription.request(itemCount); + String op = (String) invocations.take(); + if ("onNext".equals(op)) { + ByteBuffer buffer = (ByteBuffer) invocations.take(); + accumulator.accept(buffer); + } else if ("onComplete".equals(op)) { + completed = true; + } else { + throw new AssertionError("Unexpected invocation: " + op); + } + } + } + + private static byte[] flattenBuffers(List buffers) { + int arrayLength = buffers.stream().mapToInt(ByteBuffer::limit).sum(); + byte[] array = new byte[arrayLength]; + for (int bufferIndex = 0, arrayOffset = 0; bufferIndex < buffers.size(); bufferIndex++) { + ByteBuffer buffer = buffers.get(bufferIndex); + int bufferLimit = buffer.limit(); + buffer.get(array, arrayOffset, bufferLimit); + arrayOffset += bufferLimit; + } + return array; + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java b/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java index 9401c10cdf2..4544c85c5e8 100644 --- a/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java @@ -23,11 +23,14 @@ import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -36,6 +39,8 @@ import java.util.stream.Stream; import java.net.http.HttpRequest; import static java.net.http.HttpRequest.BodyPublishers.ofString; import static java.net.http.HttpRequest.BodyPublishers.noBody; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /* * @test @@ -206,6 +211,15 @@ public class HttpRequestBuilderTest { builder = test2("method", builder, builder::method, null, ofString("foo"), NullPointerException.class); + + builder = test2("setOption", builder, builder::setOption, + (HttpOption)null, (Http3DiscoveryMode) null, + NullPointerException.class); + + builder = test2("setOption", builder, builder::setOption, + (HttpOption)null, HTTP_3_URI_ONLY, + NullPointerException.class); + // see JDK-8170093 // // builder = test2("method", builder, builder::method, "foo", @@ -268,6 +282,20 @@ public class HttpRequestBuilderTest { HttpRequest defaultHeadReq = new NotOverriddenHEADImpl().HEAD().uri(TEST_URI).build(); assertEquals("HEAD", defaultHeadReq.method(), "Method"); assertEquals(false, defaultHeadReq.bodyPublisher().isEmpty(), "Body publisher absence"); + HttpRequest defaultReqWithoutOption = new NotOverriddenHEADImpl().HEAD().uri(TEST_URI).build(); + assertEquals(Optional.empty(), defaultReqWithoutOption.getOption(H3_DISCOVERY), "default without options"); + HttpRequest defaultReqWithOption = new NotOverriddenHEADImpl().HEAD().uri(TEST_URI) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + assertEquals(Optional.empty(), defaultReqWithOption.getOption(H3_DISCOVERY), "default with options"); + HttpRequest reqWithoutOption = HttpRequest.newBuilder().HEAD().uri(TEST_URI).build(); + assertEquals(Optional.empty(), reqWithoutOption.getOption(H3_DISCOVERY), "req without options"); + HttpRequest reqWithOption = HttpRequest.newBuilder().HEAD().uri(TEST_URI) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + assertEquals(Optional.of(HTTP_3_URI_ONLY), reqWithOption.getOption(H3_DISCOVERY), "req with options"); + HttpRequest resetReqWithOption = HttpRequest.newBuilder().HEAD().uri(TEST_URI) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .setOption(H3_DISCOVERY, null).build(); + assertEquals(Optional.empty(), resetReqWithOption.getOption(H3_DISCOVERY), "req with option reset"); verifyCopy(); @@ -383,6 +411,7 @@ public class HttpRequestBuilderTest { .method("GET", noBody()) .expectContinue(true) .timeout(Duration.ofSeconds(0xBEEF)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) .version(HttpClient.Version.HTTP_2); // Create the original and the _copy_ requests @@ -399,6 +428,8 @@ public class HttpRequestBuilderTest { assertEquals(request.expectContinue(), copiedRequest.expectContinue(), "Expect continue setting"); assertEquals(request.timeout(), copiedRequest.timeout(), "Timeout"); assertEquals(request.version(), copiedRequest.version(), "Version"); + assertEquals(request.getOption(H3_DISCOVERY), copiedRequest.getOption(H3_DISCOVERY), "H3_DISCOVERY option"); + assertEquals(Optional.of(HTTP_3_URI_ONLY), copiedRequest.getOption(H3_DISCOVERY), "copied H3_DISCOVERY option"); // Verify headers assertEquals(request.headers().map(), Map.of("X-Foo", List.of("1")), "Request headers"); @@ -415,66 +446,85 @@ public class HttpRequestBuilderTest { // doesn't override the default HEAD() method private static final class NotOverriddenHEADImpl implements HttpRequest.Builder { - private final HttpRequest.Builder underlying = HttpRequest.newBuilder(); + private final HttpRequest.Builder underlying; + + NotOverriddenHEADImpl() { + this(HttpRequest.newBuilder()); + } + + NotOverriddenHEADImpl(HttpRequest.Builder underlying) { + this.underlying = underlying; + } @Override public HttpRequest.Builder uri(URI uri) { - return this.underlying.uri(uri); + underlying.uri(uri); + return this; } @Override public HttpRequest.Builder expectContinue(boolean enable) { - return this.underlying.expectContinue(enable); + underlying.expectContinue(enable); return this; } @Override public HttpRequest.Builder version(HttpClient.Version version) { - return this.underlying.version(version); + this.underlying.version(version); + return this; } @Override public HttpRequest.Builder header(String name, String value) { - return this.underlying.header(name, value); + this.underlying.header(name, value); + return this; } @Override public HttpRequest.Builder headers(String... headers) { - return this.underlying.headers(headers); + underlying.headers(headers); + return this; } @Override public HttpRequest.Builder timeout(Duration duration) { - return this.underlying.timeout(duration); + this.underlying.timeout(duration); + return this; } @Override public HttpRequest.Builder setHeader(String name, String value) { - return this.underlying.setHeader(name, value); + underlying.setHeader(name, value); + return this; } @Override public HttpRequest.Builder GET() { - return this.underlying.GET(); + this.underlying.GET(); + return this; } @Override public HttpRequest.Builder POST(HttpRequest.BodyPublisher bodyPublisher) { - return this.underlying.POST(bodyPublisher); + this.underlying.POST(bodyPublisher); + return this; } @Override public HttpRequest.Builder PUT(HttpRequest.BodyPublisher bodyPublisher) { - return this.underlying.PUT(bodyPublisher); + this.underlying.PUT(bodyPublisher); + return this; } @Override public HttpRequest.Builder DELETE() { - return this.underlying.DELETE(); + this.underlying.DELETE(); + return this; } @Override public HttpRequest.Builder method(String method, HttpRequest.BodyPublisher bodyPublisher) { - return this.underlying.method(method, bodyPublisher); + this.underlying.method(method, bodyPublisher); + return this; } @Override @@ -484,7 +534,7 @@ public class HttpRequestBuilderTest { @Override public HttpRequest.Builder copy() { - return this.underlying.copy(); + return new NotOverriddenHEADImpl(underlying.copy()); } } } diff --git a/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java b/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java index eab9ecfc2c7..d7598ede3be 100644 --- a/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.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 @@ -39,6 +39,9 @@ import java.util.function.BiConsumer; import java.util.function.BiPredicate; import static java.net.http.HttpClient.Version.HTTP_2; import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import org.testng.annotations.Test; @@ -66,6 +69,7 @@ public class HttpRequestNewBuilderTest { new NamedAssertion("timeout", (r1, r2) -> assertEquals(r1.timeout(), r2.timeout())), new NamedAssertion("version", (r1, r2) -> assertEquals(r1.version(), r2.version())), new NamedAssertion("headers", (r1, r2) -> assertEquals(r1.headers(), r2.headers())), + new NamedAssertion("options", (r1, r2) -> assertEquals(r1.getOption(H3_DISCOVERY), r2.getOption(H3_DISCOVERY))), new NamedAssertion("expectContinue", (r1, r2) -> assertEquals(r1.expectContinue(), r2.expectContinue())), new NamedAssertion("method", (r1, r2) -> { assertEquals(r1.method(), r2.method()); @@ -141,6 +145,9 @@ public class HttpRequestNewBuilderTest { { HttpRequest.newBuilder(URI.create("https://all-fields-1/")).GET().expectContinue(true).version(HTTP_2) .timeout(Duration.ofSeconds(1)).header("testName1", "testValue1").build() }, + { HttpRequest.newBuilder(URI.create("https://all-fields-2/")).GET().expectContinue(true).version(HTTP_2) + .timeout(Duration.ofSeconds(1)).header("testName1", "testValue1") + .setOption(H3_DISCOVERY, ANY).build() }, }; } @@ -313,6 +320,15 @@ public class HttpRequestNewBuilderTest { assertAllOtherElementsEqual(r, request, "headers"); } + @Test(dataProvider = "testRequests") + public void testSetOption(HttpRequest request) { + BiPredicate filter = (n, v) -> true; + + var r = HttpRequest.newBuilder(request, filter).setOption(H3_DISCOVERY, ALT_SVC).build(); + assertEquals(r.getOption(H3_DISCOVERY).get(), ALT_SVC); + assertAllOtherElementsEqual(r, request, "options"); + } + @Test(dataProvider = "testRequests") public void testRemoveHeader(HttpRequest request) { if(!request.headers().map().isEmpty()) { @@ -325,6 +341,18 @@ public class HttpRequestNewBuilderTest { assertEquals(r.headers().map(), HttpHeaders.of(request.headers().map(), filter).map()); } + @Test(dataProvider = "testRequests") + public void testRemoveOption(HttpRequest request) { + if(!request.getOption(H3_DISCOVERY).isEmpty()) { + assertEquals(request.getOption(H3_DISCOVERY).get(), ANY); + } + + var r = HttpRequest.newBuilder(request, (a, b) -> true) + .setOption(H3_DISCOVERY, null).build(); + assertTrue(r.getOption(H3_DISCOVERY).isEmpty()); + assertAllOtherElementsEqual(r, request, "options"); + } + @Test(dataProvider = "testRequests") public void testRemoveSingleHeaderValue(HttpRequest request) { if(!request.headers().map().isEmpty()) { diff --git a/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java b/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java index b6c13c51ee1..d1dc8b0a5f5 100644 --- a/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java +++ b/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java @@ -29,10 +29,13 @@ * @build jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.test.lib.net.SimpleSSLContext * - * @comment Use a higher idle timeout to increase the chances of the same connection being used for sequential HTTP requests - * @run junit/othervm -Djdk.httpclient.keepalive.timeout=120 HttpResponseConnectionLabelTest + * @comment Use a higher idle timeout to increase the chances of + * the same connection being used for sequential HTTP requests + * @run junit/othervm -Djdk.httpclient.keepalive.timeout=120 + * HttpResponseConnectionLabelTest */ +import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.internal.net.http.common.Logger; @@ -55,6 +58,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.Charset; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -64,6 +68,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -83,8 +89,12 @@ class HttpResponseConnectionLabelTest { private static final SSLContext SSL_CONTEXT = createSslContext(); - // Start with a fresh client having no connections in the pool - private final HttpClient client = HttpClient.newBuilder().sslContext(SSL_CONTEXT).proxy(NO_PROXY).build(); + // For each test instance, start with a fresh client having no connections in the pool + private final HttpClient client = HttpServerAdapters + .createClientBuilderForH3() + .sslContext(SSL_CONTEXT) + .proxy(NO_PROXY) + .build(); // Primary server-client pairs @@ -96,6 +106,8 @@ class HttpResponseConnectionLabelTest { private static final ServerRequestPair PRI_HTTPS2 = ServerRequestPair.of(Version.HTTP_2, true); + private static final ServerRequestPair PRI_HTTP3 = ServerRequestPair.of(Version.HTTP_3, true); + // Secondary server-client pairs private static final ServerRequestPair SEC_HTTP1 = ServerRequestPair.of(Version.HTTP_1_1, false); @@ -106,6 +118,8 @@ class HttpResponseConnectionLabelTest { private static final ServerRequestPair SEC_HTTPS2 = ServerRequestPair.of(Version.HTTP_2, true); + private static final ServerRequestPair SEC_HTTP3 = ServerRequestPair.of(Version.HTTP_3, true); + private static SSLContext createSslContext() { try { return new SimpleSSLContext().get(); @@ -140,8 +154,8 @@ class HttpResponseConnectionLabelTest { AtomicReference serverResponseLatchRef = new AtomicReference<>(); server.addHandler(createServerHandler(serverId, serverResponseLatchRef), handlerPath); - // Create the client and the request - HttpRequest request = HttpRequest.newBuilder(requestUri).version(version).build(); + // Create the request + HttpRequest request = createRequest(version, requestUri); // Create the pair ServerRequestPair pair = new ServerRequestPair( @@ -168,13 +182,15 @@ class HttpResponseConnectionLabelTest { // - Only the HTTP/1.1 test server gets wedged when running // tests involving parallel request handling. // - // - The HTTP/2 test server creates its own sufficiently sized - // executor, and the thread names used there makes it easy to - // find which server they belong to. + // - The HTTP/2 and HTTP/3 test servers create their own + // sufficiently sized executor, and the thread names used + // there makes it easy to find which server they belong to. executorRef[0] = Version.HTTP_1_1.equals(version) ? createExecutor(version, secure, serverId) : null; - return HttpTestServer.create(version, sslContext, executorRef[0]); + return Version.HTTP_3.equals(version) + ? HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, executorRef[0]) + : HttpTestServer.create(version, sslContext, executorRef[0]); } catch (IOException exception) { throw new UncheckedIOException(exception); } @@ -196,7 +212,7 @@ class HttpResponseConnectionLabelTest { return (exchange) -> { String responseBody = "" + SERVER_RESPONSE_COUNTER.getAndIncrement(); String connectionKey = exchange.getConnectionKey(); - LOGGER.log("Server[%d] has received request (connectionKey=%s)", serverId, connectionKey); + LOGGER.log("Server[%s] has received request (connectionKey=%s)", serverId, connectionKey); try (exchange) { // Participate in the latch count down @@ -232,6 +248,14 @@ class HttpResponseConnectionLabelTest { }; } + private static HttpRequest createRequest(Version version, URI requestUri) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(requestUri).version(version); + if (Version.HTTP_3.equals(version)) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return requestBuilder.build(); + } + @Override public String toString() { String version = server.getVersion().toString(); @@ -244,7 +268,9 @@ class HttpResponseConnectionLabelTest { static void closeServers() { Exception[] exceptionRef = {null}; Stream - .of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2) + .of( + PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, PRI_HTTP3, + SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2, SEC_HTTP3) .flatMap(pair -> Stream.of( pair.server::stop, () -> { if (pair.executor != null) { pair.executor.shutdownNow(); } })) @@ -274,7 +300,8 @@ class HttpResponseConnectionLabelTest { PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, - PRI_HTTPS2 + PRI_HTTPS2, + PRI_HTTP3 }; } @@ -283,8 +310,9 @@ class HttpResponseConnectionLabelTest { void testParallelRequestsToSameServer(ServerRequestPair pair) throws Exception { // There is no implementation-agnostic reliable way to force admission - // of multiple connections targeting the same server to an HTTP/2 pool. - if (Version.HTTP_2.equals(pair.server.getVersion())) { + // of multiple connections targeting the same server to an HTTP/2 or + // HTTP/3 client connection pool. + if (Set.of(Version.HTTP_2, Version.HTTP_3).contains(pair.server.getVersion())) { return; } @@ -359,9 +387,9 @@ class HttpResponseConnectionLabelTest { static Stream testParallelRequestsToDifferentServers() { return Stream - .of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2) + .of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, PRI_HTTP3) .flatMap(source -> Stream - .of(SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2) + .of(SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2, SEC_HTTP3) .map(target -> Arguments.of(source, target))); } @@ -440,7 +468,7 @@ class HttpResponseConnectionLabelTest { } static Stream testSerialRequestsToSameServer() { - return Stream.of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2); + return Stream.of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, PRI_HTTP3); } @ParameterizedTest diff --git a/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java b/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java index b87e7ea8e49..e0bda0b0071 100644 --- a/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java +++ b/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java @@ -34,6 +34,7 @@ * @run junit HttpResponseLimitingTest */ +import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; @@ -66,6 +67,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static java.net.http.HttpClient.Builder.NO_PROXY; +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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.copyOfRange; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -89,15 +95,17 @@ class HttpResponseLimitingTest { */ private static final String RESPONSE_HEADER_VALUE = "!".repeat(RESPONSE_BODY.length + 1); - private static final ServerClientPair HTTP1 = ServerClientPair.of(HttpClient.Version.HTTP_1_1, false); + private static final ServerClientPair H1 = ServerClientPair.of(HTTP_1_1, false); - private static final ServerClientPair HTTPS1 = ServerClientPair.of(HttpClient.Version.HTTP_1_1, true); + private static final ServerClientPair H1S = ServerClientPair.of(HTTP_1_1, true); - private static final ServerClientPair HTTP2 = ServerClientPair.of(HttpClient.Version.HTTP_2, false); + private static final ServerClientPair H2 = ServerClientPair.of(HTTP_2, false); - private static final ServerClientPair HTTPS2 = ServerClientPair.of(HttpClient.Version.HTTP_2, true); + private static final ServerClientPair H2S = ServerClientPair.of(HTTP_2, true); - private record ServerClientPair(HttpTestServer server, HttpClient client, HttpRequest request) { + private static final ServerClientPair H3 = ServerClientPair.of(HTTP_3, true); + + private record ServerClientPair(HttpTestServer server, HttpClient client, HttpRequest request, boolean secure) { private static final SSLContext SSL_CONTEXT = createSslContext(); @@ -128,7 +136,7 @@ class HttpResponseLimitingTest { // Create the server and the request URI SSLContext sslContext = secure ? SSL_CONTEXT : null; HttpTestServer server = createServer(version, sslContext); - String handlerPath = "/"; + String handlerPath = "/" + /* salting the path: */ HttpResponseLimitingTest.class.getSimpleName(); String requestUriScheme = secure ? "https" : "http"; URI requestUri = URI.create(requestUriScheme + "://" + server.serverAuthority() + handlerPath); @@ -146,29 +154,40 @@ class HttpResponseLimitingTest { // Create the client and the request HttpClient client = createClient(version, sslContext); - HttpRequest request = HttpRequest.newBuilder(requestUri).version(version).build(); + HttpRequest request = createRequest(version, requestUri); // Create the pair - return new ServerClientPair(server, client, request); + return new ServerClientPair(server, client, request, secure); } private static HttpTestServer createServer(HttpClient.Version version, SSLContext sslContext) { try { - return HttpTestServer.create(version, sslContext); + return HTTP_3.equals(version) + ? HttpTestServer.create(HTTP_3_URI_ONLY, sslContext) + : HttpTestServer.create(version, sslContext); } catch (IOException exception) { throw new UncheckedIOException(exception); } } private static HttpClient createClient(HttpClient.Version version, SSLContext sslContext) { - HttpClient.Builder builder = HttpClient.newBuilder().version(version).proxy(NO_PROXY); + HttpClient.Builder builder = HttpServerAdapters.createClientBuilderFor(version) + .version(version).proxy(NO_PROXY); if (sslContext != null) { builder.sslContext(sslContext); } return builder.build(); } + private static HttpRequest createRequest(HttpClient.Version version, URI requestUri) { + HttpRequest.Builder builder = HttpRequest.newBuilder(requestUri).version(version); + if (HTTP_3.equals(version)) { + builder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder.build(); + } + private HttpResponse request(BodyHandler downstreamHandler, long capacity) throws Exception { var handler = BodyHandlers.limiting(downstreamHandler, capacity); return client.send(request, handler); @@ -176,8 +195,9 @@ class HttpResponseLimitingTest { @Override public String toString() { - String version = client.version().toString(); - return client.sslContext() != null ? version.replaceFirst("_", "S_") : version; + HttpClient.Version version = client.version(); + String versionString = version.toString(); + return secure && !HTTP_3.equals(version) ? versionString.replaceFirst("_", "S_") : versionString; } } @@ -186,7 +206,7 @@ class HttpResponseLimitingTest { static void closeServerClientPairs() { Exception[] exceptionRef = {null}; Stream - .of(HTTP1, HTTPS1, HTTP2, HTTPS2) + .of(H1, H1S, H2, H2S, H3) .flatMap(pair -> Stream.of( pair.client::close, pair.server::stop)) @@ -306,7 +326,7 @@ class HttpResponseLimitingTest { private static Arguments[] capacityArgs(long... capacities) { return Stream - .of(HTTP1, HTTPS1, HTTP2, HTTPS2) + .of(H1, H1S, H2, H2S, H3) .flatMap(pair -> Arrays .stream(capacities) .mapToObj(capacity -> Arguments.of(pair, capacity))) diff --git a/test/jdk/java/net/httpclient/HttpSlowServerTest.java b/test/jdk/java/net/httpclient/HttpSlowServerTest.java index a71eaf746ad..ca841cd8ae9 100644 --- a/test/jdk/java/net/httpclient/HttpSlowServerTest.java +++ b/test/jdk/java/net/httpclient/HttpSlowServerTest.java @@ -52,6 +52,9 @@ import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -62,8 +65,9 @@ import static java.net.http.HttpClient.Version.HTTP_2; * DigestEchoServer HttpSlowServerTest * jdk.httpclient.test.lib.common.TestServerConfigurator * @run main/othervm/timeout=480 -Dtest.requiresHost=true - * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.httpclient.HttpClient.log=errors,headers,quic:hs * -Djdk.internal.httpclient.debug=false + * -Djdk.httpclient.quic.maxInitialTimeout=60 * HttpSlowServerTest * */ @@ -97,12 +101,14 @@ public class HttpSlowServerTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -115,8 +121,7 @@ public class HttpSlowServerTest implements HttpServerAdapters { TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // Used by the client public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .proxy(ps); @@ -155,6 +160,12 @@ public class HttpSlowServerTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/HttpSlowServerTest/https2/"); + // HTTP/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestSlowHandler(), "/HttpSlowServerTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/HttpSlowServerTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -185,9 +196,10 @@ public class HttpSlowServerTest implements HttpServerAdapters { } public void run(String... args) throws Exception { - List serverURIs = List.of(http1URI, http2URI, https1URI, https2URI); + List serverURIs = List.of(http3URI, http1URI, http2URI, https1URI, https2URI); for (int i=0; i<20; i++) { for (URI base : serverURIs) { + if (base.getRawPath().contains("/http3/")) continue; // proxy not supported if (base.getScheme().equalsIgnoreCase("https")) { URI proxy = i % 1 == 0 ? base.resolve(URI.create("proxy/foo?n="+requestCounter.incrementAndGet())) : base.resolve(URI.create("direct/foo?n="+requestCounter.incrementAndGet())); @@ -205,7 +217,11 @@ public class HttpSlowServerTest implements HttpServerAdapters { public void test(URI uri) throws Exception { System.out.println("Testing with " + uri); pending.add(uri); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + HttpRequest request = builder.build(); CompletableFuture> resp = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete((r, t) -> this.requestCompleted(request, r, t)); @@ -228,6 +244,7 @@ public class HttpSlowServerTest implements HttpServerAdapters { https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/ISO_8859_1_Test.java b/test/jdk/java/net/httpclient/ISO_8859_1_Test.java index e81b0130418..a5465ad5103 100644 --- a/test/jdk/java/net/httpclient/ISO_8859_1_Test.java +++ b/test/jdk/java/net/httpclient/ISO_8859_1_Test.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, 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 @@ -41,50 +41,31 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; -import java.net.URL; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.Flow.Subscription; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.LongStream; -import java.util.stream.Stream; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; -import org.testng.Assert; import org.testng.ITestContext; import org.testng.ITestResult; import org.testng.SkipException; @@ -98,24 +79,28 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; public class ISO_8859_1_Test implements HttpServerAdapters { SSLContext sslContext; DummyServer http1DummyServer; - HttpServerAdapters.HttpTestServer http1TestServer; // HTTP/1.1 ( http ) - HttpServerAdapters.HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http1TestServer; // HTTP/1.1 ( http ) + HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http1Dummy; String http1URI; String https1URI; String http2URI; String https2URI; + String http3URI; static final int RESPONSE_CODE = 200; static final int ITERATION_COUNT = 4; @@ -216,6 +201,7 @@ public class ISO_8859_1_Test implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI, http1Dummy, http1URI, https1URI, @@ -243,9 +229,12 @@ public class ISO_8859_1_Test implements HttpServerAdapters { return result; } - private HttpClient makeNewClient() { + private HttpClient makeNewClient(Version version) { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + var builder = version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -253,14 +242,24 @@ public class ISO_8859_1_Test implements HttpServerAdapters { return TRACKER.track(client); } - HttpClient newHttpClient(boolean share) { - if (!share) return makeNewClient(); + Version version(String uri) { + if (uri == null) return null; + if (uri.contains("/http1/")) return HTTP_1_1; + if (uri.contains("/https1/")) return HTTP_1_1; + if (uri.contains("/http2/")) return HTTP_2; + if (uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) return HTTP_3; + return null; + } + + HttpClient newHttpClient(String uri, boolean share) { + if (!share) return makeNewClient(version(uri)); HttpClient shared = sharedClient; if (shared != null) return shared; synchronized (this) { shared = sharedClient; if (shared == null) { - shared = sharedClient = makeNewClient(); + shared = sharedClient = makeNewClient(HTTP_3); } return shared; } @@ -277,16 +276,25 @@ public class ISO_8859_1_Test implements HttpServerAdapters { return (Exception)c; } + private static HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void test(String uri, boolean sameClient) throws Exception { checkSkip(); System.out.println("Request to " + uri); - HttpClient client = newHttpClient(sameClient); + HttpClient client = newHttpClient(uri, sameClient); List>> cfs = new ArrayList<>(); for (int i = 0; i < ITERATION_COUNT; i++) { - HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "/" + i)) + HttpRequest request = newRequestBuilder(URI.create(uri + "/" + i)) .build(); cfs.add(client.sendAsync(request, BodyHandlers.ofString())); } @@ -431,12 +439,17 @@ public class ISO_8859_1_Test implements HttpServerAdapters { https2TestServer.addHandler(handler, "/https2/server/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/server/x"; - serverCount.addAndGet(5); + http3TestServer = HttpServerAdapters.HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(handler, "/http3/server/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/server/x"; + + serverCount.addAndGet(6); http1TestServer.start(); https1TestServer.start(); http2TestServer.start(); https2TestServer.start(); http1DummyServer.start(); + http3TestServer.start(); } @AfterTest @@ -452,6 +465,7 @@ public class ISO_8859_1_Test implements HttpServerAdapters { http2TestServer.stop(); https2TestServer.stop(); http1DummyServer.close(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java b/test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java new file mode 100644 index 00000000000..07bb0be455c --- /dev/null +++ b/test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java @@ -0,0 +1,360 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.BodyOutputStream; +import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; +import jdk.httpclient.test.lib.http2.Http2TestServerConnection; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.net.SocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2Handler; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.net.http.HttpClient.Version.HTTP_2; +import static org.testng.Assert.assertEquals; + +/* + * @test + * @bug 8288717 + * @summary Tests that when the idle connection timeout is configured for a HTTP connection, + * then the connection is closed if it has been idle for that long + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.httpclient.test.lib.http3.Http3TestServer + * + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout=1 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout=20 + * IdleConnectionTimeoutTest + * + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=1 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=20 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=abc + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=-1 + * IdleConnectionTimeoutTest + * + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=1 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=20 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=abc + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=-1 + * IdleConnectionTimeoutTest + */ +public class IdleConnectionTimeoutTest { + + URI timeoutUriH2, noTimeoutUriH2, timeoutUriH3, noTimeoutUriH3, getH3; + SSLContext sslContext; + static volatile QuicServerConnection latestServerConn; + final String KEEP_ALIVE_PROPERTY = "jdk.httpclient.keepalive.timeout"; + final String IDLE_CONN_PROPERTY_H2 = "jdk.httpclient.keepalive.timeout.h2"; + final String IDLE_CONN_PROPERTY_H3 = "jdk.httpclient.keepalive.timeout.h3"; + final String TIMEOUT_PATH = "/serverTimeoutHandler"; + final String NO_TIMEOUT_PATH = "/noServerTimeoutHandler"; + static Http2TestServer http2TestServer; + static Http3TestServer http3TestServer; + static final PrintStream testLog = System.err; + + @BeforeTest + public void setup() throws Exception { + http2TestServer = new Http2TestServer(false, 0); + http2TestServer.addHandler(new ServerTimeoutHandlerH2(), TIMEOUT_PATH); + http2TestServer.addHandler(new ServerNoTimeoutHandlerH2(), NO_TIMEOUT_PATH); + http2TestServer.setExchangeSupplier(TestExchange::new); + + sslContext = new SimpleSSLContext().get(); + http3TestServer = new Http3TestServer(sslContext) { + @Override + public boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConn) { + final boolean accepted = super.acceptIncoming(source, quicConn); + if (accepted) { + // Quic Connection maps to Http3Connection, can use this to verify h3 timeouts + latestServerConn = quicConn; + } + return accepted; + } + }; + http3TestServer.addHandler(TIMEOUT_PATH, new ServerTimeoutHandlerH3()); + http3TestServer.addHandler(NO_TIMEOUT_PATH, new ServerNoTimeoutHandlerH3()); + + http2TestServer.start(); + http3TestServer.start(); + int port = http2TestServer.getAddress().getPort(); + timeoutUriH2 = URIBuilder.newBuilder() + .scheme("http") + .loopback() + .port(port) + .path(TIMEOUT_PATH) + .build(); + noTimeoutUriH2 = URIBuilder.newBuilder() + .scheme("http") + .loopback() + .port(port) + .path(NO_TIMEOUT_PATH) + .build(); + + port = http3TestServer.getAddress().getPort(); + getH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(port) + .path("/get") + .build(); + timeoutUriH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(port) + .path(TIMEOUT_PATH) + .build(); + noTimeoutUriH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(port) + .path(NO_TIMEOUT_PATH) + .build(); + } + + @Test + public void testRoot() { + String keepAliveVal = System.getProperty(KEEP_ALIVE_PROPERTY); + String idleConnectionH2Val = System.getProperty(IDLE_CONN_PROPERTY_H2); + String idleConnectionH3Val = System.getProperty(IDLE_CONN_PROPERTY_H3); + + if (keepAliveVal != null) { + try (HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build()) { + // test H2 inherits value + testLog.println("Testing HTTP/2 connections set idleConnectionTimeout value to keep alive value"); + test(hc, keepAliveVal, HTTP_2, timeoutUriH2, noTimeoutUriH2); + } + try (HttpClient hc = HttpServerAdapters.createClientBuilderForH3().sslContext(sslContext).build()) { + // test H3 inherits value + testLog.println("Testing HTTP/3 connections set idleConnectionTimeout value to keep alive value"); + test(hc, keepAliveVal, HTTP_3, timeoutUriH3, noTimeoutUriH3); + } + } else if (idleConnectionH2Val != null) { + try (HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build()) { + testLog.println("Testing HTTP/2 idleConnectionTimeout"); + test(hc, idleConnectionH2Val, HTTP_2, timeoutUriH2, noTimeoutUriH2); + } + } else if (idleConnectionH3Val != null) { + try (HttpClient hc = HttpServerAdapters.createClientBuilderForH3().sslContext(sslContext).build()) { + testLog.println("Testing HTTP/3 idleConnectionTimeout"); + test(hc, idleConnectionH3Val, HTTP_3, timeoutUriH3, noTimeoutUriH3); + } + } + + } + + private void test(HttpClient hc, String propVal, Version version, URI timeoutUri, URI noTimeoutUri) { + if (propVal.equals("1")) { + testTimeout(hc, timeoutUri, version); + } else if (propVal.equals("20")) { + testNoTimeout(hc, noTimeoutUri, version); + } else if (propVal.equals("abc") || propVal.equals("-1")) { + testNoTimeout(hc, noTimeoutUri, version); + } else { + throw new RuntimeException("Unexpected timeout value"); + } + } + + private void testTimeout(HttpClient hc, URI uri, Version version) { + // Timeout should occur + var config = version == HTTP_3 ? HTTP_3_URI_ONLY : null; + HttpRequest hreq = HttpRequest.newBuilder(uri).version(version).GET() + .setOption(H3_DISCOVERY, config).build(); + HttpResponse hresp = runRequest(hc, hreq, 2750); + assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); + } + + private void testNoTimeout(HttpClient hc, URI uri, Version version) { + // Timeout should not occur + var config = version == HTTP_3 ? HTTP_3_URI_ONLY : null; + HttpRequest hreq = HttpRequest.newBuilder(uri).version(version).GET() + .setOption(H3_DISCOVERY, config).build(); + HttpResponse hresp = runRequest(hc, hreq, 0); + assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); + } + + private HttpResponse runRequest(HttpClient hc, HttpRequest req, int sleepTime) { + CompletableFuture> request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + HttpResponse hresp = request.join(); + assertEquals(hresp.statusCode(), 200); + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + return request.join(); + } + + static class ServerTimeoutHandlerH2 implements Http2Handler { + + volatile Object firstConnection = null; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (exchange instanceof TestExchange exch) { + if (firstConnection == null) { + firstConnection = exch.getServerConnection(); + exch.sendResponseHeaders(200, 0); + } else { + var secondConnection = exch.getServerConnection(); + + if (firstConnection != secondConnection) { + testLog.println("ServerTimeoutHandlerH2: New Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH2: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(400, 0); + } + } + } + } + } + + static class ServerNoTimeoutHandlerH2 implements Http2Handler { + + volatile Object firstConnection; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (exchange instanceof TestExchange exch) { + if (firstConnection == null) { + firstConnection = exch.getServerConnection(); + exch.sendResponseHeaders(200, 0); + } else { + var secondConnection = exch.getServerConnection(); + + if (firstConnection == secondConnection) { + testLog.println("ServerTimeoutHandlerH2: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH2: Different Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(400, 0); + } + } + } + } + } + + static class ServerTimeoutHandlerH3 implements Http2Handler { + + volatile Object firstConnection; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (firstConnection == null) { + firstConnection = latestServerConn; + exchange.sendResponseHeaders(200, 0); + } else { + var secondConnection = latestServerConn; + if (firstConnection != secondConnection) { + testLog.println("ServerTimeoutHandlerH3: New Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH3: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(400, 0); + } + } + exchange.close(); + } + } + + static class ServerNoTimeoutHandlerH3 implements Http2Handler { + + volatile Object firstConnection = null; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (firstConnection == null) { + firstConnection = latestServerConn; + exchange.sendResponseHeaders(200, 0); + } else { + var secondConnection = latestServerConn; + + if (firstConnection == secondConnection) { + testLog.println("ServerTimeoutHandlerH3: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH3: New Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(400, 0); + } + } + exchange.close(); + } + } + + static class TestExchange extends Http2TestExchangeImpl { + + public TestExchange(int streamid, String method, + HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, + URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, + Http2TestServerConnection conn, boolean pushAllowed) { + super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, + conn, pushAllowed); + } + + public Http2TestServerConnection getServerConnection() { + return this.conn; + } + } +} diff --git a/test/jdk/java/net/httpclient/ImmutableFlowItems.java b/test/jdk/java/net/httpclient/ImmutableFlowItems.java index b440ad646e8..186d3bbd9f8 100644 --- a/test/jdk/java/net/httpclient/ImmutableFlowItems.java +++ b/test/jdk/java/net/httpclient/ImmutableFlowItems.java @@ -46,7 +46,6 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; diff --git a/test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java b/test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java new file mode 100644 index 00000000000..179900479bc --- /dev/null +++ b/test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java @@ -0,0 +1,381 @@ +/* + * 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. + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.ImmutableExtendedSSLSession; +import jdk.internal.net.http.common.ImmutableSSLSession; +import jdk.internal.net.http.common.ImmutableSSLSessionAccess; +import jdk.test.lib.net.SimpleSSLContext; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +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 java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @summary Verify that the request/response headers of HTTP/2 and HTTP/3 + * are sent and received in lower case + * @library /test/lib /test/jdk/java/net/httpclient/lib /test/jdk/java/net/httpclient/access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * java.net.http/jdk.internal.net.http.common.ImmutableSSLSessionAccess + * @run junit/othervm -Djdk.httpclient.HttpClient.log=request,response,headers,errors + * ImmutableSSLSessionTest + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class ImmutableSSLSessionTest implements HttpServerAdapters { + + private HttpTestServer h1server; + private HttpTestServer h2server; + private HttpTestServer h3server; + private String h1ReqURIBase; + private String h2ReqURIBase; + private String h3ReqURIBase; + private static SSLContext sslContext; + private final AtomicInteger counter = new AtomicInteger(); + + @BeforeAll + public void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + h1server = HttpTestServer.create(HTTP_1_1, sslContext); + h1server.start(); + h1ReqURIBase = "https://" + h1server.serverAuthority() + "/h1ImmutableSSLSessionTest/"; + h1server.addHandler(new HttpHeadOrGetHandler(), "/h1ImmutableSSLSessionTest/"); + System.out.println("HTTP/1.1 server listening on " + h1server.getAddress()); + + h2server = HttpTestServer.create(HTTP_2, sslContext); + h2server.start(); + h2ReqURIBase = "https://" + h2server.serverAuthority() + "/h2ImmutableSSLSessionTest/"; + h2server.addHandler(new HttpHeadOrGetHandler(), "/h2ImmutableSSLSessionTest/"); + System.out.println("HTTP/2 server listening on " + h2server.getAddress()); + + + h3server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3server.start(); + h3ReqURIBase = "https://" + h3server.serverAuthority() + "/h3ImmutableSSLSessionTest/"; + h3server.addHandler(new HttpHeadOrGetHandler(), "/h3ImmutableSSLSessionTest/"); + System.out.println("HTTP/3 server listening on " + h3server.getAddress()); + + } + + @AfterAll + public void afterAll() throws Exception { + if (h2server != null) { + h2server.stop(); + } + if (h3server != null) { + h3server.stop(); + } + } + + private Stream params() throws Exception { + return Stream.of( + Arguments.of(HTTP_1_1, new URI(h1ReqURIBase)), + Arguments.of(HTTP_2, new URI(h2ReqURIBase)), + Arguments.of(HTTP_3, new URI(h3ReqURIBase))); + } + + private Stream sessions() throws Exception { + return Stream.of( + Arguments.of(ImmutableSSLSessionAccess.immutableSSLSession(new DummySession())), + Arguments.of(ImmutableSSLSessionAccess.immutableExtendedSSLSession(new DummySession()))); + } + + /** + * Issues an HTTPS request and verifies that the SSLSession + * is immutable. + */ + @ParameterizedTest + @MethodSource("params") + public void testImmutableSSLSession(final Version version, final URI requestURI) throws Exception { + Http3DiscoveryMode config = switch (version) { + case HTTP_3 -> HTTP_3_URI_ONLY; + default -> null; + }; + + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() + .setOption(H3_DISCOVERY, config) + .version(version); + final HttpClient.Builder clientBuilder = (version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder()) + .version(version) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY); + + try (HttpClient client = clientBuilder.build()) { + final URI uriSync = URI.create(requestURI.toString() + "?sync=true,req=" + counter.incrementAndGet()); + final HttpRequest reqSync = reqBuilder.uri(uriSync).build(); + System.out.println("Issuing " + version + " request to " + uriSync); + final HttpResponse resp = client.send(reqSync, BodyHandlers.discarding()); + final Optional syncSession = resp.sslSession(); + assertEquals(resp.version(), version, "Unexpected HTTP version in response"); + assertEquals(resp.statusCode(), 200, "Unexpected response code"); + checkImmutableSession(resp.sslSession()); + } + + // now try with async + try (HttpClient client = clientBuilder.build()) { + final URI uriAsync = URI.create(requestURI.toString() + "?sync=false,req=" + counter.incrementAndGet()); + final HttpRequest reqAsync = reqBuilder.copy().uri(uriAsync).build(); + System.out.println("Issuing (async) request to " + uriAsync); + final CompletableFuture> futureResp = client.sendAsync(reqAsync, + BodyHandlers.discarding()); + final HttpResponse asyncResp = futureResp.get(); + assertEquals(asyncResp.version(), version, "Unexpected HTTP version in response"); + assertEquals(asyncResp.statusCode(), 200, "Unexpected response code"); + checkImmutableSession(asyncResp.sslSession()); + } + } + + @ParameterizedTest + @MethodSource("sessions") + public void testImmutableSSLSessionClass(SSLSession session) throws Exception { + System.out.println("Checking session class: " + session.getClass()); + checkDummySession(session); + } + + + private void checkImmutableSession(Optional session) { + assertNotNull(session); + assertTrue(session.isPresent()); + SSLSession sess = session.get(); + assertNotNull(sess); + checkImmutableSession(sess); + } + + private void checkImmutableSession(SSLSession session) { + if (session instanceof ExtendedSSLSession) { + assertEquals(ImmutableExtendedSSLSession.class, session.getClass()); + } else { + assertEquals(ImmutableSSLSession.class, session.getClass()); + } + assertThrows(UnsupportedOperationException.class, session::invalidate); + assertThrows(UnsupportedOperationException.class, + () -> session.putValue("foo", "bar")); + for (String name : session.getValueNames()) { + assertThrows(UnsupportedOperationException.class, + () -> session.removeValue(name)); + } + } + + private void checkDummySession(SSLSession session) throws Exception { + checkImmutableSession(session); + assertEquals("abcd", new String(session.getId(), US_ASCII)); + assertEquals(sslContext.getClientSessionContext(), session.getSessionContext()); + assertEquals(42, session.getCreationTime()); + assertEquals(4242, session.getLastAccessedTime()); + assertFalse(session.isValid()); + assertEquals("bar", session.getValue("foo")); + assertEquals(List.of("foo"), Arrays.asList(session.getValueNames())); + assertEquals(0, session.getPeerCertificates().length); + assertEquals(0, session.getLocalCertificates().length); + assertNull(session.getPeerPrincipal()); + assertNull(session.getLocalPrincipal()); + assertEquals("MyCipherSuite", session.getCipherSuite()); + assertEquals("TLSv1.3", session.getProtocol()); + assertEquals("dummy", session.getPeerHost()); + assertEquals(42, session.getPeerPort()); + assertEquals(42, session.getPacketBufferSize()); + assertEquals(42, session.getApplicationBufferSize()); + if (session instanceof ExtendedSSLSession ext) { + assertEquals(List.of("bar", "foo"), + Arrays.asList(ext.getPeerSupportedSignatureAlgorithms())); + assertEquals(List.of("foo", "bar"), + Arrays.asList(ext.getLocalSupportedSignatureAlgorithms())); + assertEquals(List.of(new SNIHostName("localhost")), ((ExtendedSSLSession) session).getRequestedServerNames()); + List status = ext.getStatusResponses(); + assertEquals(1, status.size()); + assertEquals("42", new String(status.get(0), US_ASCII)); + assertThrows(UnsupportedOperationException.class, + () -> ext.exportKeyingMaterialData("foo", new byte[] {1,2,3,4}, 4)); + assertThrows(UnsupportedOperationException.class, + () -> ext.exportKeyingMaterialKey("foo", "foo", new byte[] {1,2,3,4}, 4)); + } + } + + static class DummySession extends ExtendedSSLSession { + + @Override + public byte[] getId() { + return new byte[] {'a', 'b', 'c', 'd'}; + } + + @Override + public SSLSessionContext getSessionContext() { + return sslContext.getClientSessionContext(); + } + + @Override + public long getCreationTime() { + return 42; + } + + @Override + public long getLastAccessedTime() { + return 4242; + } + + @Override + public void invalidate() {} + + @Override + public boolean isValid() { + return false; + } + + @Override + public void putValue(String name, Object value) { + + } + + @Override + public Object getValue(String name) { + if (name.equals("foo")) return "bar"; + return null; + } + + @Override + public void removeValue(String name) { + + } + + @Override + public String[] getValueNames() { + return new String[] {"foo"}; + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + return new Certificate[0]; + } + + @Override + public Certificate[] getLocalCertificates() { + return new Certificate[0]; + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return null; + } + + @Override + public Principal getLocalPrincipal() { + return null; + } + + @Override + public String getCipherSuite() { + return "MyCipherSuite"; + } + + @Override + public String getProtocol() { + return "TLSv1.3"; + } + + @Override + public String getPeerHost() { + return "dummy"; + } + + @Override + public int getPeerPort() { + return 42; + } + + @Override + public int getPacketBufferSize() { + return 42; + } + + @Override + public int getApplicationBufferSize() { + return 42; + } + + @Override + public String[] getLocalSupportedSignatureAlgorithms() { + return new String[] {"foo", "bar"}; + } + + @Override + public String[] getPeerSupportedSignatureAlgorithms() { + return new String[] {"bar", "foo"}; + } + + @Override + public List getRequestedServerNames() { + return List.of(new SNIHostName("localhost")); + } + + @Override + public List getStatusResponses() { + return List.of(new byte[] {'4', '2'}); + } + } + +} diff --git a/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java b/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java index 2f4f96e8b9b..d1e3cc026d3 100644 --- a/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java +++ b/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -22,19 +22,39 @@ */ /* - * @test + * @test id=http3 * @summary Tests an asynchronous BodySubscriber that completes * immediately with an InputStream which issues bad * requests * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker * jdk.httpclient.test.lib.common.HttpServerAdapters - * @run testng/othervm InvalidInputStreamSubscriptionRequest + * @run testng/othervm -Dtest.http.version=http3 + * -Djdk.internal.httpclient.debug=true + * InvalidInputStreamSubscriptionRequest + */ +/* + * @test id=http2 + * @summary Tests an asynchronous BodySubscriber that completes + * immediately with an InputStream which issues bad + * requests + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm -Dtest.http.version=http2 InvalidInputStreamSubscriptionRequest + */ +/* + * @test id=http1 + * @summary Tests an asynchronous BodySubscriber that completes + * immediately with an InputStream which issues bad + * requests + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm -Dtest.http.version=http1 InvalidInputStreamSubscriptionRequest */ import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterTest; @@ -47,7 +67,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -55,7 +74,6 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.HttpResponse.BodySubscribers; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -67,15 +85,16 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -86,6 +105,7 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -94,6 +114,8 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -180,46 +202,81 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters @DataProvider(name = "variants") public Object[][] variants() { - return new Object[][]{ - { httpURI_fixed, false, OF_INPUTSTREAM }, - { httpURI_chunk, false, OF_INPUTSTREAM }, - { httpsURI_fixed, false, OF_INPUTSTREAM }, - { httpsURI_chunk, false, OF_INPUTSTREAM }, + Object[][] http3 = new Object[][]{ + {http3URI_fixed, false, OF_INPUTSTREAM}, + {http3URI_chunk, false, OF_INPUTSTREAM}, + {http3URI_fixed, true, OF_INPUTSTREAM}, + {http3URI_chunk, true, OF_INPUTSTREAM}, + }; + Object[][] http1 = new Object[][] { + {httpURI_fixed, false, OF_INPUTSTREAM}, + {httpURI_chunk, false, OF_INPUTSTREAM}, + {httpsURI_fixed, false, OF_INPUTSTREAM}, + {httpsURI_chunk, false, OF_INPUTSTREAM}, + {httpURI_fixed, true, OF_INPUTSTREAM}, + {httpURI_chunk, true, OF_INPUTSTREAM}, + {httpsURI_fixed, true, OF_INPUTSTREAM}, + {httpsURI_chunk, true, OF_INPUTSTREAM}, + }; + Object[][] http2 = new Object[][] { { http2URI_fixed, false, OF_INPUTSTREAM }, { http2URI_chunk, false, OF_INPUTSTREAM }, { https2URI_fixed, false, OF_INPUTSTREAM }, { https2URI_chunk, false, OF_INPUTSTREAM }, - - { httpURI_fixed, true, OF_INPUTSTREAM }, - { httpURI_chunk, true, OF_INPUTSTREAM }, - { httpsURI_fixed, true, OF_INPUTSTREAM }, - { httpsURI_chunk, true, OF_INPUTSTREAM }, { http2URI_fixed, true, OF_INPUTSTREAM }, { http2URI_chunk, true, OF_INPUTSTREAM }, { https2URI_fixed, true, OF_INPUTSTREAM }, { https2URI_chunk, true, OF_INPUTSTREAM }, }; + String version = System.getProperty("test.http.version"); + if ("http3".equals(version)) { + return http3; + } + if ("http2".equals(version)) { + return http2; + } + if ("http1".equals(version)) { + return http1; + } + if (version == null) throw new AssertionError("test.http.version not set"); + throw new AssertionError("test.http.version should be set to http3|http2|http1. Found " + version); } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; - HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + HttpClient newHttpClient(String uri) { + HttpClient.Builder builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return TRACKER.track(builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build()); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void testNoBody(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) - client = newHttpClient(); + if (!sameClient || client == null) { + client = newHttpClient(uri); + } - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -246,7 +303,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -256,11 +329,12 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -295,7 +369,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -305,11 +395,12 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -331,7 +422,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -341,11 +448,12 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -374,7 +482,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -476,20 +600,32 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_VariableLengthHandler(); + + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest public void teardown() throws Exception { - AssertionError fail = TRACKER.check(500); + AssertionError fail = TRACKER.check(1500); try { httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java b/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java index 3cd7cb629ea..327dc57d6df 100644 --- a/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java +++ b/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -34,8 +34,6 @@ */ import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -47,7 +45,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -69,11 +66,13 @@ import java.util.concurrent.Flow.Publisher; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -84,6 +83,7 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -92,6 +92,8 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -127,6 +129,11 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ + { http3URI_fixed, false, OF_PUBLISHER_API }, + { http3URI_chunk, false, OF_PUBLISHER_API }, + { http3URI_fixed, true, OF_PUBLISHER_API }, + { http3URI_chunk, true, OF_PUBLISHER_API }, + { httpURI_fixed, false, OF_PUBLISHER_API }, { httpURI_chunk, false, OF_PUBLISHER_API }, { httpsURI_fixed, false, OF_PUBLISHER_API }, @@ -148,22 +155,35 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; - HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + HttpClient newHttpClient(String uri) { + HttpClient.Builder builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return TRACKER.track(builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build()); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void testNoBody(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -190,7 +210,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -198,11 +234,12 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @Test(dataProvider = "variants") public void testNoBodyAsync(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -234,7 +271,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -242,11 +295,12 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @Test(dataProvider = "variants") public void testAsString(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -269,7 +323,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -277,11 +347,12 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @Test(dataProvider = "variants") public void testAsStringAsync(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -307,7 +378,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -409,10 +496,21 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_VariableLengthHandler(); + + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -423,6 +521,7 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/LargeHandshakeTest.java b/test/jdk/java/net/httpclient/LargeHandshakeTest.java index 1f4c85b361e..ff3981e8b3d 100644 --- a/test/jdk/java/net/httpclient/LargeHandshakeTest.java +++ b/test/jdk/java/net/httpclient/LargeHandshakeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, 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 @@ -62,6 +62,9 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.TestServerConfigurator; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -964,12 +967,14 @@ public class LargeHandshakeTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -988,8 +993,7 @@ public class LargeHandshakeTest implements HttpServerAdapters { } public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .proxy(ps); @@ -1028,6 +1032,12 @@ public class LargeHandshakeTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/LargeHandshakeTest/https2/"); + // HTTP/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestLargeHandler(), "/LargeHandshakeTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/LargeHandshakeTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -1073,9 +1083,11 @@ public class LargeHandshakeTest implements HttpServerAdapters { } public void run(String... args) throws Exception { - List serverURIs = List.of(http1URI, http2URI, https1URI, https2URI); + List serverURIs = List.of(http3URI, http1URI, http2URI, https1URI, https2URI); for (int i = 0; i < 5; i++) { for (URI base : serverURIs) { + // skip HTTP/3 if proxy + if (base.getRawPath().contains("/http3/")) continue; if (base.getScheme().equalsIgnoreCase("https")) { URI proxy = i % 1 == 0 ? base.resolve(URI.create("proxy/foo?n=" + requestCounter.incrementAndGet())) : base.resolve(URI.create("direct/foo?n=" + requestCounter.incrementAndGet())); @@ -1090,10 +1102,19 @@ public class LargeHandshakeTest implements HttpServerAdapters { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + public void test(URI uri) throws Exception { System.out.println("Testing with " + uri); pending.add(uri); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); CompletableFuture> resp = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete((r, t) -> this.requestCompleted(request, r, t)); @@ -1111,11 +1132,13 @@ public class LargeHandshakeTest implements HttpServerAdapters { } public void tearDown() { + client.close(); proxy = stop(proxy, DigestEchoServer.TunnelingProxy::stop); http1Server = stop(http1Server, HttpTestServer::stop); https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/LargeResponseTest.java b/test/jdk/java/net/httpclient/LargeResponseTest.java index bfc7c9ca5db..02cf5ddcc8d 100644 --- a/test/jdk/java/net/httpclient/LargeResponseTest.java +++ b/test/jdk/java/net/httpclient/LargeResponseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, 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 @@ -51,6 +51,9 @@ import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -65,6 +68,7 @@ import static java.net.http.HttpClient.Version.HTTP_2; * @run main/othervm -Dtest.requiresHost=true * -Djdk.httpclient.HttpClient.log=headers * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.quic.maxInitialTimeout=60 * LargeResponseTest * */ @@ -94,12 +98,14 @@ public class LargeResponseTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -112,8 +118,7 @@ public class LargeResponseTest implements HttpServerAdapters { TimeUnit.SECONDS, new LinkedBlockingQueue<>()); public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .proxy(ps); @@ -152,6 +157,12 @@ public class LargeResponseTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/LargeResponseTest/https2/"); + // HTTP/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestLargeHandler(), "/LargeResponseTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/LargeResponseTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -182,9 +193,11 @@ public class LargeResponseTest implements HttpServerAdapters { } public void run(String... args) throws Exception { - List serverURIs = List.of(http1URI, http2URI, https1URI, https2URI); + List serverURIs = List.of(http3URI, http1URI, http2URI, https1URI, https2URI); for (int i=0; i<5; i++) { for (URI base : serverURIs) { + // no proxy with HTTP/3 + if (base.getRawPath().contains("/http3/")) continue; if (base.getScheme().equalsIgnoreCase("https")) { URI proxy = i % 1 == 0 ? base.resolve(URI.create("proxy/foo?n="+requestCounter.incrementAndGet())) : base.resolve(URI.create("direct/foo?n="+requestCounter.incrementAndGet())); @@ -199,10 +212,19 @@ public class LargeResponseTest implements HttpServerAdapters { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + public void test(URI uri) throws Exception { System.out.println("Testing with " + uri); pending.add(uri); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); CompletableFuture> resp = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete((r, t) -> this.requestCompleted(request, r, t)); @@ -220,11 +242,13 @@ public class LargeResponseTest implements HttpServerAdapters { } public void tearDown() { + client.close(); proxy = stop(proxy, DigestEchoServer.TunnelingProxy::stop); http1Server = stop(http1Server, HttpTestServer::stop); https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/LineBodyHandlerTest.java b/test/jdk/java/net/httpclient/LineBodyHandlerTest.java index 3271f21a130..62afcab9ee2 100644 --- a/test/jdk/java/net/httpclient/LineBodyHandlerTest.java +++ b/test/jdk/java/net/httpclient/LineBodyHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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,8 +28,6 @@ import java.io.PrintStream; import java.io.StringReader; import java.io.UncheckedIOException; import java.math.BigInteger; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Builder; @@ -55,10 +53,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -67,6 +61,9 @@ import org.testng.annotations.Test; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_16; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpRequest.BodyPublishers.ofString; @@ -90,14 +87,16 @@ import static org.testng.Assert.assertTrue; public class LineBodyHandlerTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; final AtomicInteger clientCount = new AtomicInteger(); @@ -106,6 +105,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { @DataProvider(name = "uris") public Object[][] variants() { return new Object[][]{ + { http3URI }, { httpURI }, { httpsURI }, { http2URI }, @@ -195,17 +195,26 @@ public class LineBodyHandlerTest implements HttpServerAdapters { return sharedClient; } clientCount.incrementAndGet(); - return sharedClient = TRACKER.track(HttpClient.newBuilder() + return sharedClient = TRACKER.track(newClientBuilderForH3() .sslContext(sslContext) .proxy(Builder.NO_PROXY) .build()); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "uris") void testStringWithFinisher(String url) { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -226,7 +235,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStream(String url) { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -249,7 +258,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { String body = "May the luck\r\n\r\n of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -270,7 +279,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithCRLF(String url) { String body = "May the luck\r\n\r\n of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -294,7 +303,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testStringWithFinisherBlocking(String url) throws Exception { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)).build(); StringSubscriber subscriber = new StringSubscriber(); @@ -311,7 +320,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testStringWithoutFinisherBlocking(String url) throws Exception { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)).build(); StringSubscriber subscriber = new StringSubscriber(); @@ -330,7 +339,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithMixedCRLF(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -357,7 +366,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithMixedCRLF_UTF8(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .header("Content-type", "text/text; charset=UTF-8") .POST(BodyPublishers.ofString(body, UTF_8)).build(); @@ -383,7 +392,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithMixedCRLF_UTF16(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .header("Content-type", "text/text; charset=UTF-16") .POST(BodyPublishers.ofString(body, UTF_16)).build(); @@ -410,7 +419,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithFinisher(String url) { String body = "May\r\n the wind\r\n always be\rat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -435,7 +444,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithFinisher_UTF16(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .header("Content-type", "text/text; charset=UTF-16") .POST(BodyPublishers.ofString(body, UTF_16)).build(); ObjectSubscriber subscriber = new ObjectSubscriber(); @@ -461,7 +470,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithoutFinisher(String url) { String body = "May\r\n the wind\r\n always be\rat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -487,7 +496,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithFinisherBlocking(String url) throws Exception { String body = "May\r\n the wind\r\n always be\nat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -511,7 +520,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithoutFinisherBlocking(String url) throws Exception { String body = "May\r\n the wind\r\n always be\nat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -546,7 +555,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testBigTextFromLineSubscriber(String url) { HttpClient client = newClient(); String bigtext = bigtext(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(bigtext)) .build(); @@ -567,7 +576,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testBigTextAsStream(String url) { HttpClient client = newClient(); String bigtext = bigtext(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(bigtext)) .build(); @@ -690,10 +699,15 @@ public class LineBodyHandlerTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpTestEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpTestEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -712,6 +726,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/ManyRequests.java b/test/jdk/java/net/httpclient/ManyRequests.java index c196ef76466..190205a9ef5 100644 --- a/test/jdk/java/net/httpclient/ManyRequests.java +++ b/test/jdk/java/net/httpclient/ManyRequests.java @@ -25,11 +25,11 @@ * @test * @bug 8087112 8180044 8256459 * @key intermittent - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib - * @build jdk.test.lib.net.SimpleSSLContext + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/EchoHandler.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java @@ -77,7 +77,9 @@ import java.util.logging.Level; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.Platform; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; @@ -107,7 +109,7 @@ public class ManyRequests { InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); HttpsServer server = HttpsServer.create(addr, 0); ExecutorService executor = executorFor("HTTPS/1.1 Server Thread"); - server.setHttpsConfigurator(new Configurator(ctx)); + server.setHttpsConfigurator(new Configurator(addr.getAddress(), ctx)); server.setExecutor(executor); ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .name("HttpClient-Worker", 0).factory()); @@ -366,12 +368,17 @@ public class ManyRequests { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } + @Override public void configure(HttpsParameters params) { - params.setSSLParameters(getSSLContext().getSupportedSSLParameters()); + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } diff --git a/test/jdk/java/net/httpclient/ManyRequests2.java b/test/jdk/java/net/httpclient/ManyRequests2.java index e6aa5ffa83f..a1e2307b820 100644 --- a/test/jdk/java/net/httpclient/ManyRequests2.java +++ b/test/jdk/java/net/httpclient/ManyRequests2.java @@ -24,11 +24,11 @@ /* * @test * @bug 8087112 8180044 8256459 - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib - * @build jdk.test.lib.net.SimpleSSLContext + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/EchoHandler.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java diff --git a/test/jdk/java/net/httpclient/ManyRequestsLegacy.java b/test/jdk/java/net/httpclient/ManyRequestsLegacy.java index 010c04cc51d..1d8091d3085 100644 --- a/test/jdk/java/net/httpclient/ManyRequestsLegacy.java +++ b/test/jdk/java/net/httpclient/ManyRequestsLegacy.java @@ -23,11 +23,11 @@ /* * @test - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib - * @build jdk.test.lib.net.SimpleSSLContext + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/EchoHandler.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java @@ -65,6 +65,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; @@ -81,6 +82,7 @@ import java.util.Random; import java.util.logging.Logger; import java.util.logging.Level; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.Platform; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; @@ -112,7 +114,7 @@ public class ManyRequestsLegacy { }); InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); HttpsServer server = HttpsServer.create(addr, 0); - server.setHttpsConfigurator(new Configurator(ctx)); + server.setHttpsConfigurator(new Configurator(addr.getAddress(), ctx)); LegacyHttpClient client = new LegacyHttpClient(); @@ -431,12 +433,18 @@ public class ManyRequestsLegacy { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } + @Override public void configure(HttpsParameters params) { - params.setSSLParameters(getSSLContext().getSupportedSSLParameters()); + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } } diff --git a/test/jdk/java/net/httpclient/MappingResponseSubscriber.java b/test/jdk/java/net/httpclient/MappingResponseSubscriber.java index 6f5964970cc..5e5497cf0e5 100644 --- a/test/jdk/java/net/httpclient/MappingResponseSubscriber.java +++ b/test/jdk/java/net/httpclient/MappingResponseSubscriber.java @@ -49,11 +49,9 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscribers; import java.net.http.HttpResponse.BodySubscriber; import java.util.function.Function; @@ -64,6 +62,7 @@ import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -149,14 +148,14 @@ public class MappingResponseSubscriber { Tracker tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker, 1500); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1500)); if (error != null) throw error; // the client didn't shut down properly } if (sameClient) { Tracker tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker,1500); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1500)); if (error != null) throw error; // the client didn't shut down properly } } diff --git a/test/jdk/java/net/httpclient/NoBodyPartOne.java b/test/jdk/java/net/httpclient/NoBodyPartOne.java index d05d3e9c5e1..7c7a51c92e7 100644 --- a/test/jdk/java/net/httpclient/NoBodyPartOne.java +++ b/test/jdk/java/net/httpclient/NoBodyPartOne.java @@ -44,6 +44,7 @@ import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -57,6 +58,9 @@ public class NoBodyPartOne extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -78,6 +82,9 @@ public class NoBodyPartOne extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { @@ -101,6 +108,9 @@ public class NoBodyPartOne extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { diff --git a/test/jdk/java/net/httpclient/NoBodyPartThree.java b/test/jdk/java/net/httpclient/NoBodyPartThree.java index d0f71130421..d5e310d1914 100644 --- a/test/jdk/java/net/httpclient/NoBodyPartThree.java +++ b/test/jdk/java/net/httpclient/NoBodyPartThree.java @@ -28,6 +28,7 @@ * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer * @run testng/othervm + * -Djdk.httpclient.HttpClient.log=quic,errors * -Djdk.httpclient.HttpClient.log=all * NoBodyPartThree */ @@ -44,8 +45,10 @@ import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import jdk.internal.net.http.common.Utils; import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -62,6 +65,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testAsByteArrayPublisher/first/" + REQID.getAndIncrement(); @@ -72,7 +78,7 @@ public class NoBodyPartThree extends AbstractNoBody { Consumer> consumer = oba -> { consumerHasBeenCalled = true; oba.ifPresent(ba -> fail("Unexpected non-empty optional:" - + asString(ByteBuffer.wrap(ba)))); + + Utils.asString(ByteBuffer.wrap(ba)))); }; consumerHasBeenCalled = false; var response = client.send(req, BodyHandlers.ofByteArrayConsumer(consumer)); @@ -99,6 +105,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testStringPublisher/" + REQID.getAndIncrement(); @@ -121,6 +130,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testInputStreamPublisherBuffering/" + REQID.getAndIncrement(); @@ -144,6 +156,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testEmptyArrayPublisher/" + REQID.getAndIncrement(); diff --git a/test/jdk/java/net/httpclient/NoBodyPartTwo.java b/test/jdk/java/net/httpclient/NoBodyPartTwo.java index 563516b11bf..f7d331cb526 100644 --- a/test/jdk/java/net/httpclient/NoBodyPartTwo.java +++ b/test/jdk/java/net/httpclient/NoBodyPartTwo.java @@ -43,8 +43,10 @@ import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import jdk.internal.net.http.common.Utils; import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -59,6 +61,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -67,7 +72,7 @@ public class NoBodyPartTwo extends AbstractNoBody { Consumer> consumer = oba -> { consumerHasBeenCalled = true; oba.ifPresent(ba -> fail("Unexpected non-empty optional: " - + asString(ByteBuffer.wrap(ba)))); + + Utils.asString(ByteBuffer.wrap(ba)))); }; consumerHasBeenCalled = false; client.send(req, BodyHandlers.ofByteArrayConsumer(consumer)); @@ -83,6 +88,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -102,6 +110,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -122,6 +133,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) diff --git a/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java b/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java index 56a35119ae2..ba93645aef4 100644 --- a/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java +++ b/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -30,7 +30,7 @@ * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * @compile -encoding utf-8 NonAsciiCharsInURI.java * @run testng/othervm - * -Djdk.httpclient.HttpClient.log=reqeusts,headers + * -Djdk.httpclient.HttpClient.log=requests,headers,errors,quic * NonAsciiCharsInURI */ @@ -49,6 +49,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -58,6 +59,8 @@ import static java.lang.System.err; import static java.lang.System.out; 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 java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -65,14 +68,17 @@ import static org.testng.Assert.assertEquals; public class NonAsciiCharsInURI implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; + String http3URI_head; private volatile HttpClient sharedClient; @@ -104,6 +110,9 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { Arrays.asList(pathsAndQueryStrings).stream() .map(e -> new Object[] {https2URI + e[0], sameClient}) .forEach(list::add); + Arrays.asList(pathsAndQueryStrings).stream() + .map(e -> new Object[] {http3URI + e[0], sameClient}) + .forEach(list::add); } return list.stream().toArray(Object[][]::new); } @@ -122,11 +131,38 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + private HttpClient makeNewClient() { - return HttpClient.newBuilder() + return newClientBuilderForH3() .proxy(NO_PROXY) .sslContext(sslContext) .build(); @@ -167,11 +203,14 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); HttpResponse resp = client.send(request, BodyHandlers.ofString()); out.println("Got response: " + resp); @@ -203,10 +242,13 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); client.sendAsync(request, BodyHandlers.ofString()) .thenApply(response -> { @@ -259,16 +301,28 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { https2TestServer.addHandler(handler, "/https2/get"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/get"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new HttpUriStringHandler(), "/http3/get"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/get"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + err.println(now() + "Starting servers"); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); out.println(now() + "setup done"); err.println(now() + "setup done"); @@ -281,6 +335,7 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } /** A handler that returns, as its body, the exact received request URI. */ diff --git a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java index f3bf5521f4b..b5ea5da13e4 100644 --- a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java +++ b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.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 @@ -40,9 +40,6 @@ * @run testng/othervm BodyHandlerOfFileDownloadTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import org.testng.annotations.AfterTest; @@ -54,8 +51,6 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -68,16 +63,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; -import jdk.httpclient.test.lib.http2.OutgoingPushPromise; -import jdk.httpclient.test.lib.http2.Queue; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; @@ -89,14 +81,16 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { static final String contentDispositionValue = "attachment; filename=example.html"; SSLContext sslContext; - HttpServerAdapters.HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] - HttpServerAdapters.HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] + HttpTestServer httpsTestServer; // HTTPS/1.1 + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; FileSystem zipFs; Path defaultFsPath; @@ -115,6 +109,9 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { @DataProvider(name = "defaultFsData") public Object[][] defaultFsData() { return new Object[][]{ + { http3URI, defaultFsPath, MSG, true }, + { http3URI, defaultFsPath, MSG, false }, + { httpURI, defaultFsPath, MSG, true }, { httpsURI, defaultFsPath, MSG, true }, { http2URI, defaultFsPath, MSG, true }, @@ -138,6 +135,24 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { private static final int ITERATION_COUNT = 3; + private HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return builder.proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + } + + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + private void receive(String uriString, Path path, String expectedMsg, @@ -146,12 +161,9 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { for (int i = 0; i < ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(NO_PROXY) - .sslContext(sslContext) - .build(); + client = newHttpClient(uriString); } - var req = HttpRequest.newBuilder(URI.create(uriString)) + var req = newRequestBuilder(URI.create(uriString)) .POST(BodyPublishers.noBody()) .build(); var resp = client.send(req, BodyHandlers.ofFileDownload(path, CREATE, TRUNCATE_EXISTING, WRITE)); @@ -163,6 +175,12 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { assertEquals(msg, expectedMsg); assertTrue(resp.headers().firstValue("Content-Disposition").isPresent()); assertEquals(resp.headers().firstValue("Content-Disposition").get(), contentDispositionValue); + if (!sameClient) { + client.close(); + } + } + if (sameClient && client != null) { + client.close(); } } @@ -198,26 +216,31 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { zipFs = newZipFs(); zipFsPath = zipFsDir(zipFs); - httpTestServer = HttpServerAdapters.HttpTestServer.create(HTTP_1_1); + httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new HttpEchoHandler(), "/http1/echo"); httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/echo"; - httpsTestServer = HttpServerAdapters.HttpTestServer.create(HTTP_1_1, sslContext); + httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(new HttpEchoHandler(), "/https1/echo"); httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1/echo"; - http2TestServer = HttpServerAdapters.HttpTestServer.create(HTTP_2); + http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(new HttpEchoHandler(), "/http2/echo"); http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo"; - https2TestServer = HttpServerAdapters.HttpTestServer.create(HTTP_2, sslContext); + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -231,6 +254,7 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); zipFs.close(); } diff --git a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java index ee8a9c7dcbb..42d5c2596c7 100644 --- a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java +++ b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.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 @@ -39,9 +39,6 @@ * @run testng/othervm BodyHandlerOfFileTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import org.testng.annotations.AfterTest; @@ -53,8 +50,6 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -64,30 +59,29 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.Map; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; -import jdk.httpclient.test.lib.http2.OutgoingPushPromise; -import jdk.httpclient.test.lib.http2.Queue; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; public class BodyHandlerOfFileTest implements HttpServerAdapters { static final String MSG = "msg"; SSLContext sslContext; - HttpServerAdapters.HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] - HttpServerAdapters.HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] + HttpTestServer httpsTestServer; // HTTPS/1.1 + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; FileSystem zipFs; Path defaultFsPath; @@ -106,6 +100,9 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { @DataProvider(name = "defaultFsData") public Object[][] defaultFsData() { return new Object[][]{ + { http3URI, defaultFsPath, MSG, true }, + { http3URI, defaultFsPath, MSG, false }, + { httpURI, defaultFsPath, MSG, true }, { httpsURI, defaultFsPath, MSG, true }, { http2URI, defaultFsPath, MSG, true }, @@ -145,6 +142,9 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { @DataProvider(name = "zipFsData") public Object[][] zipFsData() { return new Object[][]{ + { http3URI, zipFsPath, MSG, true }, + { http3URI, zipFsPath, MSG, false }, + { httpURI, zipFsPath, MSG, true }, { httpsURI, zipFsPath, MSG, true }, { http2URI, zipFsPath, MSG, true }, @@ -168,6 +168,24 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { private static final int ITERATION_COUNT = 3; + private HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return builder.proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + } + + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + private void receive(String uriString, Path path, String expectedMsg, @@ -176,12 +194,9 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { for (int i = 0; i < ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(NO_PROXY) - .sslContext(sslContext) - .build(); + client = newHttpClient(uriString); } - var req = HttpRequest.newBuilder(URI.create(uriString)) + var req = newRequestBuilder(URI.create(uriString)) .POST(BodyPublishers.noBody()) .build(); var resp = client.send(req, HttpResponse.BodyHandlers.ofFile(path)); @@ -190,6 +205,12 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { out.printf("Msg written to %s: %s\n", resp.body(), msg); assertEquals(resp.statusCode(), 200); assertEquals(msg, expectedMsg); + if (!sameClient) { + client.close(); + } + } + if (sameClient && client != null) { + client.close(); } } @@ -219,10 +240,15 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -236,6 +262,7 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); zipFs.close(); } diff --git a/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java b/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java index f2adc89ec17..2a7ba90c7b2 100644 --- a/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java +++ b/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.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 @@ -38,9 +38,6 @@ * @run testng/othervm BodySubscriberOfFileTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import org.testng.annotations.AfterTest; @@ -52,14 +49,11 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; import java.nio.Buffer; import java.nio.ByteBuffer; @@ -69,30 +63,29 @@ import java.util.Map; import java.util.concurrent.Flow; import java.util.stream.IntStream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; -import jdk.httpclient.test.lib.http2.OutgoingPushPromise; -import jdk.httpclient.test.lib.http2.Queue; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; public class BodySubscriberOfFileTest implements HttpServerAdapters { static final String MSG = "msg"; SSLContext sslContext; - HttpServerAdapters.HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] - HttpServerAdapters.HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] + HttpTestServer httpsTestServer; // HTTPS/1.1 + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; FileSystem zipFs; Path defaultFsPath; @@ -111,6 +104,9 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { @DataProvider(name = "defaultFsData") public Object[][] defaultFsData() { return new Object[][]{ + { http3URI, defaultFsPath, MSG, true }, + { http3URI, defaultFsPath, MSG, false }, + { httpURI, defaultFsPath, MSG, true }, { httpsURI, defaultFsPath, MSG, true }, { http2URI, defaultFsPath, MSG, true }, @@ -150,6 +146,9 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { @DataProvider(name = "zipFsData") public Object[][] zipFsData() { return new Object[][]{ + { http3URI, zipFsPath, MSG, true }, + { http3URI, zipFsPath, MSG, false }, + { httpURI, zipFsPath, MSG, true }, { httpsURI, zipFsPath, MSG, true }, { http2URI, zipFsPath, MSG, true }, @@ -173,6 +172,24 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { private static final int ITERATION_COUNT = 3; + private HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return builder.proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + } + + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + private void receive(String uriString, Path path, String expectedMsg, @@ -181,12 +198,9 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { for (int i = 0; i < ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(NO_PROXY) - .sslContext(sslContext) - .build(); + client = newHttpClient(uriString); } - var req = HttpRequest.newBuilder(URI.create(uriString)) + var req = newRequestBuilder(URI.create(uriString)) .POST(BodyPublishers.noBody()) .build(); @@ -197,6 +211,12 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { out.printf("Msg written to %s: %s\n", resp.body(), msg); assertEquals(resp.statusCode(), 200); assertEquals(msg, expectedMsg); + if (!sameClient) { + client.close(); + } + } + if (sameClient && client != null) { + client.close(); } } @@ -250,10 +270,15 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -267,6 +292,7 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); zipFs.close(); } diff --git a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java index f0eb63f2a40..6d9d7e4f11b 100644 --- a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java +++ b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java @@ -35,16 +35,29 @@ * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes=Basic,Digest * -Djdk.http.auth.tunneling.disabledSchemes=Digest,Basic - * ProxyAuthDisabledSchemesSSL SSL + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.internal.httpclient.debug=err + * -Djdk.httpclient.HttpClient.log=headers + * ProxyAuthDisabledSchemesSSL SSL SERVER307 + * @run main/othervm/timeout=300 + * -Djdk.http.auth.proxying.disabledSchemes=Basic,Digest + * -Djdk.http.auth.tunneling.disabledSchemes=Digest,Basic + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers + * ProxyAuthDisabledSchemesSSL SSL SERVER PROXY * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes=Basic * -Djdk.http.auth.tunneling.disabledSchemes=Basic * -Dtest.requiresHost=true + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers * ProxyAuthDisabledSchemesSSL SSL PROXY * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes=Digest * -Djdk.http.auth.tunneling.disabledSchemes=Digest * -Dtest.requiresHost=true + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers * ProxyAuthDisabledSchemesSSL SSL PROXY */ diff --git a/test/jdk/java/net/httpclient/ProxyTest.java b/test/jdk/java/net/httpclient/ProxyTest.java index 8763e168a06..45c21e7ef4a 100644 --- a/test/jdk/java/net/httpclient/ProxyTest.java +++ b/test/jdk/java/net/httpclient/ProxyTest.java @@ -52,10 +52,13 @@ import java.util.concurrent.CopyOnWriteArrayList; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; + +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.net.SimpleSSLContext; import static java.net.Proxy.NO_PROXY; @@ -67,9 +70,10 @@ import static java.net.Proxy.NO_PROXY; * Verifies that downgrading from HTTP/2 to HTTP/1.1 works through * an SSL Tunnel connection when the client is HTTP/2 and the server * and proxy are HTTP/1.1 - * @modules java.net.http - * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.common + * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext ProxyTest + * jdk.httpclient.test.lib.common.TestServerConfigurator * @run main/othervm ProxyTest * @author danielfuchs */ @@ -103,9 +107,8 @@ public class ProxyTest { he.close(); } }); - - server.setHttpsConfigurator(new Configurator(SSLContext.getDefault())); InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + server.setHttpsConfigurator(new Configurator(addr.getAddress(), SSLContext.getDefault())); server.bind(addr, 0); return server; } @@ -407,13 +410,18 @@ public class ProxyTest { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } @Override - public void configure (HttpsParameters params) { - params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); + public void configure (final HttpsParameters params) { + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } diff --git a/test/jdk/java/net/httpclient/RedirectMethodChange.java b/test/jdk/java/net/httpclient/RedirectMethodChange.java index ed1afb384e1..f713c0dc5d1 100644 --- a/test/jdk/java/net/httpclient/RedirectMethodChange.java +++ b/test/jdk/java/net/httpclient/RedirectMethodChange.java @@ -33,19 +33,13 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -53,6 +47,9 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.testng.Assert.assertEquals; @@ -61,14 +58,16 @@ public class RedirectMethodChange implements HttpServerAdapters { SSLContext sslContext; HttpClient client; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String RESPONSE = "Hello world"; static final String POST_BODY = "This is the POST body 123909090909090"; @@ -90,6 +89,22 @@ public class RedirectMethodChange implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][] { + { http3URI, "GET", 301, "GET" }, + { http3URI, "GET", 302, "GET" }, + { http3URI, "GET", 303, "GET" }, + { http3URI, "GET", 307, "GET" }, + { http3URI, "GET", 308, "GET" }, + { http3URI, "POST", 301, "GET" }, + { http3URI, "POST", 302, "GET" }, + { http3URI, "POST", 303, "GET" }, + { http3URI, "POST", 307, "POST" }, + { http3URI, "POST", 308, "POST" }, + { http3URI, "PUT", 301, "PUT" }, + { http3URI, "PUT", 302, "PUT" }, + { http3URI, "PUT", 303, "GET" }, + { http3URI, "PUT", 307, "PUT" }, + { http3URI, "PUT", 308, "PUT" }, + { httpURI, "GET", 301, "GET" }, { httpURI, "GET", 302, "GET" }, { httpURI, "GET", 303, "GET" }, @@ -156,6 +171,15 @@ public class RedirectMethodChange implements HttpServerAdapters { }; } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void test(String uriString, String method, @@ -163,7 +187,7 @@ public class RedirectMethodChange implements HttpServerAdapters { String expectedMethod) throws Exception { - HttpRequest req = HttpRequest.newBuilder(URI.create(uriString)) + HttpRequest req = newRequestBuilder(URI.create(uriString)) .method(method, getRequestBodyFor(method)) .header("X-Redirect-Code", Integer.toString(redirectCode)) .header("X-Expect-Method", expectedMethod) @@ -183,7 +207,7 @@ public class RedirectMethodChange implements HttpServerAdapters { if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); - client = HttpClient.newBuilder() + client = newClientBuilderForH3() .followRedirects(HttpClient.Redirect.NORMAL) .sslContext(sslContext) .build(); @@ -212,10 +236,17 @@ public class RedirectMethodChange implements HttpServerAdapters { https2TestServer.addHandler(handler, "/https2/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test/rmt"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + targetURI = "https://" + http3TestServer.serverAuthority() + "/http3/redirect/rmt"; + handler = new RedirMethodChgeHandler(targetURI); + http3TestServer.addHandler(handler, "/http3/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/test/rmt"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -225,6 +256,7 @@ public class RedirectMethodChange implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } /** diff --git a/test/jdk/java/net/httpclient/RedirectTimeoutTest.java b/test/jdk/java/net/httpclient/RedirectTimeoutTest.java index 88b8fd964b0..be634398ba2 100644 --- a/test/jdk/java/net/httpclient/RedirectTimeoutTest.java +++ b/test/jdk/java/net/httpclient/RedirectTimeoutTest.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 @@ -32,10 +32,8 @@ * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,trace -Djdk.internal.httpclient.debug=false RedirectTimeoutTest */ -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestResponseHeaders; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; import org.testng.TestException; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -54,16 +52,24 @@ import java.net.http.HttpTimeoutException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.Optional; + +import javax.net.ssl.SSLContext; import static java.net.http.HttpClient.Redirect.ALWAYS; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static jdk.test.lib.Utils.adjustTimeout; -public class RedirectTimeoutTest { +public class RedirectTimeoutTest implements HttpServerAdapters { - static HttpTestServer h1TestServer, h2TestServer; - static URI h1Uri, h1RedirectUri, h2Uri, h2RedirectUri, h2WarmupUri, testRedirectURI; + static SSLContext sslContext; + static HttpTestServer h1TestServer, h2TestServer, h3TestServer; + static URI h1Uri, h1RedirectUri, h2Uri, h2RedirectUri, + h3Uri, h3RedirectUri, h2WarmupUri, h3WarmupUri, testRedirectURI; private static final long TIMEOUT_MILLIS = 3000L; // 3s private static final long SLEEP_TIME = 1500L; // 1.5s public static final int ITERATIONS = 4; @@ -71,33 +77,47 @@ public class RedirectTimeoutTest { @BeforeTest public void setup() throws IOException { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + h1TestServer = HttpTestServer.create(HTTP_1_1); h2TestServer = HttpTestServer.create(HTTP_2); + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); h1Uri = URI.create("http://" + h1TestServer.serverAuthority() + "/h1_test"); h1RedirectUri = URI.create("http://" + h1TestServer.serverAuthority() + "/h1_redirect"); h2Uri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_test"); h2RedirectUri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_redirect"); + h3Uri = URI.create("https://" + h3TestServer.serverAuthority() + "/h3_test"); + h3RedirectUri = URI.create("https://" + h3TestServer.serverAuthority() + "/h3_redirect"); h2WarmupUri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_warmup"); + h3WarmupUri = URI.create("https://" + h3TestServer.serverAuthority() + "/h3_warmup"); h1TestServer.addHandler(new GetHandler(), "/h1_test"); h1TestServer.addHandler(new RedirectHandler(), "/h1_redirect"); h2TestServer.addHandler(new GetHandler(), "/h2_test"); h2TestServer.addHandler(new RedirectHandler(), "/h2_redirect"); - h2TestServer.addHandler(new Http2Warmup(), "/h2_warmup"); + h3TestServer.addHandler(new GetHandler(), "/h3_test"); + h3TestServer.addHandler(new RedirectHandler(), "/h3_redirect"); + h2TestServer.addHandler(new HttpWarmup(), "/h2_warmup"); + h3TestServer.addHandler(new HttpWarmup(), "/h3_warmup"); h1TestServer.start(); h2TestServer.start(); + h3TestServer.start(); } @AfterTest public void teardown() { h1TestServer.stop(); h2TestServer.stop(); + h3TestServer.stop(); } @DataProvider(name = "testData") public Object[][] testData() { return new Object[][] { + { HTTP_3, h3Uri, h3RedirectUri }, { HTTP_1_1, h1Uri, h1RedirectUri }, - { HTTP_2, h2Uri, h2RedirectUri } + { HTTP_2, h2Uri, h2RedirectUri }, }; } @@ -105,16 +125,31 @@ public class RedirectTimeoutTest { public void test(Version version, URI uri, URI redirectURI) throws InterruptedException { out.println("Testing for " + version); testRedirectURI = redirectURI; - HttpClient.Builder clientBuilder = HttpClient.newBuilder().followRedirects(ALWAYS); - HttpRequest request = HttpRequest.newBuilder().uri(uri) + HttpClient.Builder clientBuilder = version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + clientBuilder = clientBuilder.followRedirects(ALWAYS).sslContext(sslContext); + HttpRequest.Builder reqBuilder = HttpRequest.newBuilder().uri(uri); + if (version == HTTP_3) { + reqBuilder = reqBuilder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + HttpRequest request = reqBuilder .GET() .version(version) .timeout(Duration.ofMillis(adjustTimeout(TIMEOUT_MILLIS))) .build(); try (HttpClient client = clientBuilder.build()) { - if (version.equals(HTTP_2)) - client.send(HttpRequest.newBuilder(h2WarmupUri).HEAD().build(), HttpResponse.BodyHandlers.discarding()); + Optional warmupUri = switch (version) { + case HTTP_1_1 -> Optional.empty(); + case HTTP_2 -> Optional.of(h2WarmupUri); + case HTTP_3 -> Optional.of(h3WarmupUri); + }; + if (warmupUri.isPresent()) { + HttpRequest head = reqBuilder.copy().uri(warmupUri.get()) + .version(version).HEAD().build(); + client.send(head, HttpResponse.BodyHandlers.discarding()); + } /* With TIMEOUT_MILLIS set to 1500ms and the server's RedirectHandler sleeping for 750ms before responding to each request, 4 iterations will take a guaranteed minimum time of 3000ms which will ensure that any @@ -135,7 +170,7 @@ public class RedirectTimeoutTest { } } - public static class Http2Warmup implements HttpTestHandler { + public static class HttpWarmup implements HttpTestHandler { @Override public void handle(HttpTestExchange t) throws IOException { diff --git a/test/jdk/java/net/httpclient/RedirectWithCookie.java b/test/jdk/java/net/httpclient/RedirectWithCookie.java index 13dfe766647..afc8618835b 100644 --- a/test/jdk/java/net/httpclient/RedirectWithCookie.java +++ b/test/jdk/java/net/httpclient/RedirectWithCookie.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -31,15 +31,10 @@ * RedirectWithCookie */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CookieManager; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; @@ -49,7 +44,6 @@ import java.net.http.HttpResponse.BodyHandlers; import java.util.List; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -58,6 +52,9 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -69,10 +66,12 @@ public class RedirectWithCookie implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String MESSAGE = "BasicRedirectTest message body"; static final int ITERATIONS = 3; @@ -80,6 +79,7 @@ public class RedirectWithCookie implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, }, { httpURI, }, { httpsURI, }, { http2URI, }, @@ -87,10 +87,22 @@ public class RedirectWithCookie implements HttpServerAdapters { }; } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "positive") void test(String uriString) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); - HttpClient client = HttpClient.newBuilder() + var builder = uriString.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .followRedirects(Redirect.ALWAYS) .cookieHandler(new CookieManager()) .sslContext(sslContext) @@ -98,7 +110,7 @@ public class RedirectWithCookie implements HttpServerAdapters { assert client.cookieHandler().isPresent(); URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); out.println("Initial request: " + request.uri()); for (int i=0; i< ITERATIONS; i++) { @@ -114,6 +126,8 @@ public class RedirectWithCookie implements HttpServerAdapters { assertTrue(response.uri().getPath().endsWith("message")); assertPreviousRedirectResponses(request, response); } + + client.close(); } static void assertPreviousRedirectResponses(HttpRequest initialRequest, @@ -164,10 +178,15 @@ public class RedirectWithCookie implements HttpServerAdapters { https2TestServer.addHandler(new CookieRedirectHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/redirect"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new CookieRedirectHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/redirect"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -176,6 +195,7 @@ public class RedirectWithCookie implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class CookieRedirectHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/ReferenceTracker.java b/test/jdk/java/net/httpclient/ReferenceTracker.java index d7e16d01201..6ea97ab6ac2 100644 --- a/test/jdk/java/net/httpclient/ReferenceTracker.java +++ b/test/jdk/java/net/httpclient/ReferenceTracker.java @@ -133,6 +133,15 @@ public class ReferenceTracker { "outstanding operations or unreleased resources", true); } + public AssertionError checkClosed(long graceDelayMs) { + Predicate hasOperations = (t) -> t.getOutstandingOperations() > 0; + Predicate hasSubscribers = (t) -> t.getOutstandingSubscribers() > 0; + return check(graceDelayMs, + hasOperations.or(hasSubscribers) + .or(Tracker::isSelectorAlive), + "outstanding operations or unreleased resources", true); + } + // This method is copied from ThreadInfo::toString, but removes the // limit on the stack trace depth (8 frames max) that ThreadInfo::toString // forcefully implement. We want to print all frames for better diagnosis. @@ -369,6 +378,7 @@ public class ReferenceTracker { warning.append("\n\tPending HTTP Requests: " + tracker.getOutstandingHttpRequests()); warning.append("\n\tPending HTTP/1.1 operations: " + tracker.getOutstandingHttpOperations()); warning.append("\n\tPending HTTP/2 streams: " + tracker.getOutstandingHttp2Streams()); + warning.append("\n\tPending HTTP/3 streams: " + tracker.getOutstandingHttp3Streams()); warning.append("\n\tPending WebSocket operations: " + tracker.getOutstandingWebSocketOperations()); warning.append("\n\tPending TCP connections: " + tracker.getOutstandingTcpConnections()); warning.append("\n\tPending Subscribers: " + tracker.getOutstandingSubscribers()); @@ -404,4 +414,23 @@ public class ReferenceTracker { "outstanding unclosed resources", true); return failed; } + + // This is a slightly more permissive check than the default checks, + // it only verifies that all CFs returned by send/sendAsync have been + // completed, and that all opened channels have been closed, and that + // the selector manager thread has exited. + // It doesn't check that all refcounts have reached 0. + // This is typically useful to only check that resources have been released. + public AssertionError checkShutdown(Tracker tracker, long graceDelayMs, boolean dumpThreads) { + Predicate isAlive = Tracker::isSelectorAlive; + Predicate hasPendingRequests = (t) -> t.getOutstandingHttpRequests() > 0; + Predicate hasPendingConnections = (t) -> t.getOutstandingTcpConnections() > 0; + Predicate hasPendingSubscribers = (t) -> t.getOutstandingSubscribers() > 0; + AssertionError failed = check(tracker, graceDelayMs, + isAlive.or(hasPendingRequests) + .or(hasPendingConnections) + .or(hasPendingSubscribers), + "outstanding unclosed resources", dumpThreads); + return failed; + } } diff --git a/test/jdk/java/net/httpclient/RequestBuilderTest.java b/test/jdk/java/net/httpclient/RequestBuilderTest.java index 991b6f9d2d9..97f4793d63c 100644 --- a/test/jdk/java/net/httpclient/RequestBuilderTest.java +++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, 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 @@ -30,19 +30,25 @@ import java.net.URI; import java.net.URISyntaxException; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; import java.util.List; import java.util.Map; import java.util.Set; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpRequest.newBuilder; import static java.time.Duration.ofNanos; import static java.time.Duration.ofMinutes; import static java.time.Duration.ofSeconds; import static java.time.Duration.ZERO; -import static java.net.http.HttpClient.Version.HTTP_1_1; -import static java.net.http.HttpClient.Version.HTTP_2; -import static java.net.http.HttpRequest.newBuilder; import static org.testng.Assert.*; import org.testng.annotations.Test; @@ -96,6 +102,8 @@ public class RequestBuilderTest { assertThrows(NPE, () -> builder.setHeader(null, null)); assertThrows(NPE, () -> builder.setHeader("name", null)); assertThrows(NPE, () -> builder.setHeader(null, "value")); + assertThrows(NPE, () -> builder.setOption(null, null)); + assertThrows(NPE, () -> builder.setOption((HttpOption) null, ANY)); assertThrows(NPE, () -> builder.timeout(null)); assertThrows(NPE, () -> builder.POST(null)); assertThrows(NPE, () -> builder.PUT(null)); @@ -402,6 +410,7 @@ public class RequestBuilderTest { .header("A", "B") .POST(BodyPublishers.ofString("")) .timeout(ofSeconds(30)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) .version(HTTP_1_1); HttpRequest.Builder copy = builder.copy(); assertTrue(builder != copy); @@ -418,6 +427,8 @@ public class RequestBuilderTest { assertEquals(copyRequest.timeout().get(), ofSeconds(30)); assertTrue(copyRequest.version().isPresent()); assertEquals(copyRequest.version().get(), HTTP_1_1); + assertTrue(copyRequest.getOption(H3_DISCOVERY).isPresent()); + assertEquals(copyRequest.getOption(H3_DISCOVERY).get(), HTTP_3_URI_ONLY); // lazy set URI ( maybe builder as a template ) copyRequest = newBuilder().copy().uri(uri).build(); diff --git a/test/jdk/java/net/httpclient/Response1xxTest.java b/test/jdk/java/net/httpclient/Response1xxTest.java index 57aed9407c2..d0148f0d684 100644 --- a/test/jdk/java/net/httpclient/Response1xxTest.java +++ b/test/jdk/java/net/httpclient/Response1xxTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, 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 @@ -30,22 +30,29 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpTimeoutException; import java.nio.charset.StandardCharsets; import java.time.Duration; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; + +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 java.net.http.HttpClient.Version.valueOf; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -55,7 +62,7 @@ import static java.net.http.HttpClient.Version.HTTP_2; * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.httpclient.test.lib.http2.Http2TestServer * @run testng/othervm -Djdk.internal.httpclient.debug=true - * * -Djdk.httpclient.HttpClient.log=headers,requests,responses,errors Response1xxTest + * -Djdk.httpclient.HttpClient.log=headers,requests,responses,errors Response1xxTest */ public class Response1xxTest implements HttpServerAdapters { private static final String EXPECTED_RSP_BODY = "Hello World"; @@ -73,6 +80,9 @@ public class Response1xxTest implements HttpServerAdapters { private HttpTestServer https2Server; // h2 private String https2RequestURIBase; + private HttpTestServer http3Server; // h3 + private String http3RequestURIBase; + private final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; @BeforeClass @@ -81,7 +91,7 @@ public class Response1xxTest implements HttpServerAdapters { server = new Http11Server(serverSocket); new Thread(server).start(); http1RequestURIBase = URIBuilder.newBuilder().scheme("http").loopback() - .port(serverSocket.getLocalPort()).build().toString(); + .port(serverSocket.getLocalPort()).path("/http1").build().toString(); http2Server = HttpTestServer.create(HTTP_2); http2Server.addHandler(new Http2Handler(), "/http2/102"); @@ -109,6 +119,19 @@ public class Response1xxTest implements HttpServerAdapters { https2Server.start(); System.out.println("Started (https) HTTP2 server at " + https2Server.getAddress()); + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3Server.addHandler(new Http3Handler(), "/http3/102"); + http3Server.addHandler(new Http3Handler(), "/http3/103"); + http3Server.addHandler(new Http3Handler(), "/http3/100"); + http3Server.addHandler(new Http3Handler(), "/http3/101"); + http3Server.addHandler(new OKHandler(), "/http3/200"); + http3Server.addHandler(new OnlyInformationalHandler(), "/http3/only-informational"); + http3RequestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(http3Server.getAddress().getPort()) + .path("/http3").build().toString(); + http3Server.start(); + System.out.println("Started (https) HTTP3 server at " + http3Server.getAddress()); + } @AfterClass @@ -132,6 +155,10 @@ public class Response1xxTest implements HttpServerAdapters { https2Server.stop(); System.out.println("Stopped (https) HTTP2 server"); } + if (http3Server != null) { + http3Server.stop(); + System.out.println("Stopped (https) HTTP3 server"); + } } } @@ -142,10 +169,11 @@ public class Response1xxTest implements HttpServerAdapters { "Content-Length: " + CONTENT_LENGTH + "\r\n\r\n" + EXPECTED_RSP_BODY; - private static final String REQ_LINE_FOO = "GET /test/foo HTTP/1.1\r\n"; - private static final String REQ_LINE_BAR = "GET /test/bar HTTP/1.1\r\n"; - private static final String REQ_LINE_HELLO = "GET /test/hello HTTP/1.1\r\n"; - private static final String REQ_LINE_BYE = "GET /test/bye HTTP/1.1\r\n"; + private static final String REQ_LINE_102 = "GET /http1/102 HTTP/1.1\r\n"; + private static final String REQ_LINE_103 = "GET /http1/103 HTTP/1.1\r\n"; + private static final String REQ_LINE_100 = "GET /http1/100 HTTP/1.1\r\n"; + private static final String REQ_LINE_101 = "GET /http1/101 HTTP/1.1\r\n"; + private static final String REQ_LINE_ONLY_INFO = "GET /http1/only-informational HTTP/1.1\r\n"; private final ServerSocket serverSocket; @@ -160,6 +188,7 @@ public class Response1xxTest implements HttpServerAdapters { System.out.println("Server running at " + serverSocket); while (!stop) { Socket socket = null; + boolean onlyInfo = false; try { // accept a connection socket = serverSocket.accept(); @@ -180,18 +209,22 @@ public class Response1xxTest implements HttpServerAdapters { System.out.println("Received following request line from client " + socket + " :\n" + requestLine); final int informationalResponseCode; - if (requestLine.startsWith(REQ_LINE_FOO)) { + if (requestLine.startsWith(REQ_LINE_102)) { // we will send intermediate/informational 102 response informationalResponseCode = 102; - } else if (requestLine.startsWith(REQ_LINE_BAR)) { + } else if (requestLine.startsWith(REQ_LINE_103)) { // we will send intermediate/informational 103 response informationalResponseCode = 103; - } else if (requestLine.startsWith(REQ_LINE_HELLO)) { + } else if (requestLine.startsWith(REQ_LINE_100)) { // we will send intermediate/informational 100 response informationalResponseCode = 100; - } else if (requestLine.startsWith(REQ_LINE_BYE)) { + } else if (requestLine.startsWith(REQ_LINE_101)) { // we will send intermediate/informational 101 response informationalResponseCode = 101; + } else if (requestLine.startsWith(REQ_LINE_ONLY_INFO)) { + // we will send intermediate/informational 102 response + informationalResponseCode = 102; + onlyInfo = true; } else { // unexpected client. ignore and close the client System.err.println("Ignoring unexpected request from client " + socket); @@ -215,6 +248,10 @@ public class Response1xxTest implements HttpServerAdapters { os.flush(); System.out.println("Sent response code " + informationalResponseCode + " to client " + socket); + if (onlyInfo) { + Thread.sleep(2000); + i = 1; + } } // now send a final response System.out.println("Now sending 200 response code to client " + socket); @@ -226,8 +263,10 @@ public class Response1xxTest implements HttpServerAdapters { // close the client connection safeClose(socket); // continue accepting any other client connections until we are asked to stop - System.err.println("Ignoring exception in server:"); - t.printStackTrace(); + if (!onlyInfo) { + System.err.println("Ignoring exception in server:"); + t.printStackTrace(); + } } } } @@ -261,6 +300,8 @@ public class Response1xxTest implements HttpServerAdapters { @Override public void handle(final HttpTestExchange exchange) throws IOException { final URI requestURI = exchange.getRequestURI(); + final Version version = exchange.getServerVersion(); + final int informationResponseCode; if (requestURI.getPath().endsWith("/102")) { informationResponseCode = 102; @@ -281,25 +322,29 @@ public class Response1xxTest implements HttpServerAdapters { // be sent multiple times) for (int i = 0; i < 3; i++) { exchange.sendResponseHeaders(informationResponseCode, -1); - System.out.println("Sent " + informationResponseCode + " response code from H2 server"); + System.out.println("Sent " + informationResponseCode + " response code from " + version + " server"); } // now send 200 response try { final byte[] body = EXPECTED_RSP_BODY.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, body.length); - System.out.println("Sent 200 response from H2 server"); + System.out.println("Sent 200 response from " + version + " server"); try (OutputStream os = exchange.getResponseBody()) { os.write(body); } - System.out.println("Sent response body from H2 server"); + System.out.println("Sent response body from " + version + " server"); } catch (Throwable e) { - System.err.println("Failed to send response from HTTP2 handler:"); + System.err.println("Failed to send response from " + version + " handler:"); e.printStackTrace(); throw e; } } } + private static final class Http3Handler extends Http2Handler { + + } + private static class OnlyInformationalHandler implements HttpTestHandler { @Override @@ -338,20 +383,7 @@ public class Response1xxTest implements HttpServerAdapters { .version(HttpClient.Version.HTTP_1_1) .proxy(HttpClient.Builder.NO_PROXY).build(); TRACKER.track(client); - final URI[] requestURIs = new URI[]{ - new URI(http1RequestURIBase + "/test/foo"), - new URI(http1RequestURIBase + "/test/bar"), - new URI(http1RequestURIBase + "/test/hello")}; - for (final URI requestURI : requestURIs) { - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); - System.out.println("Issuing request to " + requestURI); - final HttpResponse response = client.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - Assert.assertEquals(response.version(), HttpClient.Version.HTTP_1_1, - "Unexpected HTTP version in response"); - Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); - Assert.assertEquals(response.body(), EXPECTED_RSP_BODY, "Unexpected response body"); - } + test1xxFor(client, HTTP_1_1, http1RequestURIBase); } /** @@ -364,23 +396,58 @@ public class Response1xxTest implements HttpServerAdapters { final HttpClient client = HttpClient.newBuilder() .version(HTTP_2) .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxFor(client, HTTP_2, http2RequestURIBase); + } + + /** + * Tests that when a HTTP3 server sends intermediate 1xx response codes and then the final + * response, the client (internally) will ignore those intermediate informational response codes + * and only return the final response to the application + */ + @Test + public void test1xxForHTTP3() throws Exception { + final HttpClient client = newClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxFor(client, HTTP_3, http3RequestURIBase); + } + + private void test1xxFor(HttpClient client, Version version, String baseURI) throws Exception { TRACKER.track(client); final URI[] requestURIs = new URI[]{ - new URI(http2RequestURIBase + "/102"), - new URI(http2RequestURIBase + "/103"), - new URI(http2RequestURIBase + "/100")}; + new URI(baseURI + "/102"), + new URI(baseURI + "/103"), + new URI(baseURI + "/100")}; + var requestBuilder = HttpRequest.newBuilder(); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } for (final URI requestURI : requestURIs) { - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); + final HttpRequest request = requestBuilder.copy().uri(requestURI).build(); System.out.println("Issuing request to " + requestURI); final HttpResponse response = client.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - Assert.assertEquals(response.version(), HTTP_2, + BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.version(), version, "Unexpected HTTP version in response"); Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); Assert.assertEquals(response.body(), EXPECTED_RSP_BODY, "Unexpected response body"); } } + /** + * Tests that when a request is issued with a specific request timeout and the server + * responds with intermediate 1xx response code but doesn't respond with a final response within + * the timeout duration, then the application fails with a request timeout + */ + @Test + public void test1xxRequestTimeoutH1() throws Exception { + final HttpClient client = HttpClient.newBuilder() + .version(HTTP_1_1) + .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxRequestTimeout(client, HTTP_1_1, http1RequestURIBase); + } + /** * Tests that when a request is issued with a specific request timeout and the server @@ -388,22 +455,45 @@ public class Response1xxTest implements HttpServerAdapters { * the timeout duration, then the application fails with a request timeout */ @Test - public void test1xxRequestTimeout() throws Exception { + public void test1xxRequestTimeoutH2() throws Exception { final HttpClient client = HttpClient.newBuilder() .version(HTTP_2) .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxRequestTimeout(client, HTTP_2, http2RequestURIBase); + } + + /** + * Tests that when a request is issued with a specific request timeout and the server + * responds with intermediate 1xx response code but doesn't respond with a final response within + * the timeout duration, then the application fails with a request timeout + */ + @Test + public void test1xxRequestTimeoutH3() throws Exception { + final HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxRequestTimeout(client, HTTP_3, http3RequestURIBase); + } + + private void test1xxRequestTimeout(HttpClient client, Version version, String uriBase) throws Exception { TRACKER.track(client); - final URI requestURI = new URI(http2RequestURIBase + "/only-informational"); + final URI requestURI = new URI(uriBase + "/only-informational"); final Duration requestTimeout = Duration.ofSeconds(2); - final HttpRequest request = HttpRequest.newBuilder(requestURI).timeout(requestTimeout) + var requestBuilder = HttpRequest.newBuilder(requestURI); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + final HttpRequest request = requestBuilder.timeout(requestTimeout) .build(); System.out.println("Issuing request to " + requestURI); // we expect the request to timeout Assert.assertThrows(HttpTimeoutException.class, () -> { - client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + client.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); }); } + /** * Tests that when the HTTP/1.1 server sends a 101 response when the request hasn't asked * for an "Upgrade" then the request fails. @@ -413,13 +503,7 @@ public class Response1xxTest implements HttpServerAdapters { final HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .proxy(HttpClient.Builder.NO_PROXY).build(); - TRACKER.track(client); - final URI requestURI = new URI(http1RequestURIBase + "/test/bye"); - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); - System.out.println("Issuing request to " + requestURI); - // we expect the request to fail because the server sent an unexpected 101 - Assert.assertThrows(ProtocolException.class, - () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); + testUnexpected101(client, HTTP_1_1, http1RequestURIBase); } @@ -433,13 +517,20 @@ public class Response1xxTest implements HttpServerAdapters { .version(HTTP_2) .sslContext(sslContext) .proxy(HttpClient.Builder.NO_PROXY).build(); - TRACKER.track(client); - final URI requestURI = new URI(https2RequestURIBase + "/101"); - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); - System.out.println("Issuing request to " + requestURI); - // we expect the request to fail because the server sent an unexpected 101 - Assert.assertThrows(ProtocolException.class, - () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); + testUnexpected101(client, HTTP_2, https2RequestURIBase); + } + + /** + * Tests that when the HTTP2 server (over HTTPS) sends a 101 response when the request + * hasn't asked for an "Upgrade" then the request fails. + */ + @Test + public void testHTT3Unexpected101() throws Exception { + final HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY).build(); + testUnexpected101(client, HTTP_3, http3RequestURIBase); } /** @@ -451,7 +542,6 @@ public class Response1xxTest implements HttpServerAdapters { final HttpClient client = HttpClient.newBuilder() .version(HTTP_2) .proxy(HttpClient.Builder.NO_PROXY).build(); - TRACKER.track(client); // when using HTTP2 version against a "http://" (non-secure) URI // the HTTP client (implementation) internally initiates a HTTP/1.1 connection // and then does an "Upgrade:" to "h2c". This it does when there isn't already a @@ -462,12 +552,21 @@ public class Response1xxTest implements HttpServerAdapters { // start our testing warmupH2Client(client); // start the actual testing - final URI requestURI = new URI(http2RequestURIBase + "/101"); - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); + testUnexpected101(client, HTTP_2, http2RequestURIBase); + } + + private void testUnexpected101(HttpClient client, Version version, String baseUri) throws Exception { + TRACKER.track(client); + final URI requestURI = new URI(baseUri + "/101"); + var requestBuilder = HttpRequest.newBuilder(requestURI); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + final HttpRequest request = requestBuilder.build(); System.out.println("Issuing request to " + requestURI); // we expect the request to fail because the server sent an unexpected 101 Assert.assertThrows(ProtocolException.class, - () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); + () -> client.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8))); } // sends a request and expects a 200 response back diff --git a/test/jdk/java/net/httpclient/Response204V2Test.java b/test/jdk/java/net/httpclient/Response204V2Test.java index 610c312b667..9f867d0fb17 100644 --- a/test/jdk/java/net/httpclient/Response204V2Test.java +++ b/test/jdk/java/net/httpclient/Response204V2Test.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, 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 @@ -54,7 +54,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; @@ -71,14 +70,19 @@ import javax.net.ssl.SSLContext; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; public class Response204V2Test implements HttpServerAdapters { SSLContext sslContext; HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http2URI; String https2URI; + String http3URI; static final int RESPONSE_CODE = 204; static final int ITERATION_COUNT = 4; @@ -177,6 +181,7 @@ public class Response204V2Test implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI, http2URI, https2URI, }; @@ -201,9 +206,9 @@ public class Response204V2Test implements HttpServerAdapters { return result; } - private HttpClient makeNewClient() { + private HttpClient makeNewClient(HttpClient.Builder builder) { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -211,14 +216,17 @@ public class Response204V2Test implements HttpServerAdapters { return TRACKER.track(client); } - HttpClient newHttpClient(boolean share) { - if (!share) return makeNewClient(); + HttpClient newHttpClient(String uri, boolean share) { + if (!share) return makeNewClient(newClientBuilderForH3()); HttpClient shared = sharedClient; if (shared != null) return shared; synchronized (this) { shared = sharedClient; if (shared == null) { - shared = sharedClient = makeNewClient(); + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + shared = sharedClient = makeNewClient(builder); } return shared; } @@ -241,25 +249,37 @@ public class Response204V2Test implements HttpServerAdapters { } } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } @Test(dataProvider = "variants") public void test(String uri, boolean sameClient) throws Exception { checkSkip(); - System.out.println("Request to " + uri); + out.println("Request to " + uri); - HttpClient client = newHttpClient(sameClient); + HttpClient client = newHttpClient(uri, sameClient); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest request = newRequestBuilder(URI.create(uri)) .GET() .build(); for (int i = 0; i < ITERATION_COUNT; i++) { - System.out.println("Iteration: " + i); + out.println("Iteration: " + i); HttpResponse response = client.send(request, BodyHandlers.ofString()); int expectedResponse = RESPONSE_CODE; if (response.statusCode() != expectedResponse) - throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode())); + throw new RuntimeException("wrong response code " + response.statusCode()); } - System.out.println("test: DONE"); + if (!sameClient) { + out.println("test: closing test client"); + client.close(); + } + out.println("test: DONE"); } @BeforeTest @@ -279,9 +299,14 @@ public class Response204V2Test implements HttpServerAdapters { https2TestServer.addHandler(handler204, "/https2/test204/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test204/x"; - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(handler204, "/http3/test204/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/test204/x"; + + serverCount.addAndGet(3); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -294,6 +319,7 @@ public class Response204V2Test implements HttpServerAdapters { try { http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java b/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java index beed8f86fa6..04ce3d531af 100644 --- a/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java +++ b/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 diff --git a/test/jdk/java/net/httpclient/ResponsePublisher.java b/test/jdk/java/net/httpclient/ResponsePublisher.java index 3d6cf601b2f..5d90e20c626 100644 --- a/test/jdk/java/net/httpclient/ResponsePublisher.java +++ b/test/jdk/java/net/httpclient/ResponsePublisher.java @@ -31,11 +31,7 @@ * @run testng/othervm/timeout=480 ResponsePublisher */ -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.internal.net.http.common.OperationTrackers; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -48,10 +44,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; @@ -71,11 +65,13 @@ import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -84,10 +80,11 @@ import static org.testng.Assert.assertTrue; public class ResponsePublisher implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -96,6 +93,8 @@ public class ResponsePublisher implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -143,6 +142,16 @@ public class ResponsePublisher implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ + { http3URI_fixed, false, OF_PUBLISHER_API }, + { http3URI_chunk, false, OF_PUBLISHER_API }, + { http3URI_fixed, true, OF_PUBLISHER_API }, + { http3URI_chunk, true, OF_PUBLISHER_API }, + + { http3URI_fixed, false, OF_PUBLISHER_TEST }, + { http3URI_chunk, false, OF_PUBLISHER_TEST }, + { http3URI_fixed, true, OF_PUBLISHER_TEST }, + { http3URI_chunk, true, OF_PUBLISHER_TEST }, + { httpURI_fixed, false, OF_PUBLISHER_API }, { httpURI_chunk, false, OF_PUBLISHER_API }, { httpsURI_fixed, false, OF_PUBLISHER_API }, @@ -182,21 +191,33 @@ public class ResponsePublisher implements HttpServerAdapters { } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; - HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return TRACKER.track(builder .executor(executor) .sslContext(sslContext) .build()); } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void testExceptions(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -241,9 +262,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -270,9 +291,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -302,9 +323,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -331,9 +352,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -470,10 +491,21 @@ public class ResponsePublisher implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_VariableLengthHandler(); + + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -485,6 +517,7 @@ public class ResponsePublisher implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/RestrictedHeadersTest.java b/test/jdk/java/net/httpclient/RestrictedHeadersTest.java index 7e5881d3cee..f2ddf85b3f5 100644 --- a/test/jdk/java/net/httpclient/RestrictedHeadersTest.java +++ b/test/jdk/java/net/httpclient/RestrictedHeadersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -47,7 +47,7 @@ public class RestrictedHeadersTest { // This list must be same as impl static Set defaultRestrictedHeaders = - Set.of("connection", "content-length", "expect", "host", "upgrade"); + Set.of("connection", "content-length", "expect", "host", "upgrade", "alt-used"); private static void runDefaultTest() { System.out.println("DEFAULT TEST: no property set"); diff --git a/test/jdk/java/net/httpclient/RetryWithCookie.java b/test/jdk/java/net/httpclient/RetryWithCookie.java index ec83fa5df89..dc046031bd3 100644 --- a/test/jdk/java/net/httpclient/RetryWithCookie.java +++ b/test/jdk/java/net/httpclient/RetryWithCookie.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -34,9 +34,6 @@ * RetryWithCookie */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -48,8 +45,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CookieManager; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; @@ -63,12 +58,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -76,14 +73,16 @@ import static org.testng.Assert.assertTrue; public class RetryWithCookie implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String MESSAGE = "BasicRedirectTest message body"; static final int ITERATIONS = 3; @@ -91,6 +90,7 @@ public class RetryWithCookie implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, }, { httpURI, }, { httpsURI, }, { http2URI, }, @@ -101,11 +101,23 @@ public class RetryWithCookie implements HttpServerAdapters { static final AtomicLong requestCounter = new AtomicLong(); final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "positive") void test(String uriString) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); CookieManager cookieManager = new CookieManager(); - HttpClient client = HttpClient.newBuilder() + var builder = uriString.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) .cookieHandler(cookieManager) @@ -121,7 +133,7 @@ public class RetryWithCookie implements HttpServerAdapters { cookieHeaders.put("Set-Cookie", cookies); cookieManager.put(uri, cookieHeaders); - HttpRequest request = HttpRequest.newBuilder(uri) + HttpRequest request = newRequestBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) .build(); out.println("Initial request: " + request.uri()); @@ -136,7 +148,7 @@ public class RetryWithCookie implements HttpServerAdapters { assertEquals(response.statusCode(), 200); assertEquals(response.body(), MESSAGE); assertEquals(response.headers().allValues("X-Request-Cookie"), cookies); - request = HttpRequest.newBuilder(uri) + request = newRequestBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) .build(); } @@ -164,10 +176,15 @@ public class RetryWithCookie implements HttpServerAdapters { https2TestServer.addHandler(new CookieRetryHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new CookieRetryHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -179,6 +196,7 @@ public class RetryWithCookie implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/ShutdownNow.java b/test/jdk/java/net/httpclient/ShutdownNow.java index 045876597a2..a5850b4af61 100644 --- a/test/jdk/java/net/httpclient/ShutdownNow.java +++ b/test/jdk/java/net/httpclient/ShutdownNow.java @@ -45,7 +45,9 @@ import java.io.OutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -73,6 +75,10 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -90,10 +96,15 @@ public class ShutdownNow implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "ShutdownNow message body"; static final int ITERATIONS = 3; @@ -101,10 +112,12 @@ public class ShutdownNow implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -118,6 +131,15 @@ public class ShutdownNow implements HttpServerAdapters { return t; } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + static boolean hasExpectedMessage(IOException io) { String message = io.getMessage(); if (message == null) return false; @@ -155,22 +177,28 @@ public class ShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting concurrent (%s, %s, %s) ----%n%n", uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); int step = RANDOM.nextInt(ITERATIONS); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + List>> responses = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; @@ -216,11 +244,13 @@ public class ShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting sequential (%s, %s, %s) ----%n%n", + uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -228,10 +258,15 @@ public class ShutdownNow implements HttpServerAdapters { int step = RANDOM.nextInt(ITERATIONS); out.printf("will shutdown client in step %d%n", step); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; @@ -304,10 +339,21 @@ public class ShutdownNow implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -319,6 +365,8 @@ public class ShutdownNow implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/SmokeTest.java b/test/jdk/java/net/httpclient/SmokeTest.java index 56294e8f02f..a10afef562f 100644 --- a/test/jdk/java/net/httpclient/SmokeTest.java +++ b/test/jdk/java/net/httpclient/SmokeTest.java @@ -24,11 +24,12 @@ /* * @test * @bug 8087112 8178699 8338569 - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib / + * @library /test/lib /test/jdk/java/net/httpclient/lib / * @build jdk.test.lib.net.SimpleSSLContext ProxyServer + * jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java * @run main/othervm @@ -92,6 +93,8 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Random; + +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.net.SimpleSSLContext; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; @@ -808,7 +811,7 @@ public class SmokeTest { ctx = new SimpleSSLContext().get(); sslparams = ctx.getDefaultSSLParameters(); //sslparams.setProtocols(new String[]{"TLSv1.2"}); - s2.setHttpsConfigurator(new Configurator(ctx)); + s2.setHttpsConfigurator(new Configurator(addr.getAddress(), ctx)); s1.start(); s2.start(); @@ -935,14 +938,19 @@ public class SmokeTest { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } - public void configure (HttpsParameters params) { - SSLParameters p = getSSLContext().getDefaultSSLParameters(); + @Override + public void configure(final HttpsParameters params) { + final SSLParameters p = getSSLContext().getDefaultSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, p); //p.setProtocols(new String[]{"TLSv1.2"}); - params.setSSLParameters (p); + params.setSSLParameters(p); } } diff --git a/test/jdk/java/net/httpclient/SpecialHeadersTest.java b/test/jdk/java/net/httpclient/SpecialHeadersTest.java index d3e1f37b592..9ad3114da2a 100644 --- a/test/jdk/java/net/httpclient/SpecialHeadersTest.java +++ b/test/jdk/java/net/httpclient/SpecialHeadersTest.java @@ -92,6 +92,7 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; 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 java.nio.charset.StandardCharsets.US_ASCII; import org.testng.Assert; import static org.testng.Assert.assertEquals; @@ -347,7 +348,7 @@ public class SpecialHeadersTest implements HttpServerAdapters { boolean isInitialRequest = i == 0; boolean isSecure = uri.getScheme().equalsIgnoreCase("https"); - boolean isHTTP2 = resp.version() == HTTP_2; + boolean isHTTP1 = resp.version() == HTTP_1_1; boolean isNotH2CUpgrade = isSecure || (sameClient == true && !isInitialRequest); boolean isDefaultHostHeader = name.equalsIgnoreCase("host") && useDefault; @@ -356,13 +357,13 @@ public class SpecialHeadersTest implements HttpServerAdapters { // header in the response, except the response to the h2c Upgrade // request which will have been sent through HTTP/1.1. - if (isDefaultHostHeader && isHTTP2 && isNotH2CUpgrade) { + if (isDefaultHostHeader && !isHTTP1 && isNotH2CUpgrade) { assertTrue(resp.headers().firstValue("X-" + key).isEmpty()); assertTrue(resp.headers().allValues("X-" + key).isEmpty()); out.println("No X-" + key + " header received, as expected"); } else { String receivedHeaderString = value == null ? null - : resp.headers().firstValue("X-" + key).orElse(null); + : resp.headers().firstValue("X-" + key).get(); out.println("Got X-" + key + ": " + resp.headers().allValues("X-" + key)); if (value != null) { assertEquals(receivedHeaderString, value); @@ -512,7 +513,7 @@ public class SpecialHeadersTest implements HttpServerAdapters { // header in the response, except the response to the h2c Upgrade // request which will have been sent through HTTP/1.1. - if (isDefaultHostHeader && resp.version() == HTTP_2 && isNotH2CUpgrade) { + if (isDefaultHostHeader && resp.version() != HTTP_1_1 && isNotH2CUpgrade) { assertTrue(resp.headers().firstValue("X-" + key).isEmpty()); assertTrue(resp.headers().allValues("X-" + key).isEmpty()); out.println("No X-" + key + " header received, as expected"); diff --git a/test/jdk/java/net/httpclient/SplitResponse.java b/test/jdk/java/net/httpclient/SplitResponse.java index ad04e8a1627..9213901df4b 100644 --- a/test/jdk/java/net/httpclient/SplitResponse.java +++ b/test/jdk/java/net/httpclient/SplitResponse.java @@ -24,11 +24,8 @@ import java.io.IOException; import java.net.SocketException; import java.net.URI; -import java.util.ArrayList; import java.util.EnumSet; -import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.List; import java.util.concurrent.CompletableFuture; import javax.net.ssl.SSLContext; import javax.net.ServerSocketFactory; @@ -43,6 +40,8 @@ import java.util.stream.Stream; import jdk.test.lib.net.SimpleSSLContext; import static java.lang.System.out; import static java.lang.String.format; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.net.http.HttpResponse.BodyHandlers.ofString; @@ -212,7 +211,10 @@ public class SplitResponse { HttpClient client = newHttpClient(); - HttpRequest request = HttpRequest.newBuilder(uri).version(version).build(); + HttpRequest request = HttpRequest.newBuilder(uri) + .version(version) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); HttpResponse r; CompletableFuture> cf1; diff --git a/test/jdk/java/net/httpclient/StreamCloseTest.java b/test/jdk/java/net/httpclient/StreamCloseTest.java index 2d00a539e80..fcb00188581 100644 --- a/test/jdk/java/net/httpclient/StreamCloseTest.java +++ b/test/jdk/java/net/httpclient/StreamCloseTest.java @@ -27,13 +27,12 @@ * @test * @bug 8257736 * @library /test/jdk/java/net/httpclient/lib + * @library /test/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.httpclient.test.lib.http2.Http2TestServer * @run testng/othervm StreamCloseTest */ -import com.sun.net.httpserver.HttpServer; - import java.io.InputStream; import java.io.IOException; import java.net.http.HttpClient; @@ -42,11 +41,8 @@ import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; diff --git a/test/jdk/java/net/httpclient/StreamingBody.java b/test/jdk/java/net/httpclient/StreamingBody.java index 7943968d239..bff6a2b39c4 100644 --- a/test/jdk/java/net/httpclient/StreamingBody.java +++ b/test/jdk/java/net/httpclient/StreamingBody.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, 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 @@ -32,14 +32,9 @@ * StreamingBody */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -47,7 +42,6 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -56,6 +50,9 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -67,10 +64,12 @@ public class StreamingBody implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String MESSAGE = "StreamingBody message body"; static final int ITERATIONS = 100; @@ -78,6 +77,7 @@ public class StreamingBody implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, }, { httpURI, }, { httpsURI, }, { http2URI, }, @@ -85,15 +85,27 @@ public class StreamingBody implements HttpServerAdapters { }; } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "positive") void test(String uriString) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); for (int i=0; i< ITERATIONS; i++) { out.println("iteration: " + i); - HttpResponse response = HttpClient.newBuilder() + var builder = uriString.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpResponse response = builder .sslContext(sslContext) .proxy(NO_PROXY) .build() @@ -129,14 +141,20 @@ public class StreamingBody implements HttpServerAdapters { http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(new MessageHandler(), "/http2/streamingbody/"); http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/streamingbody/y"; + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(new MessageHandler(), "/https2/streamingbody/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/streamingbody/z"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new MessageHandler(), "/http3/streamingbody/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/streamingbody/z"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -145,6 +163,7 @@ public class StreamingBody implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class MessageHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/TEST.properties b/test/jdk/java/net/httpclient/TEST.properties index b4ca833b5e5..b79f7113952 100644 --- a/test/jdk/java/net/httpclient/TEST.properties +++ b/test/jdk/java/net/httpclient/TEST.properties @@ -1,9 +1,22 @@ -modules=java.base/sun.net.www.http \ +modules=java.base/jdk.internal.util \ + java.base/sun.net.www.http \ java.base/sun.net.www \ java.base/sun.net \ + java.net.http/jdk.internal.net.http \ java.net.http/jdk.internal.net.http.common \ java.net.http/jdk.internal.net.http.frame \ java.net.http/jdk.internal.net.http.hpack \ + java.base/jdk.internal.net.quic \ + java.net.http/jdk.internal.net.http.quic \ + java.net.http/jdk.internal.net.http.quic.packets \ + java.net.http/jdk.internal.net.http.quic.frames \ + java.net.http/jdk.internal.net.http.quic.streams \ + java.net.http/jdk.internal.net.http.http3.streams \ + java.net.http/jdk.internal.net.http.http3.frames \ + java.net.http/jdk.internal.net.http.http3 \ + java.net.http/jdk.internal.net.http.qpack \ + java.net.http/jdk.internal.net.http.qpack.readers \ + java.net.http/jdk.internal.net.http.qpack.writers \ + java.security.jgss \ java.logging \ jdk.httpserver -maxOutputSize = 2500000 diff --git a/test/jdk/java/net/httpclient/TimeoutBasic.java b/test/jdk/java/net/httpclient/TimeoutBasic.java index cdaedf06219..47210209408 100644 --- a/test/jdk/java/net/httpclient/TimeoutBasic.java +++ b/test/jdk/java/net/httpclient/TimeoutBasic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, 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 @@ -32,10 +32,13 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpTimeoutException; import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; import javax.net.ServerSocketFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLServerSocketFactory; +import java.nio.channels.DatagramChannel; import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -43,6 +46,13 @@ import java.util.concurrent.CompletionException; import java.util.function.Function; import static java.lang.System.out; +import static java.net.StandardSocketOptions.SO_REUSEADDR; +import static java.net.StandardSocketOptions.SO_REUSEPORT; +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 java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -67,7 +77,7 @@ public class TimeoutBasic { null); static final List VERSIONS = - Arrays.asList(HttpClient.Version.HTTP_2, HttpClient.Version.HTTP_1_1, null); + Arrays.asList(HTTP_2, HTTP_1_1, HTTP_3, null); static final List SCHEMES = List.of("https", "http"); @@ -81,7 +91,7 @@ public class TimeoutBasic { public static void main(String[] args) throws Exception { for (Function m : METHODS) { - for (HttpClient.Version version : List.of(HttpClient.Version.HTTP_1_1)) { + for (HttpClient.Version version : List.of(HTTP_1_1)) { for (HttpClient.Version reqVersion : VERSIONS) { for (String scheme : SCHEMES) { ServerSocketFactory ssf; @@ -141,11 +151,46 @@ public class TimeoutBasic { if (version != null) builder.version(version); HttpClient client = builder.build(); out.printf("%ntest(version=%s, reqVersion=%s, scheme=%s)%n", version, reqVersion, scheme); + DatagramChannel dc = null; try (ServerSocket ss = ssf.createServerSocket()) { ss.setReuseAddress(false); ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); int port = ss.getLocalPort(); - URI uri = new URI(scheme +"://localhost:" + port + "/"); + boolean useAltSvc = false; + if (reqVersion == HTTP_3 && "https".equalsIgnoreCase(scheme)) { + // Prevent the client to connecting to any random server + // opened by other tests on the machine, by opening a + // datagram channel on the same port than the server socket + dc = DatagramChannel.open(); + try { + if (dc.supportedOptions().contains(SO_REUSEADDR)) { + dc.setOption(SO_REUSEADDR, false); + } + if (dc.supportedOptions().contains(SO_REUSEPORT)) { + dc.setOption(SO_REUSEPORT, false); + } + dc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); + } catch (IOException io) { + // failed to bind - presumably the port was already taken + // we will configure the request to use ALT_SVC instead, which + // means no HTTP/3 connection will be attempted + useAltSvc = true; + // cleanup channel + dc.close(); + dc = null; + out.println("HTTP/3 direct connection cannot be tested: " + io); + } + } + + // can only reach here if dc port == port + assert dc == null || ((InetSocketAddress)dc.getLocalAddress()).getPort() == port; + + URI uri = URIBuilder.newBuilder() + .scheme(scheme) + .loopback() + .port(port) + .path("/") + .build(); out.println("--- TESTING Async"); int count = 0; @@ -153,6 +198,13 @@ public class TimeoutBasic { out.println(" with duration of " + duration); HttpRequest request = newRequest(uri, duration, reqVersion, method); if (request == null) continue; + if (useAltSvc) { + // make sure request will be downgraded to HTTP/2 if we + // have not been able to create `dc`. + request = HttpRequest.newBuilder(request, (n,v) -> true) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + } count++; try { HttpResponse resp = client.sendAsync(request, BodyHandlers.discarding()).join(); @@ -163,11 +215,17 @@ public class TimeoutBasic { out.println("Body (should be null): " + resp.body()); throw new RuntimeException("Unexpected response: " + resp.statusCode()); } catch (CompletionException e) { - if (!(e.getCause() instanceof HttpTimeoutException)) { + Throwable x = e; + if (x.getCause() instanceof SSLHandshakeException s) { + if (s.getCause() instanceof HttpTimeoutException) { + x = s; + } + } + if (!(x.getCause() instanceof HttpTimeoutException)) { e.printStackTrace(out); throw new RuntimeException("Unexpected exception: " + e.getCause()); } else { - out.println("Caught expected timeout: " + e.getCause()); + out.println("Caught expected timeout: " + x.getCause()); } } } @@ -179,15 +237,25 @@ public class TimeoutBasic { out.println(" with duration of " + duration); HttpRequest request = newRequest(uri, duration, reqVersion, method); if (request == null) continue; + if (useAltSvc) { + // make sure request will be downgraded to HTTP/2 if we + // have not been able to create `dc`. + request = HttpRequest.newBuilder(request, (n,v) -> true) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + } count++; try { - client.send(request, BodyHandlers.discarding()); + HttpResponse resp = client.send(request, BodyHandlers.discarding()); + throw new RuntimeException("Unexpected response: " + resp.statusCode()); } catch (HttpTimeoutException e) { out.println("Caught expected timeout: " + e); } } assert count >= TIMEOUTS.size() -1; + } finally { + if (dc != null) dc.close(); } } } diff --git a/test/jdk/java/net/httpclient/TlsContextTest.java b/test/jdk/java/net/httpclient/TlsContextTest.java index 707a2e2d7c7..f24007f6d90 100644 --- a/test/jdk/java/net/httpclient/TlsContextTest.java +++ b/test/jdk/java/net/httpclient/TlsContextTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, 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 @@ -42,6 +42,7 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Version; 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 java.net.http.HttpResponse.BodyHandlers.ofString; import static org.testng.Assert.assertEquals; import jdk.test.lib.security.SecurityUtils; @@ -75,7 +76,8 @@ public class TlsContextTest implements HttpServerAdapters { server = SimpleSSLContext.getContext("TLS"); final ExecutorService executor = Executors.newCachedThreadPool(); https2Server = HttpTestServer.of( - new Http2TestServer("localhost", true, 0, executor, 50, null, server, true)); + new Http2TestServer("localhost", true, 0, executor, 50, null, server, true) + .enableH3AltServiceOnSamePort()); https2Server.addHandler(new TlsVersionTestHandler("https", https2Server), "/server/"); https2Server.start(); @@ -89,6 +91,9 @@ public class TlsContextTest implements HttpServerAdapters { { SimpleSSLContext.getContext("TLSv1.2"), HTTP_2, "TLSv1.2" }, { SimpleSSLContext.getContext("TLSv1.1"), HTTP_1_1, "TLSv1.1" }, { SimpleSSLContext.getContext("TLSv1.1"), HTTP_2, "TLSv1.1" }, + { SimpleSSLContext.getContext("TLSv1.3"), HTTP_3, "TLSv1.3" }, + { SimpleSSLContext.getContext("TLSv1.2"), HTTP_3, "TLSv1.2" }, + { SimpleSSLContext.getContext("TLSv1.1"), HTTP_3, "TLSv1.1" }, }; } @@ -99,21 +104,32 @@ public class TlsContextTest implements HttpServerAdapters { public void testVersionProtocols(SSLContext context, Version version, String expectedProtocol) throws Exception { - HttpClient client = HttpClient.newBuilder() - .sslContext(context) - .version(version) + // for HTTP/3 we won't accept to set the version to HTTP/3 on the + // client if we don't have TLSv1.3; We will set the version + // on the request instead in that case. + var builder = version == HTTP_3 ? newClientBuilderForH3() + : HttpClient.newBuilder().version(version); + var reqBuilder = HttpRequest.newBuilder(new URI(https2URI)); + + HttpClient client = builder.sslContext(context) .build(); - HttpRequest request = HttpRequest.newBuilder(new URI(https2URI)) - .GET() - .build(); + if (version == HTTP_3) { + // warmup to obtain AltService + client.send(reqBuilder.version(HTTP_2).GET().build(), ofString()); + reqBuilder = reqBuilder.version(HTTP_3); + } + + HttpRequest request = reqBuilder.GET().build(); for (int i = 0; i < ITERATIONS; i++) { HttpResponse response = client.send(request, ofString()); - testAllProtocols(response, expectedProtocol); + testAllProtocols(response, expectedProtocol, version); } + client.close(); } private void testAllProtocols(HttpResponse response, - String expectedProtocol) throws Exception { + String expectedProtocol, + Version clientVersion) throws Exception { String protocol = response.sslSession().get().getProtocol(); int statusCode = response.statusCode(); Version version = response.version(); @@ -121,7 +137,12 @@ public class TlsContextTest implements HttpServerAdapters { out.println("The protocol negotiated is :" + protocol); assertEquals(statusCode, 200); assertEquals(protocol, expectedProtocol); - assertEquals(version, expectedProtocol.equals("TLSv1.1") ? HTTP_1_1 : HTTP_2); + if (clientVersion == HTTP_3) { + assertEquals(version, expectedProtocol.equals("TLSv1.1") ? HTTP_1_1 : + expectedProtocol.equals("TLSv1.2") ? HTTP_2 : HTTP_3); + } else { + assertEquals(version, expectedProtocol.equals("TLSv1.1") ? HTTP_1_1 : HTTP_2); + } } @AfterTest @@ -143,8 +164,10 @@ public class TlsContextTest implements HttpServerAdapters { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); - t.sendResponseHeaders(200, 10); - os.write(bytes); + t.sendResponseHeaders(200, bytes.length); + if (bytes.length > 0) { + os.write(bytes); + } } } } diff --git a/test/jdk/java/net/httpclient/UnauthorizedTest.java b/test/jdk/java/net/httpclient/UnauthorizedTest.java index e2124f2a7ed..0c8c2b1d2cd 100644 --- a/test/jdk/java/net/httpclient/UnauthorizedTest.java +++ b/test/jdk/java/net/httpclient/UnauthorizedTest.java @@ -30,7 +30,8 @@ * for the client. If no authenticator is configured the client * should simply let the caller deal with the unauthorized response. * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext ReferenceTracker * @run testng/othervm * -Djdk.httpclient.HttpClient.log=headers * UnauthorizedTest @@ -60,6 +61,9 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -71,10 +75,12 @@ public class UnauthorizedTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; HttpClient authClient; HttpClient noAuthClient; @@ -97,6 +103,12 @@ public class UnauthorizedTest implements HttpServerAdapters { @DataProvider(name = "all") public Object[][] positive() { return new Object[][] { + { http3URI + "/server", UNAUTHORIZED, true, ref(authClient)}, + { http3URI + "/server", UNAUTHORIZED, false, ref(authClient)}, + { http3URI + "/server", UNAUTHORIZED, true, ref(noAuthClient)}, + { http3URI + "/server", UNAUTHORIZED, false, ref(noAuthClient)}, + + { httpURI + "/server", UNAUTHORIZED, true, ref(authClient)}, { httpsURI + "/server", UNAUTHORIZED, true, ref(authClient)}, { http2URI + "/server", UNAUTHORIZED, true, ref(authClient)}, @@ -137,6 +149,15 @@ public class UnauthorizedTest implements HttpServerAdapters { static final Authenticator authenticator = new Authenticator() { }; + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "all") void test(String uriString, int code, boolean async, WeakReference clientRef) throws Throwable { HttpClient client = clientRef.get(); @@ -145,8 +166,7 @@ public class UnauthorizedTest implements HttpServerAdapters { client.authenticator().isPresent() ? "authClient" : "noAuthClient"); URI uri = URI.create(uriString); - HttpRequest.Builder requestBuilder = HttpRequest - .newBuilder(uri) + HttpRequest.Builder requestBuilder = newRequestBuilder(uri) .GET(); HttpRequest request = requestBuilder.build(); @@ -163,6 +183,7 @@ public class UnauthorizedTest implements HttpServerAdapters { try { response = client.sendAsync(request, BodyHandlers.ofString()).get(); } catch (ExecutionException ex) { + ex.printStackTrace(); throw ex.getCause(); } } @@ -204,13 +225,17 @@ public class UnauthorizedTest implements HttpServerAdapters { https2TestServer.addHandler(new UnauthorizedHandler(), "/https2/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2"; - authClient = HttpClient.newBuilder() + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new UnauthorizedHandler(), "/http3/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3"; + + authClient = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .authenticator(authenticator) .build(); - noAuthClient = HttpClient.newBuilder() + noAuthClient = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); @@ -219,6 +244,7 @@ public class UnauthorizedTest implements HttpServerAdapters { httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -236,6 +262,7 @@ public class UnauthorizedTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); if (error != null) throw error; } diff --git a/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java b/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java index 97be90f8587..ee28980c035 100644 --- a/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java +++ b/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -21,34 +21,12 @@ * questions. */ -/** - * @test - * @bug 8326949 - * @summary Authorization header is removed when a proxy Authenticator is set - * @library /test/lib /test/jdk/java/net/httpclient /test/jdk/java/net/httpclient/lib - * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters - * jdk.httpclient.test.lib.http2.Http2TestServer - * jdk.test.lib.net.IPSupport - * - * @modules java.net.http/jdk.internal.net.http.common - * java.net.http/jdk.internal.net.http.frame - * java.net.http/jdk.internal.net.http.hpack - * java.logging - * java.base/sun.net.www.http - * java.base/sun.net.www - * java.base/sun.net - * - * @run main/othervm UserAuthWithAuthenticator - */ - import java.io.*; import java.net.*; import java.net.http.HttpClient; -import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; import javax.net.ssl.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -56,132 +34,165 @@ import java.util.regex.*; import java.util.*; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; -import jdk.test.lib.net.IPSupport; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.http2.Http2TestServer; import com.sun.net.httpserver.BasicAuthenticator; - -import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.Test; import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; -public class UserAuthWithAuthenticator { - private static final String AUTH_PREFIX = "Basic "; +/* + * @test + * @bug 8326949 + * @summary Authorization header is removed when a proxy Authenticator is set + * @library /test/lib /test/jdk/java/net/httpclient /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.net.IPSupport + * @run junit UserAuthWithAuthenticator + */ +class UserAuthWithAuthenticator { - static class AuthTestHandler implements HttpTestHandler { - volatile String authValue; - final String response = "Hello world"; + private static final class AuthTestHandler implements HttpTestHandler { + private volatile String authHeaderValue; @Override public void handle(HttpTestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { - byte[] bytes = is.readAllBytes(); - authValue = t.getRequestHeaders() + is.readAllBytes(); + authHeaderValue = t.getRequestHeaders() .firstValue("Authorization") - .orElse(AUTH_PREFIX) - .substring(AUTH_PREFIX.length()); + .orElse(""); + String response = "Hello world"; t.sendResponseHeaders(200, response.length()); os.write(response.getBytes(US_ASCII)); t.close(); } } - String authValue() {return authValue;} } - // if useHeader is true, we expect the Authenticator was not called - // and the user set header used. If false, Authenticator must - // be called and the user set header not used. + @Test + void h2Test() throws Exception { + h2Test(true, true); + h2Test(false, true); + h2Test(true, false); + } - // If rightPassword is true we expect the authentication to succeed and 200 OK - // If false, then an error should be returned. - - static void h2Test(final boolean useHeader, boolean rightPassword) throws Exception { - SSLContext ctx; - HttpTestServer h2s = null; - HttpClient client = null; - ExecutorService ex=null; - try { - ctx = new SimpleSSLContext().get(); - ex = Executors.newCachedThreadPool(); - InetAddress addr = InetAddress.getLoopbackAddress(); - - h2s = HttpTestServer.of(new Http2TestServer(addr, "::1", true, 0, ex, - 10, null, ctx, false)); - AuthTestHandler h = new AuthTestHandler(); - var context = h2s.addHandler(h, "/test1"); - context.setAuthenticator(new BasicAuthenticator("realm") { - public boolean checkCredentials(String username, String password) { - if (useHeader) { - return username.equals("user") && password.equals("pwd"); - } else { - return username.equals("serverUser") && password.equals("serverPwd"); - } - } - }); - h2s.start(); - - int port = h2s.getAddress().getPort(); - ServerAuth sa = new ServerAuth(); - var plainCreds = rightPassword? "user:pwd" : "user:wrongPwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); - - URI uri = URIBuilder.newBuilder() - .scheme("https") - .host(addr.getHostAddress()) - .port(port) - .path("/test1/foo.txt") - .build(); - - HttpClient.Builder builder = HttpClient.newBuilder() - .sslContext(ctx) - .executor(ex); - - builder.authenticator(sa); - client = builder.build(); - - HttpRequest req = HttpRequest.newBuilder(uri) - .version(HttpClient.Version.HTTP_2) - .header(useHeader ? "Authorization" : "X-Ignore", AUTH_PREFIX + encoded) - .GET() - .build(); - - HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); - if (!useHeader) { - assertTrue(resp.statusCode() == 200, "Expected 200 response"); - assertTrue(!h.authValue().equals(encoded), "Expected user set header to not be set"); - assertTrue(h.authValue().equals(sa.authValue()), "Expected auth value from Authenticator"); - assertTrue(sa.wasCalled(), "Expected authenticator to be called"); - System.out.println("h2Test: using authenticator OK"); - } else if (rightPassword) { - assertTrue(resp.statusCode() == 200, "Expected 200 response"); - assertTrue(h.authValue().equals(encoded), "Expected user set header to be set"); - assertTrue(!sa.wasCalled(), "Expected authenticator not to be called"); - System.out.println("h2Test: using user set header OK"); - } else { - assertTrue(resp.statusCode() == 401, "Expected 401 response"); - assertTrue(!sa.wasCalled(), "Expected authenticator not to be called"); - System.out.println("h2Test: using user set header with wrong password OK"); - } - } finally { - if (h2s != null) - h2s.stop(); - if (client != null) - client.close(); - if (ex != null) - ex.shutdown(); + private static void h2Test(final boolean useHeader, boolean rightPassword) throws Exception { + SSLContext sslContext = new SimpleSSLContext().get(); + try (ExecutorService executor = Executors.newCachedThreadPool(); + HttpTestServer server = HttpTestServer.of(new Http2TestServer( + InetAddress.getLoopbackAddress(), + "::1", + true, + 0, + executor, + 10, + null, + sslContext, + false)); + HttpClient client = HttpClient.newBuilder() + .sslContext(sslContext) + .executor(executor) + .authenticator(new ServerAuth()) + .build()) { + hXTest(useHeader, rightPassword, server, client, HttpClient.Version.HTTP_2); } } - static final String data = "0123456789"; + @Test + void h3Test() throws Exception { + h3Test(true, true); + h3Test(false, true); + h3Test(true, false); + } - static final String data1 = "ABCDEFGHIJKL"; + private static void h3Test(final boolean useHeader, boolean rightPassword) throws Exception { + SSLContext sslContext = new SimpleSSLContext().get(); + try (ExecutorService executor = Executors.newCachedThreadPool(); + HttpTestServer server = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext, executor); + HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .executor(executor) + .authenticator(new ServerAuth()) + .build()) { + hXTest(useHeader, rightPassword, server, client, HttpClient.Version.HTTP_3); + } + } - static final String[] proxyResponses = { + /** + * @param useHeader If {@code true}, we expect the authenticator was not called and the user set header used. + * If {@code false}, authenticator must be called and the user set header discarded. + * @param rightPassword If {@code true}, we expect the authentication to succeed with {@code 200 OK}. + * If {@code false}, then an error should be returned. + */ + private static void hXTest( + final boolean useHeader, + boolean rightPassword, + HttpTestServer server, + HttpClient client, + HttpClient.Version version) + throws Exception { + + AuthTestHandler handler = new AuthTestHandler(); + var context = server.addHandler(handler, "/test1"); + context.setAuthenticator(new BasicAuthenticator("realm") { + public boolean checkCredentials(String username, String password) { + if (useHeader) { + return username.equals("user") && password.equals("pwd"); + } else { + return username.equals("serverUser") && password.equals("serverPwd"); + } + } + }); + server.start(); + + URI uri = URIBuilder.newBuilder() + .scheme("https") + .host(server.getAddress().getAddress()) + .port(server.getAddress().getPort()) + .path("/test1/foo.txt") + .build(); + + var authHeaderValue = authHeaderValue("user", rightPassword ? "pwd" : "wrongPwd"); + HttpRequest req = HttpRequest.newBuilder(uri) + .version(version) + .header(useHeader ? "Authorization" : "X-Ignore", authHeaderValue) + .GET() + .build(); + + HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); + var sa = (ServerAuth) client.authenticator().orElseThrow(); + if (!useHeader) { + assertEquals(200, resp.statusCode(), "Expected 200 response"); + assertNotEquals(handler.authHeaderValue, authHeaderValue, "Expected user set header to not be set"); + assertEquals(handler.authHeaderValue, ServerAuth.AUTH_HEADER_VALUE, "Expected auth value from Authenticator"); + assertTrue(sa.called, "Expected authenticator to be called"); + } else if (rightPassword) { + assertEquals(200, resp.statusCode(), "Expected 200 response"); + assertEquals(authHeaderValue, handler.authHeaderValue, "Expected user set header to be set"); + assertFalse(sa.called, "Expected authenticator not to be called"); + } else { + assertEquals(401, resp.statusCode(), "Expected 401 response"); + assertFalse(sa.called, "Expected authenticator not to be called"); + } + + } + + private static final String data = "0123456789"; + + private static final String data1 = "ABCDEFGHIJKL"; + + private static final String[] proxyResponses = { "HTTP/1.1 407 Proxy Authentication Required\r\n"+ "Content-Length: 0\r\n" + "Proxy-Authenticate: Basic realm=\"Access to the proxy\"\r\n\r\n" @@ -192,7 +203,7 @@ public class UserAuthWithAuthenticator { "Content-Length: " + data.length() + "\r\n\r\n" + data }; - static final String[] proxyWithErrorResponses = { + private static final String[] proxyWithErrorResponses = { "HTTP/1.1 407 Proxy Authentication Required\r\n"+ "Content-Length: 0\r\n" + "Proxy-Authenticate: Basic realm=\"Access to the proxy\"\r\n\r\n" @@ -202,14 +213,14 @@ public class UserAuthWithAuthenticator { "Proxy-Authenticate: Basic realm=\"Access to the proxy\"\r\n\r\n" }; - static final String[] serverResponses = { + private static final String[] serverResponses = { "HTTP/1.1 200 OK\r\n"+ "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" + "Server: Apache/1.3.14 (Unix)\r\n" + "Content-Length: " + data1.length() + "\r\n\r\n" + data1 }; - static final String[] authenticatorResponses = { + private static final String[] authenticatorResponses = { "HTTP/1.1 401 Authentication Required\r\n"+ "Content-Length: 0\r\n" + "WWW-Authenticate: Basic realm=\"Access to the server\"\r\n\r\n" @@ -220,119 +231,97 @@ public class UserAuthWithAuthenticator { "Content-Length: " + data1.length() + "\r\n\r\n" + data1 }; - public static void main(String[] args) throws Exception { - testServerOnly(); - testServerWithProxy(); - testServerWithProxyError(); - testServerOnlyAuthenticator(); - h2Test(true, true); - h2Test(false, true); - h2Test(true, false); - } - - static void testServerWithProxy() throws IOException, InterruptedException { - Mocker proxyMock = new Mocker(proxyResponses); - proxyMock.start(); + @Test + void h1TestServerWithProxy() throws IOException, InterruptedException { ProxyAuth p = new ProxyAuth(); - try (var client = HttpClient.newBuilder() + try (var proxyMock = new Mocker(proxyResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .proxy(new ProxySel(proxyMock.getPort())) .authenticator(p) .build()) { - var plainCreds = "user:pwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); + var authHeaderValue = authHeaderValue("user", "pwd"); var request = HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1/some_url")) .setHeader("User-Agent", "myUserAgent") - .setHeader("Authorization", AUTH_PREFIX + encoded) + .setHeader("Authorization", authHeaderValue) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); - assertTrue(p.wasCalled(), "Proxy authenticator was not called"); + assertTrue(p.called, "Proxy authenticator was not called"); assertEquals(data, response.body()); - var proxyStr = proxyMock.getRequest(1); + var proxyStr = proxyMock.requests.get(1); assertContains(proxyStr, "/some_url"); - assertPattern(".*^Proxy-Authorization:.*Basic " + encoded + ".*", proxyStr); + assertPattern(".*^Proxy-Authorization:.*\\Q" + authHeaderValue + "\\E.*", proxyStr); assertPattern(".*^User-Agent:.*myUserAgent.*", proxyStr); assertPattern(".*^Authorization:.*Basic.*", proxyStr); - System.out.println("testServerWithProxy: OK"); - } finally { - proxyMock.stopMocker(); } } - static void testServerWithProxyError() throws IOException, InterruptedException { - Mocker proxyMock = new Mocker(proxyWithErrorResponses); - proxyMock.start(); + @Test + void h1TestServerWithProxyError() throws IOException, InterruptedException { ProxyAuth p = new ProxyAuth(); - try (var client = HttpClient.newBuilder() + try (var proxyMock = new Mocker(proxyWithErrorResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .proxy(new ProxySel(proxyMock.getPort())) .authenticator(p) .build()) { - var badCreds = "user:wrong"; - var encoded1 = java.util.Base64.getEncoder().encodeToString(badCreds.getBytes(US_ASCII)); + var authHeaderValue = authHeaderValue("user", "wrong"); var request = HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1/some_url")) .setHeader("User-Agent", "myUserAgent") - .setHeader("Proxy-Authorization", AUTH_PREFIX + encoded1) + .setHeader("Proxy-Authorization", authHeaderValue) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); - var proxyStr = proxyMock.getRequest(0); + var proxyStr = proxyMock.requests.getFirst(); assertEquals(407, response.statusCode()); - assertPattern(".*^Proxy-Authorization:.*Basic " + encoded1 + ".*", proxyStr); - assertTrue(!p.wasCalled(), "Proxy Auth should not have been called"); - System.out.println("testServerWithProxyError: OK"); - } finally { - proxyMock.stopMocker(); + assertPattern(".*^Proxy-Authorization:.*\\Q" + authHeaderValue + "\\E.*", proxyStr); + assertFalse(p.called, "Proxy Auth should not have been called"); } } - static void testServerOnly() throws IOException, InterruptedException { - Mocker serverMock = new Mocker(serverResponses); - serverMock.start(); - try (var client = HttpClient.newBuilder() + @Test + void h1TestServerOnly() throws IOException, InterruptedException { + try (var serverMock = new Mocker(serverResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .build()) { - var plainCreds = "user:pwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); + var authHeaderValue = authHeaderValue("user", "pwd"); var request = HttpRequest.newBuilder().uri(URI.create(serverMock.baseURL() + "/some_serv_url")) .setHeader("User-Agent", "myUserAgent") - .setHeader("Authorization", AUTH_PREFIX + encoded) + .setHeader("Authorization", authHeaderValue) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); assertEquals(data1, response.body()); - var serverStr = serverMock.getRequest(0); + var serverStr = serverMock.requests.getFirst(); assertContains(serverStr, "/some_serv_url"); assertPattern(".*^User-Agent:.*myUserAgent.*", serverStr); - assertPattern(".*^Authorization:.*Basic " + encoded + ".*", serverStr); - System.out.println("testServerOnly: OK"); - } finally { - serverMock.stopMocker(); + assertPattern(".*^Authorization:.*\\Q" + authHeaderValue + "\\E.*", serverStr); } } - // This is effectively a regression test for existing behavior - static void testServerOnlyAuthenticator() throws IOException, InterruptedException { - Mocker serverMock = new Mocker(authenticatorResponses); - serverMock.start(); - try (var client = HttpClient.newBuilder() + /** + * A regression test for existing behavior. + */ + @Test + void h1TestServerOnlyAuthenticator() throws IOException, InterruptedException { + try (var serverMock = new Mocker(authenticatorResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .authenticator(new ServerAuth()) .build()) { // credentials set in the server authenticator - var plainCreds = "serverUser:serverPwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); var request = HttpRequest.newBuilder().uri(URI.create(serverMock.baseURL() + "/some_serv_url")) .setHeader("User-Agent", "myUserAgent") .build(); @@ -341,42 +330,43 @@ public class UserAuthWithAuthenticator { assertEquals(200, response.statusCode()); assertEquals(data1, response.body()); - var serverStr = serverMock.getRequest(1); + var serverStr = serverMock.requests.get(1); assertContains(serverStr, "/some_serv_url"); assertPattern(".*^User-Agent:.*myUserAgent.*", serverStr); - assertPattern(".*^Authorization:.*Basic " + encoded + ".*", serverStr); - System.out.println("testServerOnlyAuthenticator: OK"); - } finally { - serverMock.stopMocker(); + assertPattern(".*^Authorization:.*\\Q" + authHeaderValue("serverUser", "serverPwd") + "\\E.*", serverStr); } } - static void close(Closeable... clarray) { - for (Closeable c : clarray) { - try { - c.close(); - } catch (Exception e) {} - } - } + private static final class Mocker extends Thread implements AutoCloseable { + private final ServerSocket ss; + private final String[] responses; + private final List requests; + private volatile InputStream in; + private volatile OutputStream out; + private volatile Socket s = null; - static class Mocker extends Thread { - final ServerSocket ss; - final String[] responses; - volatile List requests; - volatile InputStream in; - volatile OutputStream out; - volatile Socket s = null; - - public Mocker(String[] responses) throws IOException { + private Mocker(String[] responses) throws IOException { this.ss = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); this.responses = responses; this.requests = new LinkedList<>(); + start(); } - public void stopMocker() { + @Override + public void close() { close(ss, s, in, out); } + private static void close(Closeable... clarray) { + for (Closeable c : clarray) { + try { + c.close(); + } catch (Exception e) { + // Do nothing + } + } + } + public int getPort() { return ss.getLocalPort(); } @@ -404,15 +394,12 @@ public class UserAuthWithAuthenticator { in = s.getInputStream(); out = s.getOutputStream(); } - req += (char)x; + // noinspection StringConcatenationInLoop + req += (char) x; } return req; } - public String getRequest(int i) { - return requests.get(i); - } - public void run() { try { int index=0; @@ -424,15 +411,17 @@ public class UserAuthWithAuthenticator { out.write(responses[index++].getBytes(US_ASCII)); } } catch (Exception e) { + // noinspection CallToPrintStackTrace e.printStackTrace(); } } } - static class ProxySel extends ProxySelector { - final int port; + private static final class ProxySel extends ProxySelector { - ProxySel(int port) { + private final int port; + + private ProxySel(int port) { this.port = port; } @Override @@ -446,7 +435,8 @@ public class UserAuthWithAuthenticator { } - static class ProxyAuth extends Authenticator { + private static final class ProxyAuth extends Authenticator { + private volatile boolean called = false; @Override @@ -455,16 +445,17 @@ public class UserAuthWithAuthenticator { return new PasswordAuthentication("proxyUser", "proxyPwd".toCharArray()); } - boolean wasCalled() { - return called; - } } - static class ServerAuth extends Authenticator { + private static final class ServerAuth extends Authenticator { + private volatile boolean called = false; - private static String USER = "serverUser"; - private static String PASS = "serverPwd"; + private static final String USER = "serverUser"; + + private static final String PASS = "serverPwd"; + + private static final String AUTH_HEADER_VALUE = authHeaderValue(USER, PASS); @Override protected PasswordAuthentication getPasswordAuthentication() { @@ -476,49 +467,21 @@ public class UserAuthWithAuthenticator { return new PasswordAuthentication(USER, PASS.toCharArray()); } - String authValue() { - var plainCreds = USER + ":" + PASS; - return java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); - } - - boolean wasCalled() { - return called; - } } - static void assertTrue(boolean assertion, String failMsg) { - if (!assertion) { - throw new RuntimeException(failMsg); - } + private static String authHeaderValue(String username, String password) { + String credentials = username + ':' + password; + return "Basic " + java.util.Base64.getEncoder().encodeToString(credentials.getBytes(US_ASCII)); } - static void assertEquals(int a, int b) { - if (a != b) { - String msg = String.format("Error: expected %d Got %d", a, b); - throw new RuntimeException(msg); - } + private static void assertContains(String container, String containee) { + assertTrue(container.contains(containee), String.format("Error: expected %s Got %s", container, containee)); } - static void assertEquals(String s1, String s2) { - if (!s1.equals(s2)) { - String msg = String.format("Error: expected %s Got %s", s1, s2); - throw new RuntimeException(msg); - } - } - - static void assertContains(String container, String containee) { - if (!container.contains(containee)) { - String msg = String.format("Error: expected %s Got %s", container, containee); - throw new RuntimeException(msg); - } - } - - static void assertPattern(String pattern, String candidate) { + private static void assertPattern(String pattern, String candidate) { Pattern pat = Pattern.compile(pattern, Pattern.DOTALL | Pattern.MULTILINE); Matcher matcher = pat.matcher(candidate); - if (!matcher.matches()) { - String msg = String.format("Error: expected %s Got %s", pattern, candidate); - throw new RuntimeException(msg); - } + assertTrue(matcher.matches(), String.format("Error: expected %s Got %s", pattern, candidate)); } + } diff --git a/test/jdk/java/net/httpclient/UserCookieTest.java b/test/jdk/java/net/httpclient/UserCookieTest.java index b8de5f97955..50a13e48322 100644 --- a/test/jdk/java/net/httpclient/UserCookieTest.java +++ b/test/jdk/java/net/httpclient/UserCookieTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, 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 @@ -66,11 +66,7 @@ import java.util.stream.Stream; import javax.net.ServerSocketFactory; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -80,22 +76,27 @@ import org.testng.annotations.Test; import static java.lang.System.out; 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 java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; public class UserCookieTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 7 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) DummyServer httpDummyServer; DummyServer httpsDummyServer; String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; String httpDummy; String httpsDummy; @@ -113,6 +114,7 @@ public class UserCookieTest implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, HTTP_3 }, { httpURI, HTTP_1_1 }, { httpsURI, HTTP_1_1 }, { httpDummy, HTTP_1_1 }, @@ -134,7 +136,10 @@ public class UserCookieTest implements HttpServerAdapters { ConcurrentHashMap> cookieHeaders = new ConcurrentHashMap<>(); CookieHandler cookieManager = new TestCookieHandler(cookieHeaders); - HttpClient client = HttpClient.newBuilder() + var builder = version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .followRedirects(Redirect.ALWAYS) .cookieHandler(cookieManager) .sslContext(sslContext) @@ -159,6 +164,9 @@ public class UserCookieTest implements HttpServerAdapters { .header("Cookie", userCookie); if (version != null) { requestBuilder.version(version); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } } HttpRequest request = requestBuilder.build(); out.println("Initial request: " + request.uri()); @@ -181,9 +189,13 @@ public class UserCookieTest implements HttpServerAdapters { .header("Cookie", userCookie); if (version != null) { requestBuilder.version(version); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } } request = requestBuilder.build(); } + client.close(); } // -- Infrastructure @@ -208,6 +220,10 @@ public class UserCookieTest implements HttpServerAdapters { https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new CookieValidationHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/retry"; + InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); // DummyServer httpDummyServer = DummyServer.create(sa); @@ -219,6 +235,7 @@ public class UserCookieTest implements HttpServerAdapters { httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); httpDummyServer.start(); httpsDummyServer.start(); } @@ -229,6 +246,7 @@ public class UserCookieTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); httpsDummyServer.stopServer(); httpsDummyServer.stopServer(); } diff --git a/test/jdk/java/net/httpclient/VersionTest.java b/test/jdk/java/net/httpclient/VersionTest.java index d09ce0354d2..ff864202a9a 100644 --- a/test/jdk/java/net/httpclient/VersionTest.java +++ b/test/jdk/java/net/httpclient/VersionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -49,6 +49,7 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; 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; public class VersionTest { static HttpServer s1 ; @@ -80,6 +81,8 @@ public class VersionTest { test(HTTP_1_1, false); test(HTTP_2, false); test(HTTP_2, true); + test(HTTP_3, false); + test(HTTP_3, true); } finally { s1.stop(0); executor.shutdownNow(); diff --git a/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.java b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.java new file mode 100644 index 00000000000..3e58990fb90 --- /dev/null +++ b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.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. + */ +package jdk.internal.net.http; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.http3.ConnectionSettings; + +public final class Http3ConnectionAccess { + + private Http3ConnectionAccess() { + throw new AssertionError(); + } + + static HttpClientImpl impl(HttpClient client) { + if (client instanceof HttpClientImpl impl) return impl; + if (client instanceof HttpClientFacade facade) return facade.impl; + return null; + } + + static HttpRequestImpl impl(HttpRequest request) { + if (request instanceof HttpRequestImpl impl) return impl; + return null; + } + + public static CompletableFuture peerSettings(HttpClient client, HttpResponse resp) { + try { + Http3Connection conn = impl(client) + .client3() + .get() + .findPooledConnectionFor(impl(resp.request()), null); + if (conn == null) { + return MinimalFuture.failedFuture(new NoSuchElementException("no connection found")); + } + return conn.peerSettingsCF(); + } catch (Exception ex) { + return MinimalFuture.failedFuture(ex); + } + } +} diff --git a/src/hotspot/share/gc/shared/bufferNodeList.hpp b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java similarity index 61% rename from src/hotspot/share/gc/shared/bufferNodeList.hpp rename to test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java index 55905ec071a..ab915428dc9 100644 --- a/src/hotspot/share/gc/shared/bufferNodeList.hpp +++ b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, 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 @@ -19,23 +19,24 @@ * 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.common; -#ifndef SHARE_GC_SHARED_BUFFERNODELIST_HPP -#define SHARE_GC_SHARED_BUFFERNODELIST_HPP +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SSLSession; -#include "utilities/globalDefinitions.hpp" +public final class ImmutableSSLSessionAccess { -class BufferNode; + private ImmutableSSLSessionAccess() { + throw new AssertionError(); + } -struct BufferNodeList { - BufferNode* _head; // First node in list or null if empty. - BufferNode* _tail; // Last node in list or null if empty. - size_t _entry_count; // Sum of entries in nodes in list. + public static ImmutableSSLSession immutableSSLSession(SSLSession session) { + return new ImmutableSSLSession(session); + } - BufferNodeList(); - BufferNodeList(BufferNode* head, BufferNode* tail, size_t entry_count); -}; + public static ImmutableExtendedSSLSession immutableExtendedSSLSession(ExtendedSSLSession session) { + return new ImmutableExtendedSSLSession(session); + } -#endif // SHARE_GC_SHARED_BUFFERNODELIST_HPP +} diff --git a/test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java b/test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java new file mode 100644 index 00000000000..0da1b238f60 --- /dev/null +++ b/test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java @@ -0,0 +1,688 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import jdk.httpclient.test.lib.common.DynamicKeyStoreUtil; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.ServerNameMatcher; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.Test; +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateCert; +import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateKeyStore; +import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateRSAKeyPair; +import static jdk.httpclient.test.lib.http3.Http3TestServer.quicServerBuilder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary verifies the HttpClient's usage of alternate services + * @comment The goal of this test class is to run various tests to verify that the HttpClient + * (and the underlying layers) use an alternate server for HTTP request(s) IF AND ONLY IF such an + * advertised alternate server satisfies "reasonable assurance" expectations as noted in the + * alternate service RFC-7838. Reasonable assurance can be summarized as: + * - The origin server which advertised the alternate service, MUST be running on TLS + * - The certificate presented by origin server during TLS handshake must be valid (and trusted) + * for the origin server + * - The certificate presented by alternate server (when subsequently a connection attempt is + * made to it) MUST be valid (and trusted) for the ORIGIN server + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.common.DynamicKeyStoreUtil + * jdk.test.lib.net.URIBuilder + * @modules java.base/sun.net.www.http + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * @modules java.base/sun.security.x509 + * java.base/jdk.internal.util + * @run junit/othervm -Djdk.net.hosts.file=${test.src}/altsvc-dns-hosts.txt + * -Djdk.internal.httpclient.debug=true -Djavax.net.debug=all + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * AltServiceReasonableAssurance + */ +public class AltServiceReasonableAssurance implements HttpServerAdapters { + + private static final String ORIGIN_SERVER_HOSTNAME = "origin.server"; + private static final String ALT_SERVER_HOSTNAME = "altservice.server"; + + private static final String ALT_SERVER_RESPONSE_MESSAGE = "Hello from an alt server"; + private static final String ORIGIN_SERVER_RESPONSE_MESSAGE = "Hello from an origin server"; + + private record TestInput(HttpTestServer originServer, HttpTestServer altServer, + URI requestURI, String expectedAltSvcHeader) { + } + + /** + * Creates and starts a origin server and an alternate server. The passed (same) SSLContext + * is used by both the origin server and the alternate server. + */ + private static TestInput startOriginAndAltServer(final SSLContext sslContext) + throws Exception { + Objects.requireNonNull(sslContext); + return startOriginAndAltServer(sslContext, sslContext); + } + + /** + * Creates and starts a origin server and an alternate server. The origin server will use + * the {@code originSrvSSLCtx} and the alternate server will use the {@code altSrvSSLCtx} + */ + private static TestInput startOriginAndAltServer(final SSLContext originSrvSSLCtx, + final SSLContext altSrvSSLCtx) + throws Exception { + Objects.requireNonNull(originSrvSSLCtx); + Objects.requireNonNull(altSrvSSLCtx); + final String requestPath = "/hello"; + final QuicServer quicServer = quicServerBuilder() + .sslContext(altSrvSSLCtx) + // the client sends a SNI for origin server. this alt server should be capable + // of matching/accepting that SNI name of the origin + .sniMatcher(new ServerNameMatcher(ORIGIN_SERVER_HOSTNAME)) + .build(); + // Alt server only supports H3 + final HttpTestServer altServer = HttpTestServer.of(new Http3TestServer(quicServer)); + altServer.addHandler(new Handler(ALT_SERVER_RESPONSE_MESSAGE), requestPath); + altServer.start(); + System.out.println("Alt server started at " + altServer.getAddress()); + + // H2 server which has a (application level) handler which advertises H3 alt service + final HttpTestServer originServer = HttpTestServer.of( + new Http2TestServer(ORIGIN_SERVER_HOSTNAME, true, originSrvSSLCtx)); + final int altServerPort = altServer.getAddress().getPort(); + final String altSvcHeaderVal = "h3=\"" + ALT_SERVER_HOSTNAME + ":" + altServerPort + "\""; + originServer.addHandler(new Handler(ORIGIN_SERVER_RESPONSE_MESSAGE, altSvcHeaderVal), + requestPath); + originServer.start(); + System.out.println("Origin server started at " + originServer.getAddress()); + // request URI should be directed to the origin server + final URI requestURI = URIBuilder.newBuilder() + .scheme("https") + .host(ORIGIN_SERVER_HOSTNAME) + .port(originServer.getAddress().getPort()) + .path(requestPath) + .build(); + return new TestInput(originServer, altServer, requestURI, altSvcHeaderVal); + } + + private TestInput startHttpOriginHttpsAltServer(final SSLContext altServerSSLCtx) + throws Exception { + Objects.requireNonNull(altServerSSLCtx); + final String requestPath = "/foo"; + // Alt server only supports H3 + final HttpTestServer altServer = HttpTestServer.create(HTTP_3_URI_ONLY, altServerSSLCtx); + altServer.addHandler(new Handler(ALT_SERVER_RESPONSE_MESSAGE), requestPath); + altServer.start(); + System.out.println("Alt server (HTTPS) started at " + altServer.getAddress()); + + // supports only HTTP server and uses a (application level) handler which advertises a H3 + // alternate service + final HttpTestServer originServer = HttpTestServer.create(HTTP_2); + final int altServerPort = altServer.getAddress().getPort(); + final String altSvcHeaderVal = "h3=\"" + ALT_SERVER_HOSTNAME + ":" + altServerPort + "\""; + originServer.addHandler(new Handler(ORIGIN_SERVER_RESPONSE_MESSAGE, altSvcHeaderVal), + requestPath); + originServer.start(); + System.out.println("Origin server (HTTP) started at " + originServer.getAddress()); + // request URI should be against (HTTP) origin server + final URI requestURI = URIBuilder.newBuilder() + .scheme("http") + .host(ORIGIN_SERVER_HOSTNAME) + .port(originServer.getAddress().getPort()) + .path(requestPath) + .build(); + return new TestInput(originServer, altServer, requestURI, altSvcHeaderVal); + } + + /** + * Stop the server (and ignore any exception) + */ + private static void safeStop(final HttpTestServer server) { + if (server == null) { + return; + } + final InetSocketAddress serverAddr = server.getAddress(); + try { + System.out.println("Stopping server " + serverAddr); + server.stop(); + } catch (Exception e) { + System.err.println("Ignoring exception: " + e.getMessage() + " that occurred " + + "during stop of server: " + serverAddr); + } + } + + /** + * Returns back a 200 HTTP response with a response body containing a response message + * that was used to construct the Handler instance. Additionally, if the Handler was constructed + * with a non-null {@code altSvcHeaderVal} then that value is sent back as a header value. in + * the response, for the {@code alt-svc} header + */ + private static final class Handler implements HttpTestHandler { + private final String responseMessage; + private final byte[] responseBytes; + private final String altSvcHeaderVal; + + private Handler(final String responseMessage) { + this(responseMessage, null); + } + + private Handler(final String responseMessage, final String altSvcHeaderVal) { + Objects.requireNonNull(responseMessage); + this.responseMessage = responseMessage; + this.responseBytes = responseMessage.getBytes(StandardCharsets.UTF_8); + this.altSvcHeaderVal = altSvcHeaderVal; + } + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + System.out.println("Handling request " + exchange.getRequestURI()); + if (this.altSvcHeaderVal != null) { + System.out.println("Responding with alt-svc header: " + this.altSvcHeaderVal); + exchange.getResponseHeaders().addHeader("alt-svc", this.altSvcHeaderVal); + } + System.out.println("Responding with body: " + this.responseMessage); + exchange.sendResponseHeaders(200, this.responseBytes.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(this.responseBytes); + } + } + } + + /** + * - Keystore K1 is constructed with a certificate whose subject is origin server hostname and + * subject alternative name is alternate server hostname + * - K1 is used to construct a SSLContext and thus the SSLContext uses the keys and trusted + * certificate from this keystore + * - The constructed SSLContext instance is used by the HttpClient, the origin server and the + * alternate server + * - During TLS handshake with origin server, the origin server is expected to present the + * certificate from this K1 keystore. + * - During TLS handshake with alternate server, the alternate server is expected to present + * this same certificate from K1 keystore. + * - Since the certificate is valid (and trusted by the client) for both origin server + * and alternate server (because of the valid subject name and subject alternate name), + * the TLS handshake between the HttpClient and the origin and alternate server is expected + * to pass + *

    + * Once the servers are started, this test method does the following: + *

    + * - Client constructs a HTTP_3 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the *same* request again + * - The request is expected to be handled by the alternate server + */ + @Test + public void testOriginAltSameCert() throws Exception { + // create a keystore which contains a PrivateKey entry and a certificate associated with + // that key. the certificate's subject will be origin server's hostname and will + // additionally have the alt server hostname as a subject alternate name. Thus, the + // certificate is valid for both origin server and alternate server + final KeyStore keyStore = generateKeyStore(ORIGIN_SERVER_HOSTNAME, ALT_SERVER_HOSTNAME); + System.out.println("Generated a keystore with certificate: " + + keyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create a SSLContext that will be used by the servers and the HttpClient and will be + // backed by the keystore we just created. Thus, the HttpClient will trust the certificate + // belonging to that keystore + final SSLContext sslContext = DynamicKeyStoreUtil.createSSLContext(keyStore); + // start the servers + final TestInput testInput = startOriginAndAltServer(sslContext); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Issuing request " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue the same request again and this time expect it to be handled + // by the alt-service + System.out.println("Again issuing request " + requestURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, secondResponse.statusCode(), "Unexpected response code"); + // expect the alt service to respond + assertEquals(ALT_SERVER_RESPONSE_MESSAGE, secondResponse.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_3, secondResponse.version(), "Unexpected HTTP version in response"); + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + /** + * - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is + * origin server hostname + * - Keystore K2 is constructed with the same PrivateKey PK1 and certificate whose subject is + * alternate server hostname AND has a subject alternate name of origin server hostname + * - K1 is used to construct a SSLContext S1 and that S1 is used by origin server + * - K2 is used to construct a SSLContext S2 and that S2 is used by alternate server + * - SSLContext S3 is constructed with both the certificate of origin server and + * the certificate of alternate server as trusted certificates. HttpClient uses S3 + * - During TLS handshake with origin server, the origin server is expected to present the + * certificate from this K1 keystore, with subject as origin server hostname + * - During TLS handshake with alternate server, the alternate server is expected to present + * the certificate from K2 keystore, with subject as alternate server hostname AND a subject + * alternate name of origin server + * - HttpClient (through S3 SSLContext) trusts both these certs. The cert presented + * by alt server, is valid (even) for origin server (since its subject alternate name is + * origin server hostname). Thus, the client must consider the alternate service as valid and + * use it. + *

    + * Once the servers are started, this test method does the following: + *

    + * - Client constructs a HTTP_3 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the *same* request again + * - The request is expected to be handled by the alternate server + */ + @Test + public void testOriginAltDifferentCert() throws Exception { + final SecureRandom secureRandom = new SecureRandom(); + final KeyPair keyPair = generateRSAKeyPair(secureRandom); + + // generate a certificate for origin server, with origin server hostname as the subject + final X509Certificate originServerCert = generateCert(keyPair, secureRandom, + ORIGIN_SERVER_HOSTNAME); + // create a keystore with the private key and the cert. this keystore will then be + // used by the SSLContext of origin server + final KeyStore originServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{originServerCert}); + System.out.println("Generated a keystore, for origin server, with certificate: " + + originServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create the SSLContext for the origin server + final SSLContext originServerSSLCtx = DynamicKeyStoreUtil.createSSLContext( + originServerKeyStore); + + // create a cert for the alternate server, with alternate server hostname as the subject + // AND origin server hostname as a subject alternate name + final X509Certificate altServerCert = generateCert(keyPair, secureRandom, + ALT_SERVER_HOSTNAME, ORIGIN_SERVER_HOSTNAME); + // create keystore with the private key and the alt server's cert. this keystore will then + // be used by the SSLContext of alternate server + final KeyStore altServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{altServerCert}); + System.out.println("Generated a keystore, for alt server, with certificate: " + + altServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create SSLContext of alternate server + final SSLContext altServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(altServerKeyStore); + + // now create a SSLContext for the HttpClient. This SSLContext will contain no key manager + // and will have a trust manager which trusts origin server certificate and the alternate + // server certificate + final SSLContext clientSSLCtx = sslCtxWithTrustedCerts(List.of(originServerCert, + altServerCert)); + // start the servers + final TestInput testInput = startOriginAndAltServer(originServerSSLCtx, altServerSSLCtx); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(clientSSLCtx) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Issuing request " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue the same request again and this time expect it to be handled + // by the alt-service + System.out.println("Again issuing request " + requestURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, secondResponse.statusCode(), "Unexpected response code"); + // expect the alt service to respond + assertEquals(ALT_SERVER_RESPONSE_MESSAGE, secondResponse.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_3, secondResponse.version(), "Unexpected HTTP version in response"); + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + + /** + * - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is + * origin server hostname + * - Keystore K2 is constructed with the same PrivateKey PK1 and certificate whose subject is + * alternate server hostname + * - K1 is used to construct a SSLContext S1 and that S1 is used by origin server + * - K2 is used to construct a SSLContext S2 and that S2 is used by alternate server + * - SSLContext S3 is constructed with both the certificate of origin server and + * the certificate of alternate server as trusted certificates. HttpClient uses S3 + * - During TLS handshake with origin server, the origin server is expected to present the + * certificate from this K1 keystore, with subject as origin server hostname + * - During TLS handshake with alternate server, the alternate server is expected to present + * the certificate from K2 keystore, with subject as alternate server hostname + * - HttpClient (through S3 SSLContext) trusts both these certs, but the cert presented + * by alt server, although valid for the alt server, CANNOT/MUST NOT be valid for origin + * server (since it's subject nor subject alternate name is origin server hostname). + * Reasonable assurance expects that the alt server present a certificate that is valid for + * origin server host and since it doesn't, the alt server must not be used by the HttpClient. + *

    + * Once the servers are started, this test method does the following: + *

    + * - Client constructs a HTTP_3 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the *same* request again + * - The request is expected to be handled by the origin server again and the advertised + * alternate service MUST NOT be used (due to reasons noted above) + */ + @Test + public void testAltServerWrongCert() throws Exception { + final SecureRandom secureRandom = new SecureRandom(); + final KeyPair keyPair = generateRSAKeyPair(secureRandom); + + // generate a certificate for origin server, with origin server hostname as the subject + final X509Certificate originServerCert = generateCert(keyPair, secureRandom, + ORIGIN_SERVER_HOSTNAME); + // create a keystore with the private key and the cert. this keystore will then be + // used by the SSLContext of origin server + final KeyStore originServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{originServerCert}); + System.out.println("Generated a keystore, for origin server, with certificate: " + + originServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create the SSLContext for the origin server + final SSLContext originServerSSLCtx = DynamicKeyStoreUtil.createSSLContext( + originServerKeyStore); + + // create a cert for the alternate server, with alternate server hostname as the subject + final X509Certificate altServerCert = generateCert(keyPair, secureRandom, + ALT_SERVER_HOSTNAME); + // create keystore with the private key and the alt server's cert. this keystore will then + // be used by the SSLContext of alternate server + final KeyStore altServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{altServerCert}); + System.out.println("Generated a keystore, for alt server, with certificate: " + + altServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create SSLContext of alternate server + final SSLContext altServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(altServerKeyStore); + + // now create a SSLContext for the HttpClient. This SSLContext will contain no key manager + // and will have a trust manager which trusts origin server certificate and the alternate + // server certificate + final SSLContext clientSSLCtx = sslCtxWithTrustedCerts(List.of(originServerCert, + altServerCert)); + // start the servers + final TestInput testInput = startOriginAndAltServer(originServerSSLCtx, altServerSSLCtx); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(clientSSLCtx) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Issuing request " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue the same request again (a few times). Expect each of these requests too, + // to be handled by the origin server (since the advertised alt server isn't expected + // to satisfy the "reasonable assurances" expectations + for (int i = 1; i <= 3; i++) { + System.out.println("Again(" + i + ") issuing request " + requestURI); + final HttpResponse rsp = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, rsp.statusCode(), "Unexpected response code"); + // expect the alt service to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, rsp.body(), + "Unexpected response body"); + assertEquals(HTTP_2, rsp.version(), "Unexpected HTTP version in response"); + } + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + + /** + * - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is + * alternate server hostname + * - K1 is used to construct a SSLContext S1 and that S1 is used by alternate server + * - The same SSLContext S1 is used by the HttpClient (and thus will trust the alternate + * server's certificate) + * - Origin server runs only on HTTP + * - Any alt-svc advertised by origin server MUST NOT be used by the client, because origin + * server runs on HTTP and as a result the "reasonable assurance" for the origin server + * cannot be satisfied. + *

    + * Once the servers are started, this test method does the following: + *

    + * - Client constructs a HTTP2 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the request again, to the origin server, this time with HTTP3 as the request + * version + * - The request is expected to be handled by the origin server again and the advertised + * alternate service MUST NOT be used (due to reasons noted above) + */ + @Test + public void testAltServiceAdvertisedByHTTPOrigin() throws Exception { + // create a keystore which contains a PrivateKey entry and a certificate associated with + // that key. the certificate's subject will be alternate server's hostname. Thus, the + // certificate is valid for alternate server + final KeyStore keyStore = generateKeyStore(ALT_SERVER_HOSTNAME); + System.out.println("Generated a keystore with certificate: " + + keyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create a SSLContext that will be used by the alternate server and the HttpClient and + // will be backed by the keystore we just created. Thus, the HttpClient will trust the + // certificate belonging to that keystore + final SSLContext sslContext = DynamicKeyStoreUtil.createSSLContext(keyStore); + + // start the servers + final TestInput testInput = startHttpOriginHttpsAltServer(sslContext); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP2 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .version(HTTP_2).GET() + .uri(requestURI).build(); + System.out.println("Issuing " + request.version() + " request to " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue few more requests to the same address, but as a HTTP3 version. Expect each + // of these requests too, to be handled by the origin server (since the previously + // advertised alt server isn't expected to satisfy the "reasonable assurances" + // expectations) + for (int i = 1; i <= 3; i++) { + final HttpRequest h3Request = HttpRequest.newBuilder() + .version(HTTP_3).GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Again(" + i + ") issuing " + h3Request.version() + + " request to " + requestURI); + final HttpResponse rsp = client.send(h3Request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, rsp.statusCode(), "Unexpected response code"); + // even though the request is HTTP_3 version, the client will fall back to HTTP_2 + // since the origin server does run on TLS and thus cannot use HTTP_3 (which + // requires TLS) + assertEquals(HTTP_2, rsp.version(), "Unexpected HTTP version in response"); + // expect the alt service to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, rsp.body(), + "Unexpected response body"); + } + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + private static SSLContext sslCtxWithTrustedCerts(final List trustedCerts) + throws Exception { + Objects.requireNonNull(trustedCerts); + // start with a blank keystore + final KeyStore keyStore = DynamicKeyStoreUtil.generateBlankKeyStore(); + final String aliasPrefix = "trusted-certs-alias-"; + int i = 1; + for (final Certificate cert : trustedCerts) { + // add the cert as a trusted certificate to the keystore + keyStore.setCertificateEntry(aliasPrefix + i, cert); + i++; + } + System.out.println("Generated a keystore with (only) trusted certs: "); + for (--i; i > 0; i--) { + System.out.println(keyStore.getCertificate(aliasPrefix + i)); + } + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + // use the generated keystore for this trust manager + tmf.init(keyStore); + + final String protocol = "TLS"; + final SSLContext ctx = SSLContext.getInstance(protocol); + // initialize the SSLContext with the trust manager which trusts the passed certificates + ctx.init(null, tmf.getTrustManagers(), null); + return ctx; + } +} diff --git a/test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt b/test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt new file mode 100644 index 00000000000..67445f3b278 --- /dev/null +++ b/test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt @@ -0,0 +1,23 @@ +## 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. +## +## 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. +## +127.0.0.1 origin.server +127.0.0.1 altservice.server diff --git a/test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java b/test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java new file mode 100644 index 00000000000..a5617d85127 --- /dev/null +++ b/test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java @@ -0,0 +1,46 @@ +/* + * 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. + * + * 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.common; + +import java.util.function.Supplier; + +import jdk.internal.net.http.common.DebugLogger.LoggerConfig; + +public class TestLoggerUtil { + + public static Logger getStdoutLogger(Supplier dbgTag) { + var logLevel = Utils.DEBUG_CONFIG.logLevel(); + LoggerConfig config = Utils.DEBUG + ? LoggerConfig.STDOUT.withLogLevel(logLevel) + : LoggerConfig.OFF; + return DebugLogger.createHttpLogger(dbgTag, config); + } + + public static Logger getErrOutLogger(Supplier dbgTag) { + var logLevel = Utils.DEBUG_CONFIG.logLevel(); + LoggerConfig config = Utils.DEBUG + ? LoggerConfig.ERROUT.withLogLevel(logLevel) + : LoggerConfig.OFF; + return DebugLogger.createHttpLogger(dbgTag, config); + } +} diff --git a/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java b/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java index 73cc12ce478..c7edfd5fa8f 100644 --- a/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java +++ b/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java @@ -168,7 +168,7 @@ public class BadPushPromiseTest { } } - private void pushPromise(HttpServerAdapters.HttpTestExchange exchange) { + private void pushPromise(HttpServerAdapters.HttpTestExchange exchange) throws IOException { URI requestURI = exchange.getRequestURI(); String query = exchange.getRequestURI().getQuery(); int badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1)); diff --git a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java index 04ce8f4f4a4..f27fdb580dd 100644 --- a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java +++ b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java @@ -46,6 +46,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; + import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.ContinuationFrame; import jdk.internal.net.http.frame.HeaderFrame; @@ -154,6 +155,22 @@ public class ContinuationFrameTest { static final int ITERATION_COUNT = 20; + HttpClient sharedClient; + HttpClient httpClient(boolean shared) { + if (!shared || sharedClient == null) { + var client = HttpClient.newBuilder() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .build(); + if (sharedClient == null) { + sharedClient = client; + } + TRACKER.track(client); + return client; + } + return sharedClient; + } + @Test(dataProvider = "variants") void test(String uri, boolean sameClient, @@ -165,11 +182,7 @@ public class ContinuationFrameTest { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(HttpClient.Builder.NO_PROXY) - .sslContext(sslContext) - .build(); - TRACKER.track(client); + client = httpClient(sameClient); } HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) @@ -229,6 +242,7 @@ public class ContinuationFrameTest { @AfterTest public void teardown() throws Exception { + sharedClient = null; AssertionError fail = TRACKER.check(500); try { http2TestServer.stop(); diff --git a/test/jdk/java/net/httpclient/http2/ErrorTest.java b/test/jdk/java/net/httpclient/http2/ErrorTest.java index 061fd5cd350..e8613b9efa8 100644 --- a/test/jdk/java/net/httpclient/http2/ErrorTest.java +++ b/test/jdk/java/net/httpclient/http2/ErrorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, 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 @@ -25,12 +25,26 @@ * @test * @bug 8157105 * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.net.SimpleSSLContext * @modules java.base/sun.net.www.http * java.net.http/jdk.internal.net.http.common * java.net.http/jdk.internal.net.http.frame * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers * java.security.jgss + * @modules java.base/jdk.internal.util * @run testng/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all ErrorTest * @summary check exception thrown when bad TLS parameters selected */ @@ -47,7 +61,6 @@ import javax.net.ssl.SSLParameters; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2EchoHandler; import jdk.test.lib.net.SimpleSSLContext; diff --git a/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java index 7d5424163ba..4c63e863fee 100644 --- a/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java +++ b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java @@ -29,6 +29,6 @@ * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest + * @run testng/othervm/timeout=240 java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest */ public class HpackBinaryTestDriver { } diff --git a/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java b/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java index d2b515c6d63..650ed706c51 100644 --- a/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java +++ b/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java @@ -29,6 +29,6 @@ * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.HuffmanTest + * @run testng/othervm/timeout=300 java.net.http/jdk.internal.net.http.hpack.HuffmanTest */ public class HpackHuffmanDriver { } diff --git a/test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java b/test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java deleted file mode 100644 index ac060e8722b..00000000000 --- a/test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 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. - * - * 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 8288717 - * @summary Tests that when the idleConnectionTimeoutEvent is configured in HTTP/2, - * an HTTP/2 connection will close within the specified interval if there - * are no active streams on the connection. - * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.http2.Http2TestServer - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout=1 - * IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout=2 - * IdleConnectionTimeoutTest - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=1 - * IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=2 - * IdleConnectionTimeoutTest - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=1 - * -Djdk.httpclient.keepalive.timeout=2 - * IdleConnectionTimeoutTest - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=-1 - * IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,trace -Djdk.httpclient.keepalive.timeout.h2=abc - * IdleConnectionTimeoutTest - */ - -import jdk.httpclient.test.lib.http2.BodyOutputStream; -import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.internal.net.http.common.HttpHeadersBuilder; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.concurrent.CompletableFuture; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; - -import javax.net.ssl.SSLSession; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.net.http.HttpClient.Version.HTTP_2; -import static org.testng.Assert.assertEquals; - -public class IdleConnectionTimeoutTest { - - static Http2TestServer http2TestServer; - URI timeoutUri; - URI noTimeoutUri; - final String IDLE_CONN_PROPERTY = "jdk.httpclient.keepalive.timeout.h2"; - final String KEEP_ALIVE_PROPERTY = "jdk.httpclient.keepalive.timeout"; - final String TIMEOUT_PATH = "/serverTimeoutHandler"; - final String NO_TIMEOUT_PATH = "/noServerTimeoutHandler"; - static final PrintStream testLog = System.err; - - @BeforeTest - public void setup() throws Exception { - http2TestServer = new Http2TestServer(false, 0); - http2TestServer.addHandler(new ServerTimeoutHandler(), TIMEOUT_PATH); - http2TestServer.addHandler(new ServerNoTimeoutHandler(), NO_TIMEOUT_PATH); - http2TestServer.setExchangeSupplier(TestExchangeSupplier::new); - - http2TestServer.start(); - int port = http2TestServer.getAddress().getPort(); - timeoutUri = new URI("http://localhost:" + port + TIMEOUT_PATH); - noTimeoutUri = new URI("http://localhost:" + port + NO_TIMEOUT_PATH); - } - - /* - If the InetSocketAddress of the first remote connection is not equal to the address of the - second remote connection, then the idleConnectionTimeoutEvent has occurred and a new connection - was made to carry out the second request by the client. - */ - @Test - public void test() throws InterruptedException { - String timeoutVal = System.getProperty(IDLE_CONN_PROPERTY); - String keepAliveVal = System.getProperty(KEEP_ALIVE_PROPERTY); - testLog.println("Test run for " + IDLE_CONN_PROPERTY + "=" + timeoutVal); - - int sleepTime = 0; - HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build(); - HttpRequest hreq; - HttpResponse hresp; - if (timeoutVal != null) { - if (keepAliveVal != null) { - // In this case, specified h2 timeout should override keep alive timeout. - // Timeout should occur - hreq = HttpRequest.newBuilder(timeoutUri).version(HTTP_2).GET().build(); - sleepTime = 2000; - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was expected but did not occur"); - } else if (timeoutVal.equals("1")) { - // Timeout should occur - hreq = HttpRequest.newBuilder(timeoutUri).version(HTTP_2).GET().build(); - sleepTime = 2000; - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was expected but did not occur"); - } else if (timeoutVal.equals("2")) { - // Timeout should not occur - hreq = HttpRequest.newBuilder(noTimeoutUri).version(HTTP_2).GET().build(); - sleepTime = 1000; - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); - } else if (timeoutVal.equals("abc") || timeoutVal.equals("-1")) { - // Timeout should not occur - hreq = HttpRequest.newBuilder(noTimeoutUri).version(HTTP_2).GET().build(); - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); - } - } else { - // When no value is specified then no timeout should occur (default keep alive value of 600 used) - hreq = HttpRequest.newBuilder(noTimeoutUri).version(HTTP_2).GET().build(); - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent should not occur, no value was specified for this property"); - } - } - - private HttpResponse runRequest(HttpClient hc, HttpRequest req, int sleepTime) throws InterruptedException { - CompletableFuture> request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); - HttpResponse hresp = request.join(); - assertEquals(hresp.statusCode(), 200); - - Thread.sleep(sleepTime); - request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); - return request.join(); - } - - static class ServerTimeoutHandler implements Http2Handler { - - volatile Object firstConnection = null; - - @Override - public void handle(Http2TestExchange exchange) throws IOException { - if (exchange instanceof TestExchangeSupplier exch) { - if (firstConnection == null) { - firstConnection = exch.getTestConnection(); - exch.sendResponseHeaders(200, 0); - } else { - var secondConnection = exch.getTestConnection(); - - if (firstConnection != secondConnection) { - testLog.println("ServerTimeoutHandler: New Connection was used, idleConnectionTimeoutEvent fired." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(200, 0); - } else { - testLog.println("ServerTimeoutHandler: Same Connection was used, idleConnectionTimeoutEvent did not fire." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(400, 0); - } - } - } - } - } - - static class ServerNoTimeoutHandler implements Http2Handler { - - volatile Object firstConnection = null; - - @Override - public void handle(Http2TestExchange exchange) throws IOException { - if (exchange instanceof TestExchangeSupplier exch) { - if (firstConnection == null) { - firstConnection = exch.getTestConnection(); - exch.sendResponseHeaders(200, 0); - } else { - var secondConnection = exch.getTestConnection(); - - if (firstConnection == secondConnection) { - testLog.println("ServerTimeoutHandler: Same Connection was used, idleConnectionTimeoutEvent did not fire." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(200, 0); - } else { - testLog.println("ServerTimeoutHandler: Different Connection was used, idleConnectionTimeoutEvent fired." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(400, 0); - } - } - } - } - } - - static class TestExchangeSupplier extends Http2TestExchangeImpl { - - public TestExchangeSupplier(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { - super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed); - } - - public Http2TestServerConnection getTestConnection() { - return this.conn; - } - } -} \ No newline at end of file diff --git a/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java b/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java index 0f4b204fda8..907afc28fa6 100644 --- a/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java +++ b/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.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 @@ -56,8 +56,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; * @summary verify that the HttpClient's HTTP2 idle connection management doesn't close a connection * when that connection has been handed out from the pool to a caller * @library /test/jdk/java/net/httpclient/lib + * /test/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters - * + * jdk.test.lib.Asserts * @run junit/othervm -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.keepalive.timeout.h2=3 * IdlePooledConnectionTest diff --git a/test/jdk/java/net/httpclient/http2/ProxyTest2.java b/test/jdk/java/net/httpclient/http2/ProxyTest2.java index 733c21ffe68..2fdb1b360e1 100644 --- a/test/jdk/java/net/httpclient/http2/ProxyTest2.java +++ b/test/jdk/java/net/httpclient/http2/ProxyTest2.java @@ -21,20 +21,15 @@ * questions. */ -import com.sun.net.httpserver.HttpContext; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsParameters; -import com.sun.net.httpserver.HttpsServer; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; -import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; @@ -42,9 +37,7 @@ import java.net.ProxySelector; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; @@ -72,11 +65,6 @@ public class ProxyTest2 { static { try { - HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - return true; - } - }); SSLContext.setDefault(new SimpleSSLContext().get()); } catch (IOException ex) { throw new ExceptionInInitializerError(ex); @@ -336,15 +324,4 @@ public class ProxyTest2 { } - static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { - super(ctx); - } - - @Override - public void configure (HttpsParameters params) { - params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); - } - } - } diff --git a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java index 1f9340c5c08..e9c6447e600 100644 --- a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java +++ b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.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 @@ -233,18 +233,18 @@ public class PushPromiseContinuation { @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream content) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) { HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder(); headersBuilder.setHeader(":method", "GET"); headersBuilder.setHeader(":scheme", uri.getScheme()); headersBuilder.setHeader(":authority", uri.getAuthority()); headersBuilder.setHeader(":path", uri.getPath()); - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) headersBuilder.addHeader(entry.getKey(), value); } HttpHeaders combinedHeaders = headersBuilder.build(); - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content); // Indicates to the client that a continuation should be expected pp.setFlag(0x0); try { @@ -292,7 +292,7 @@ public class PushPromiseContinuation { } @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream content) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) { pushPromiseHeadersBuilder = new HttpHeadersBuilder(); testHeadersBuilder = new HttpHeadersBuilder(); cfs = new ArrayList<>(); @@ -301,7 +301,7 @@ public class PushPromiseContinuation { setPushHeaders(":scheme", uri.getScheme()); setPushHeaders(":authority", uri.getAuthority()); setPushHeaders(":path", uri.getPath()); - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) { setPushHeaders(entry.getKey(), value); } @@ -318,7 +318,7 @@ public class PushPromiseContinuation { HttpHeaders pushPromiseHeaders = pushPromiseHeadersBuilder.build(); testHeaders = testHeadersBuilder.build(); // Create the Push Promise Frame - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, content, cfs); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, rspHeaders, content, cfs); // Indicates to the client that a continuation should be expected pp.setFlag(0x0); diff --git a/test/jdk/java/net/httpclient/http2/RedirectTest.java b/test/jdk/java/net/httpclient/http2/RedirectTest.java index 2d7701c5e37..1d7b894bc40 100644 --- a/test/jdk/java/net/httpclient/http2/RedirectTest.java +++ b/test/jdk/java/net/httpclient/http2/RedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, 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 @@ -25,9 +25,12 @@ * @test * @bug 8156514 * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @build jdk.httpclient.test.lib.http2.Http2TestExchange + * jdk.httpclient.test.lib.http2.Http2TestServer * jdk.httpclient.test.lib.http2.Http2EchoHandler * jdk.httpclient.test.lib.http2.Http2RedirectHandler + * jdk.test.lib.Asserts + * jdk.test.lib.net.SimpleSSLContext * @run testng/othervm * -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors * -Djdk.internal.httpclient.debug=true @@ -47,7 +50,6 @@ import java.util.Arrays; import java.util.Iterator; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.Http2EchoHandler; import jdk.httpclient.test.lib.http2.Http2RedirectHandler; import org.testng.annotations.Test; diff --git a/test/jdk/java/net/httpclient/http2/SimpleGet.java b/test/jdk/java/net/httpclient/http2/SimpleGet.java new file mode 100644 index 00000000000..8a65292a65d --- /dev/null +++ b/test/jdk/java/net/httpclient/http2/SimpleGet.java @@ -0,0 +1,225 @@ +/* + * 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 + * 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 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm -XX:+CrashOnOutOfMemoryError SimpleGet + * @run testng/othervm -XX:+CrashOnOutOfMemoryError + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * SimpleGet + * @run testng/othervm -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.redirects.retrylimit=5 + * -Djdk.httpclient.HttpClient.log=errors + * -XX:+CrashOnOutOfMemoryError + * -XX:+HeapDumpOnOutOfMemoryError + * SimpleGet + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; + +public class SimpleGet implements HttpServerAdapters { + static HttpTestServer httpsServer; + static HttpClient client = null; + static SSLContext sslContext; + static String httpsURIString; + static ExecutorService serverExec = Executors.newVirtualThreadPerTaskExecutor(); + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + httpsServer = HttpTestServer.create(HTTP_2, sslContext, serverExec); + httpsServer.addHandler(new TestHandler(), "/"); + httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + + httpsServer.start(); + warmup(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + private static void warmup() throws Exception { + SimpleSSLContext sslct = new SimpleSSLContext(); + var sslContext = sslct.get(); + + // warmup server + try (var client2 = createClient(sslContext)) { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_2) + .HEAD().build(); + client2.send(request, BodyHandlers.discarding()); + } + + // warmup client + var httpsServer2 = HttpTestServer.create(HTTP_2, sslContext, + Executors.newVirtualThreadPerTaskExecutor()); + httpsServer2.addHandler(new TestHandler(), "/"); + var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/"; + httpsServer2.start(); + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2)) + .version(HTTP_2) + .HEAD().build(); + client.send(request, BodyHandlers.discarding()); + } finally { + httpsServer2.stop(); + } + } + + public static void main(String[] args) throws Exception { + test(); + } + + @Test + public static void test() throws Exception { + try { + long prestart = System.nanoTime(); + initialize(); + long done = System.nanoTime(); + System.out.println("Stat: Initialization and warmup took " + TimeUnit.NANOSECONDS.toMillis(done - prestart) + " millis"); + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_2) + .GET().build(); + long start = System.nanoTime(); + var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b -> {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("Stat: First request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = property("simpleget.requests", 50); + ; + List>> list = new ArrayList<>(max); + Set connections = new ConcurrentSkipListSet<>(); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b -> {})) + .whenComplete((r, t) -> Optional.ofNullable(r) + .flatMap(HttpResponse::connectionLabel) + .ifPresent(connections::add)); + list.add(cf); + //cf.get(); // uncomment to test with serial instead of concurrent requests + } + try { + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + } finally { + long elapsed2 = System.nanoTime() - start2; + long completed = list.stream().filter(CompletableFuture::isDone) + .filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count(); + connections.forEach(System.out::println); + if (completed > 0) { + System.out.println("Stat: Next " + completed + " requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): " + + elapsed2 / completed + " nanos per request (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + " ms) on " + + connections.size() + " connections"); + } + } + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext) { + return HttpClient.newBuilder() + .sslContext(sslContext) + .version(HTTP_2) + .proxy(Builder.NO_PROXY) + .executor(Executors.newVirtualThreadPerTaskExecutor()) + .build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext); + } + return client; + } + + static int property(String name, int defaultValue) { + return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); + } + + // 32 * 32 * 1024 * 10 chars = 10Mb responses + // 50 requests => 500Mb + // 100 requests => 1Gb + // 1000 requests => 10Gb + private final static int REPEAT = property("simpleget.repeat", 32); + private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32)); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + try (var in = t.getRequestBody()) { + byte[] input = in.readAllBytes(); + t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT); + try (var out = t.getResponseBody()) { + if (t.getRequestMethod().equals("HEAD")) return; + for (int i=0; i> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) headersBuilder.addHeader(entry.getKey(), value); } HttpHeaders combinedHeaders = headersBuilder.build(); - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content); pp.setFlag(HeaderFrame.END_HEADERS); try { @@ -311,7 +309,7 @@ public class TrailingHeadersTest { static final BiPredicate ACCEPT_ALL = (x, y) -> true; - private void pushPromise(Http2TestExchange exchange) { + private void pushPromise(Http2TestExchange exchange) throws IOException { URI requestURI = exchange.getRequestURI(); URI uri = requestURI.resolve("/promise"); InputStream is = new ByteArrayInputStream("Sample_Push_Data".getBytes(UTF_8)); diff --git a/test/jdk/java/net/httpclient/http2/UserInfoTest.java b/test/jdk/java/net/httpclient/http2/UserInfoTest.java index 7dafda0c1f8..69cded67297 100644 --- a/test/jdk/java/net/httpclient/http2/UserInfoTest.java +++ b/test/jdk/java/net/httpclient/http2/UserInfoTest.java @@ -32,6 +32,7 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import jdk.httpclient.test.lib.http2.Http2TestServer; @@ -46,13 +47,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; * @test * @bug 8292876 * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestExchange + * @compile ../ReferenceTracker.java * @run junit UserInfoTest */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class UserInfoTest { + static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; Http2TestServer server; int port; SSLContext sslContext; @@ -96,6 +101,7 @@ public class UserInfoTest { .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); + TRACKER.track(client); URI uri = URIBuilder.newBuilder() .scheme("https") @@ -112,5 +118,10 @@ public class UserInfoTest { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode(), "Test Failed : " + response.uri().getAuthority()); + + client = null; + System.gc(); + var error = TRACKER.check(500); + if (error != null) throw error; } } diff --git a/test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java b/test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java new file mode 100644 index 00000000000..abe6889e497 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java @@ -0,0 +1,119 @@ +/* + * 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. + * + * 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 8157105 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all BadCipherSuiteErrorTest + * @summary check exception thrown when bad TLS parameters selected + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; + +import jdk.test.lib.Asserts; +import jdk.test.lib.net.SimpleSSLContext; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +import org.testng.annotations.Test; + +/** + * When selecting an unacceptable cipher suite the TLS handshake will fail. + * But, the exception that was thrown was not being returned up to application + * causing hang problems + */ +public class BadCipherSuiteErrorTest implements HttpServerAdapters { + + static final String[] CIPHER_SUITES = new String[]{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" }; + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + //@Test(timeOut=5000) + @Test + public void test() throws Exception { + SSLContext sslContext = (new SimpleSSLContext()).get(); + ExecutorService exec = Executors.newCachedThreadPool(); + var builder = newClientBuilderForH3() + .executor(exec) + .sslContext(sslContext) + .version(HTTP_3); + var goodclient = builder.build(); + var badclient = builder + .sslParameters(new SSLParameters(CIPHER_SUITES)) + .build(); + + + + HttpTestServer httpsServer = null; + try { + SSLContext serverContext = (new SimpleSSLContext()).get(); + SSLParameters p = serverContext.getSupportedSSLParameters(); + p.setApplicationProtocols(new String[]{"h3"}); + httpsServer = HttpTestServer.create(HTTP_3_URI_ONLY, serverContext); + httpsServer.addHandler(new HttpTestEchoHandler(), "/"); + String httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + System.out.println("HTTP/3 Server started on: " + httpsServer.serverAuthority()); + + httpsServer.start(); + URI uri = URI.create(httpsURIString); + + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + + System.out.println("Sending request with good client to " + uri); + HttpResponse response = goodclient.send(req, BodyHandlers.ofString()); + Asserts.assertEquals(response.statusCode(), 200); + Asserts.assertEquals(response.version(), HTTP_3); + Asserts.assertEquals(response.body(), SIMPLE_STRING); + System.out.println("Expected response successfully received"); + try { + System.out.println("Sending request with bad client to " + uri); + response = badclient.send(req, BodyHandlers.discarding()); + throw new RuntimeException("Unexpected response: " + response); + } catch (IOException e) { + System.out.println("Caught Expected IOException: " + e); + } + System.out.println("DONE"); + } finally { + if (httpsServer != null ) { httpsServer.stop(); } + exec.close(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/FramesDecoderTest.java b/test/jdk/java/net/httpclient/http3/FramesDecoderTest.java new file mode 100644 index 00000000000..d406ee491cb --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/FramesDecoderTest.java @@ -0,0 +1,227 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.http3 + * @modules java.net.http/jdk.internal.net.http.http3.frames + * @modules java.net.http/jdk.internal.net.http.quic.streams + * @run junit/othervm FramesDecoderTest + * @summary Tests to check HTTP3 methods decode frames correctly + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class FramesDecoderTest { + // frames with arbitrary content, interpreted as PartialFrames + byte[][] vlframes() { + return new byte[][]{ + {0, 2, 0, 0}, // DATA frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // DATA frame, 8-byte VL encoding, 2 bytes = 0,0 + {1, 2, 0, 0}, // HEADERS frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 1, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // HEADERS frame, 8-byte VL encoding, 2 bytes = 0,0 + {5, 3, 0, 0, 0}, // PUSH_PROMISE frame, Push ID = 0, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 10, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // PUSH_PROMISE frame, 8-byte VL encoding, Push ID = 0, 2 bytes = 0,0 + {33, 2, 0, 0}, // RESERVED frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 33, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // RESERVED frame, 8-byte VL encoding, 2 bytes = 0,0 + {32, 2, 0, 0}, // UNKNOWN frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 32, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // UNKNOWN frame, 8-byte VL encoding, 2 bytes = 0,0 + }; + } + + // frames with predefined content, correct + byte[][] fixedframes() { + return new byte[][]{ + {3, 1, 0}, // CANCEL_PUSH frame, Push ID = 0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 3, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 8, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // CANCEL_PUSH frame, 8-byte VL encoding, Push ID = 0 + {7, 1, 0}, // GOAWAY frame, Push ID = 0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 8, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // GOAWAY frame, 8-byte VL encoding, Push ID = 0 + {13, 1, 0}, // MAX_PUSH_ID frame, Push ID = 0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 13, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 8, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // MAX_PUSH_ID frame, 8-byte VL encoding, Push ID = 0 + {4, 0}, // SETTINGS frame, empty + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 4, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // SETTINGS frame, 8-byte VL encoding, empty + {4, 2, 31, 0}, // SETTINGS frame, 31(reserved)->0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 4, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 16, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 33, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // SETTINGS frame, 8-byte VL encoding, 33(reserved)->0 + {4, 3, 0x40, 33, 0}, // SETTINGS frame, 33(reserved)->0 + }; + } + + // incorrect frames + byte[][] badframes() { + return new byte[][]{ + {3, 2, 0, 0}, // CANCEL_PUSH frame, Push ID = 0, extra byte + {3, 2, (byte) 0xC0, 0}, // CANCEL_PUSH frame, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 3, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0x80, 0, 0, 0, 0}, // CANCEL_PUSH frame, 8-byte VL encoding, Push ID = 0, extra byte + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 3, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0}, // CANCEL_PUSH frame, 8-byte VL encoding, Push ID = truncated VL + {7, 2, 0, 0}, // GOAWAY frame, Push ID = 0, extra byte + {7, 2, (byte) 0xC0, 0}, // GOAWAY frame, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0x80, 0, 0, 0, 0}, // GOAWAY frame, 8-byte VL encoding, Push ID = 0, extra byte + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0}, // GOAWAY frame, 8-byte VL encoding, Push ID = truncated VL + {13, 2, 0, 0}, // MAX_PUSH_ID frame, Push ID = 0, extra byte + {13, 2, (byte) 0xC0, 0}, // MAX_PUSH_ID frame, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 13, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0x80, 0, 0, 0, 0}, // MAX_PUSH_ID frame, 8-byte VL encoding, Push ID = 0, extra byte + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 13, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0}, // MAX_PUSH_ID frame, 8-byte VL encoding, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 1, (byte) 0xC0 }, // PUSH_PROMISE frame, 8-byte VL encoding, Push ID = truncated VL + {4, 5, 33, 0, 0x41, 0, 0x40}, // SETTINGS frame, 33(reserved)->0, 64 -> truncated VL + {4, 4, 33, 0, 0x41, 0}, // SETTINGS frame, 33(reserved)->0, 64 -> ? + {4, 3, 33, 0, 0x41}, // SETTINGS frame, 33(reserved)->0, truncated VL + {4, 2, 33, 0x40}, // SETTINGS frame, 33(reserved)-> truncated VL + {4, 1, 33}, // SETTINGS frame, 33(reserved)->? + }; + } + + private static int bufLength(List bufs) { + return bufs.stream().mapToInt(ByteBuffer::remaining).sum(); + } + + @ParameterizedTest + @MethodSource("vlframes") + public void testFullVLFrames(byte[] frame) { + // offer the entire frame at once + FramesDecoder fd = new FramesDecoder("test"); + fd.submit(ByteBuffer.wrap(frame)); + fd.submit(QuicStreamReader.EOF); + Http3Frame h3frame = fd.poll(); + assertEquals(2, h3frame.streamingLength()); + assertEquals(h3frame, fd.poll()); + assertFalse(fd.eof()); + List bufs = fd.readPayloadBytes(); + assertEquals(2, bufLength(bufs)); + assertNull(fd.poll()); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("vlframes") + public void testSplitVLFrames(byte[] frame) { + // offer the frame one byte at a time + FramesDecoder fd = new FramesDecoder("test"); + ByteBuffer buffer = ByteBuffer.wrap(frame); + for (int i = 1; i <= frame.length; i++) { + buffer.position(i-1); + buffer.limit(i); + fd.submit(buffer.asReadOnlyBuffer()); + if (i < frame.length - 2) { + assertNull(fd.poll()); + } else { + Http3Frame h3frame = fd.poll(); + assertEquals(2, h3frame.streamingLength()); + } + } + Http3Frame h3frame = fd.poll(); + assertEquals(2, h3frame.streamingLength()); + assertEquals(h3frame, fd.poll()); + assertFalse(fd.eof()); + List bufs = fd.readPayloadBytes(); + assertEquals(2, bufLength(bufs)); + assertNull(fd.poll()); + assertFalse(fd.eof()); + fd.submit(QuicStreamReader.EOF); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("fixedframes") + public void testFullGoodFrames(byte[] frame) { + // offer the entire frame at once + FramesDecoder fd = new FramesDecoder("test"); + fd.submit(ByteBuffer.wrap(frame)); + fd.submit(QuicStreamReader.EOF); + Http3Frame h3frame = fd.poll(); + assertEquals(0, h3frame.streamingLength()); + assertTrue(fd.eof()); + List bufs = fd.readPayloadBytes(); + assertNull(bufs); + assertNull(fd.poll()); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("fixedframes") + public void testSplitGoodFrames(byte[] frame) { + // offer the frame one byte at a time + FramesDecoder fd = new FramesDecoder("test"); + ByteBuffer buffer = ByteBuffer.wrap(frame); + for (int i = 1; i <= frame.length; i++) { + buffer.position(i-1); + buffer.limit(i); + fd.submit(buffer.asReadOnlyBuffer()); + if (i < frame.length) { + assertNull(fd.poll()); + } else { + Http3Frame h3frame = fd.poll(); + assertEquals(0, h3frame.streamingLength()); + } + } + assertNull(fd.poll()); + assertFalse(fd.eof()); + fd.submit(QuicStreamReader.EOF); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("badframes") + public void testFullBadFrames(byte[] frame) { + // offer the entire frame at once + FramesDecoder fd = new FramesDecoder("test"); + fd.submit(ByteBuffer.wrap(frame)); + fd.submit(QuicStreamReader.EOF); + Http3Frame h3frame = fd.poll(); + assertInstanceOf(MalformedFrame.class, h3frame); + assertEquals(Http3Error.H3_FRAME_ERROR.code(), ((MalformedFrame)h3frame).getErrorCode()); + } + + @ParameterizedTest + @MethodSource("badframes") + public void testSplitBadFrames(byte[] frame) { + // offer the frame one byte at a time + FramesDecoder fd = new FramesDecoder("test"); + ByteBuffer buffer = ByteBuffer.wrap(frame); + for (int i = 1; i <= frame.length; i++) { + buffer.position(i-1); + buffer.limit(i); + fd.submit(buffer.asReadOnlyBuffer()); + if (i < frame.length) { + assertNull(fd.poll()); + } else { + Http3Frame h3frame = fd.poll(); + assertInstanceOf(MalformedFrame.class, h3frame); + assertEquals(Http3Error.H3_FRAME_ERROR.code(), ((MalformedFrame)h3frame).getErrorCode()); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java b/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java new file mode 100644 index 00000000000..a67710c485f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java @@ -0,0 +1,476 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +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.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +import static java.lang.System.out; + + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm/timeout=60 -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * GetHTTP3Test + * @summary Basic HTTP/3 GET test + */ +// -Djdk.httpclient.http3.maxDirectConnectionTimeout=2500 +public class GetHTTP3Test implements HttpServerAdapters { + + // The response body + static final String BODY = """ + May the road rise up to meet you. + May the wind be always at your back. + May the sun shine warm upon your face; + """; + + SSLContext sslContext; + HttpTestServer h3TestServer; // HTTP/2 ( h2 + h3) + String h3URI; + + static final int ITERATION_COUNT = 4; + // a shared executor helps reduce the amount of threads created by the test + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + final Set sharedClientHasH3 = ConcurrentHashMap.newKeySet(); + private volatile HttpClient sharedClient; + private boolean directQuicConnectionSupported; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + final void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + h3URI, + }; + } + + @DataProvider(name = "variants") + public Object[][] variants(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + String[] uris = uris(); + Object[][] result = new Object[uris.length * 2 * 2 * 2][]; + int i = 0; + for (var version : List.of(Optional.empty(), Optional.of(HTTP_3))) { + for (Version firstRequestVersion : List.of(HTTP_2, HTTP_3)) { + for (boolean sameClient : List.of(false, true)) { + for (String uri : uris()) { + result[i++] = new Object[]{uri, firstRequestVersion, sameClient, version}; + } + } + } + } + assert i == result.length; + return result; + } + + @DataProvider(name = "uris") + public Object[][] uris(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + + @Test(dataProvider = "variants") + public void testAsync(String uri, Version firstRequestVersion, boolean sameClient, Optional version) throws Exception { + System.out.println("Request to " + uri +"/Async/*" + + ", firstRequestVersion=" + firstRequestVersion + + ", sameclient=" + sameClient + ", version=" + version); + + HttpClient client = newHttpClient(sameClient); + final URI headURI = URI.create(uri + "/Async/First/HEAD"); + final Builder headBuilder = HttpRequest.newBuilder(headURI) + .version(firstRequestVersion) + .HEAD(); + Http3DiscoveryMode config = null; + if (firstRequestVersion == HTTP_3 && !directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + config = ALT_SVC; + } + if (config != null) { + out.println("first request will use " + config); + headBuilder.setOption(H3_DISCOVERY, config); + config = null; + } + + HttpResponse response1 = client.send(headBuilder.build(), BodyHandlers.ofString()); + assertEquals(response1.statusCode(), 200, "Unexpected first response code"); + assertEquals(response1.body(), "", "Unexpected first response body"); + boolean expectH3 = sameClient && sharedClientHasH3.contains(headURI.getRawAuthority()); + if (firstRequestVersion == HTTP_3) { + if (expectH3) { + out.println("Expecting HEAD response over HTTP_3"); + assertEquals(response1.version(), HTTP_3, "Unexpected first response version"); + } + } else { + out.println("Expecting HEAD response over HTTP_2"); + assertEquals(response1.version(), HTTP_2, "Unexpected first response version"); + } + out.println("HEAD response version: " + response1.version()); + if (response1.version() == HTTP_2) { + if (sameClient) { + sharedClientHasH3.add(headURI.getRawAuthority()); + } + expectH3 = version.isEmpty() && client.version() == HTTP_3; + if (version.orElse(null) == HTTP_3 && !directQuicConnectionSupported) { + config = ALT_SVC; + expectH3 = true; + } + // we can expect H3 only if the (default) config is not ANY + if (expectH3) { + out.println("first response came over HTTP/2, so we should expect all responses over HTTP/3"); + } + } else if (response1.version() == HTTP_3) { + expectH3 = directQuicConnectionSupported && version.orElse(null) == HTTP_3; + if (expectH3) { + out.println("first response came over HTTP/3, direct connection supported: expect HTTP/3"); + } else if (firstRequestVersion == HTTP_3 && version.isEmpty() + && config == null && directQuicConnectionSupported) { + config = ANY; + expectH3 = true; + } + } + out.printf("request version: %s, directConnectionSupported: %s, first response: %s," + + " config: %s, expectH3: %s%n", + version, directQuicConnectionSupported, response1.version(), config, expectH3); + if (expectH3) { + out.println("All responses should now come through HTTP/3"); + } + + Builder builder = HttpRequest.newBuilder() + .GET(); + version.ifPresent(builder::version); + if (config != null) { + builder.setOption(H3_DISCOVERY, config); + } + Map>> responses = new HashMap<>(); + for (int i = 0; i < ITERATION_COUNT; i++) { + HttpRequest request = builder.uri(URI.create(uri+"/Async/GET/"+i)).build(); + System.out.println("Iteration: " + request.uri()); + responses.put(request.uri(), client.sendAsync(request, BodyHandlers.ofString())); + } + int h3Count = 0; + while (!responses.isEmpty()) { + CompletableFuture.anyOf(responses.values().toArray(CompletableFuture[]::new)).join(); + var done = responses.entrySet().stream() + .filter((e) -> e.getValue().isDone()).toList(); + for (var e : done) { + URI u = e.getKey(); + responses.remove(u); + out.println("Checking response: " + u); + var response = e.getValue().get(); + out.println("Response is: " + response + ", [version: " + response.version() + "]"); + assertEquals(response.statusCode(), 200,"status for " + u); + assertEquals(response.body(), BODY,"body for " + u); + if (expectH3) { + assertEquals(response.version(), HTTP_3, "version for " + u); + } + if (response.version() == HTTP_3) { + h3Count++; + } + } + } + if (client.version() == HTTP_3 || version.orElse(null) == HTTP_3) { + if (h3Count == 0) { + throw new AssertionError("No request used HTTP/3"); + } + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + System.out.println("test: DONE"); + } + + @Test(dataProvider = "uris") + public void testSync(String h3URI) throws Exception { + HttpClient client = makeNewClient(); + Builder builder = HttpRequest.newBuilder(URI.create(h3URI + "/Sync/GET/1")) + .version(HTTP_3) + .GET(); + if (!directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + builder.setOption(H3_DISCOVERY, ALT_SVC); + } + + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + if (directQuicConnectionSupported) { + // TODO unreliable assertion + //assertEquals(response.version(), HTTP_3, "Unexpected first response version"); + } else { + assertEquals(response.version(), HTTP_2, "Unexpected first response version"); + } + assertEquals(response.body(), BODY, "first response body"); + + request = builder.uri(URI.create(h3URI + "/Sync/GET/2")).build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_3, "second response version"); + assertEquals(response.body(), BODY, "second response body"); + + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + final Http2TestServer h2WithAltService = new Http2TestServer("localhost", true, + sslContext).enableH3AltServiceOnSamePort(); + h3TestServer = HttpTestServer.of(h2WithAltService); + h3TestServer.addHandler(new Handler(), "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/GET"; + serverCount.addAndGet(1); + h3TestServer.start(); + directQuicConnectionSupported = h2WithAltService.supportsH3DirectConnection(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + h3TestServer.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Server sending %d - %s length\n", 200, BODY.length()); + t.sendResponseHeaders(200, BODY.length()); + } + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + assertEquals(is.readAllBytes().length, 0); + if (!"HEAD".equals(t.getRequestMethod())) { + String[] body = BODY.split("\n"); + for (String line : body) { + os.write(line.getBytes(StandardCharsets.UTF_8)); + os.write('\n'); + os.flush(); + } + } + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java b/test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java new file mode 100644 index 00000000000..3bc59a4d67f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java @@ -0,0 +1,330 @@ +/* + * 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 + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.internal.httpclient.debug=true H3BadHeadersTest + * @summary this test verifies the behaviour of the HttpClient when presented + * with bad headers + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; + +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.util.List.of; +import static java.util.Map.entry; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +public class H3BadHeadersTest implements HttpServerAdapters { + + private static final List>> BAD_HEADERS = of( + of(entry(":status", "200"), entry(":hello", "GET")), // Unknown pseudo-header + of(entry(":status", "200"), entry("hell o", "value")), // Space in the name + of(entry(":status", "200"), entry("hello", "line1\r\n line2\r\n")), // Multiline value + of(entry(":status", "200"), entry("hello", "DE" + ((char) 0x7F) + "L")) // Bad byte in value + // Not easily testable with H3, because we use a HttpHeadersBuilders which sorts headers... + // of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one + ); + + static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + + SSLContext sslContext; + HttpTestServer http3TestServer; // HTTP/3 ( h3 only ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 + h3 ) + String http3URI; + String https2URI; + + + @DataProvider(name = "variants") + public Object[][] variants() { + return new Object[][] { + { http3URI, false}, + { https2URI, false}, + { http3URI, true}, + { https2URI, true}, + }; + } + + + @Test(dataProvider = "variants") + void test(String uri, + boolean sameClient) + throws Exception + { + System.out.printf("%ntest %s, %s, STARTING%n%n", uri, sameClient); + System.err.printf("%ntest %s, %s, STARTING%n%n", uri, sameClient); + var config = uri.startsWith(http3URI) + ? Http3DiscoveryMode.HTTP_3_URI_ONLY + : https2TestServer.supportsH3DirectConnection() + ? Http3DiscoveryMode.ANY + : Http3DiscoveryMode.ALT_SVC; + + boolean sendHeadRequest = config != Http3DiscoveryMode.HTTP_3_URI_ONLY; + + HttpClient client = null; + for (int i=0; i< BAD_HEADERS.size(); i++) { + boolean needsHeadRequest = false; + if (!sameClient || client == null) { + needsHeadRequest = sendHeadRequest; + client = newClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(sslContext) + .build(); + } + + if (needsHeadRequest) { + URI simpleURI = URI.create(uri); + HttpRequest head = HttpRequest.newBuilder(simpleURI) + .version(Version.HTTP_2) + .HEAD().setOption(H3_DISCOVERY, config).build(); + System.out.println("\nSending HEAD request: " + head); + var headResponse = client.send(head, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i); + HttpRequest request = HttpRequest.newBuilder(uriWithQuery) + .POST(BodyPublishers.ofString("Hello there!")) + .setOption(H3_DISCOVERY, config) + .version(Version.HTTP_3) + .build(); + System.out.println("\nSending request:" + uriWithQuery); + try { + HttpResponse response = client.send(request, BodyHandlers.ofString()); + fail("Expected exception, got :" + response + ", " + response.body()); + } catch (IOException ioe) { + System.out.println("Got EXPECTED: " + ioe); + assertDetailMessage(ioe, i); + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + } + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + System.out.printf("%ntest %s, %s, DONE%n%n", uri, sameClient); + System.err.printf("%ntest %s, %s, DONE%n%n", uri, sameClient); + } + + @Test(dataProvider = "variants") + void testAsync(String uri, + boolean sameClient) throws Exception + { + + System.out.printf("%ntestAsync %s, %s, STARTING%n%n", uri, sameClient); + System.err.printf("%ntestAsync %s, %s, STARTING%n%n", uri, sameClient); + var config = uri.startsWith(http3URI) + ? Http3DiscoveryMode.HTTP_3_URI_ONLY + : https2TestServer.supportsH3DirectConnection() + ? Http3DiscoveryMode.ANY + : Http3DiscoveryMode.ALT_SVC; + + boolean sendHeadRequest = config != Http3DiscoveryMode.HTTP_3_URI_ONLY; + + HttpClient client = null; + for (int i=0; i< BAD_HEADERS.size(); i++) { + boolean needsHeadRequest = false; + if (!sameClient || client == null) { + needsHeadRequest = sendHeadRequest; + client = newClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(sslContext) + .build(); + } + + if (needsHeadRequest) { + URI simpleURI = URI.create(uri); + HttpRequest head = HttpRequest.newBuilder(simpleURI) + .version(Version.HTTP_2) + .HEAD() + .setOption(H3_DISCOVERY, config) + .build(); + System.out.println("\nSending HEAD request: " + head); + + var headResponse = client.send(head, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i); + HttpRequest request = HttpRequest.newBuilder(uriWithQuery) + .POST(BodyPublishers.ofString("Hello there!")) + .setOption(H3_DISCOVERY, config) + .version(Version.HTTP_3) + .build(); + System.out.println("\nSending request:" + uriWithQuery); + + Throwable t = null; + try { + HttpResponse response = client.sendAsync(request, BodyHandlers.ofString()).get(); + fail("Expected exception, got :" + response + ", " + response.body()); + } catch (Throwable t0) { + System.out.println("Got EXPECTED: " + t0); + if (t0 instanceof ExecutionException) { + t0 = t0.getCause(); + } + t = t0; + } + assertDetailMessage(t, i); + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + } + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + System.out.printf("%ntestAsync %s, %s, DONE%n%n", uri, sameClient); + System.err.printf("%ntestAsync %s, %s, DONE%n%n", uri, sameClient); + } + + // Assertions based on implementation specific detail messages. Keep in + // sync with implementation. + static void assertDetailMessage(Throwable throwable, int iterationIndex) { + try { + assertTrue(throwable instanceof IOException, + "Expected IOException, got, " + throwable); + assertNotNull(throwable.getMessage(), "No message for " + throwable); + assertTrue(throwable.getMessage().contains("malformed response"), + "Expected \"malformed response\" in: " + throwable.getMessage()); + + if (iterationIndex == 0) { // unknown + assertTrue(throwable.getMessage().contains("Unknown pseudo-header"), + "Expected \"Unknown pseudo-header\" in: " + throwable.getMessage()); + } else if (iterationIndex == 4) { // unexpected + assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"), + "Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage()); + } else { + assertTrue(throwable.getMessage().contains("Bad header"), + "Expected \"Bad header\" in: " + throwable.getMessage()); + } + } catch (AssertionError e) { + System.out.println("Exception does not match expectation: " + throwable); + throwable.printStackTrace(System.out); + throw e; + } + } + + @BeforeTest + public void setup() throws Exception { + System.out.println("creating servers"); + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new BadHeadersHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + + https2TestServer = HttpTestServer.create(Http3DiscoveryMode.ANY, sslContext); + https2TestServer.addHandler(new BadHeadersHandler(), "/https2/echo"); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + + http3TestServer.start(); + https2TestServer.start(); + System.out.println("server started"); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("\n\n**** stopping servers\n"); + System.out.println("stopping servers"); + http3TestServer.stop(); + https2TestServer.stop(); + System.out.println("servers stopped"); + } + + static class BadHeadersHandler implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange t) throws IOException { + var uri = t.getRequestURI(); + String query = uri.getRawQuery(); + if (query != null && !query.isEmpty()) { + int badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1)); + assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() : + "Unexpected badHeadersIndex value: " + badHeadersIndex; + List> headers = BAD_HEADERS.get(badHeadersIndex); + var responseHeaders = t.getResponseHeaders(); + for (var e : headers) { + responseHeaders.addHeader(e.getKey(), e.getValue()); + } + } + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + byte[] bytes = is.readAllBytes(); + t.sendResponseHeaders(200, bytes.length); + if (t.getRequestMethod().equals("HEAD")) { + os.close(); + } else { + os.write(bytes); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3BasicTest.java b/test/jdk/java/net/httpclient/http3/H3BasicTest.java new file mode 100644 index 00000000000..b99520ab830 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3BasicTest.java @@ -0,0 +1,405 @@ +/* + * 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 + * 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 + * @key randomness + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @compile ../ReferenceTracker.java + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors + * -Djdk.internal.httpclient.debug=true + * H3BasicTest + */ +// -Dseed=-163464189156654174 + +import java.io.IOException; +import java.net.*; +import javax.net.ssl.*; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.file.*; +import java.util.Random; +import java.util.concurrent.*; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2EchoHandler; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.RandomFactory; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +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; +import static jdk.test.lib.Asserts.assertFileContentsEqual; +import static jdk.test.lib.Utils.createTempFile; +import static jdk.test.lib.Utils.createTempFileOfSize; + +public class H3BasicTest implements HttpServerAdapters { + + private static final Random RANDOM = RandomFactory.getRandom(); + + private static final String CLASS_NAME = H3BasicTest.class.getSimpleName(); + + static int http3Port, https2Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static HttpClient client = null; + static ExecutorService clientExec; + static ExecutorService serverExec; + static SSLContext sslContext; + static volatile String http3URIString, pingURIString, https2URIString; + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + // server that only supports HTTP/3 + http3OnlyServer = new Http3TestServer(sslContext, serverExec); + http3OnlyServer.addHandler("/", new Http2EchoHandler()); + http3OnlyServer.addHandler("/ping", new EchoWithPingHandler()); + http3Port = http3OnlyServer.getAddress().getPort(); + System.out.println("HTTP/3 server started at localhost:" + http3Port); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, 0, serverExec, sslContext); + if (RANDOM.nextBoolean()) { + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } else { + https2AltSvcServer.enableH3AltServiceOnSamePort(); + } + https2AltSvcServer.addHandler(new Http2EchoHandler(), "/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + if (https2AltSvcServer.supportsH3DirectConnection()) { + System.out.println("HTTP/2 server (same HTTP/3 origin) started at localhost:" + https2Port); + } else { + System.out.println("HTTP/2 server (different HTTP/3 origin) started at localhost:" + https2Port); + } + + http3URIString = "https://localhost:" + http3Port + "/foo/"; + pingURIString = "https://localhost:" + http3Port + "/ping/"; + https2URIString = "https://localhost:" + https2Port + "/bar/"; + + http3OnlyServer.start(); + https2AltSvcServer.start(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + static final List> cfs = Collections + .synchronizedList( new LinkedList<>()); + + static CompletableFuture currentCF; + + static class EchoWithPingHandler extends Http2EchoHandler { + private final Object lock = new Object(); + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + // for now only one ping active at a time. don't want to saturate + System.out.println("PING handler invoked for " + exchange.getRequestURI()); + synchronized(lock) { + CompletableFuture cf = currentCF; + if (cf == null || cf.isDone()) { + cf = exchange.sendPing(); + assert cf != null; + cfs.add(cf); + currentCF = cf; + } + } + super.handle(exchange); + } + } + + @Test + public static void test() throws Exception { + try { + initialize(); + System.out.println("servers initialized"); + warmup(false); + warmup(true); + System.out.println("warmup finished"); + simpleTest(false, false); + System.out.println("simpleTest(false, false): done"); + simpleTest(false, true); + System.out.println("simpleTest(false, true): done"); + simpleTest(true, false); + System.out.println("simpleTest(true, false): done"); + System.out.println("simple tests finished"); + streamTest(false); + streamTest(true); + System.out.println("stream tests finished"); + paramsTest(); + System.out.println("params test finished"); + CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join(); + synchronized (cfs) { + for (CompletableFuture cf : cfs) { + System.out.printf("Ping ack received in %d millisec\n", cf.get()); + } + } + System.out.println("closing client"); + if (client != null) { + var tracker = ReferenceTracker.INSTANCE; + tracker.track(client); + client = null; + System.gc(); + var error = tracker.check(1500); + clientExec.close(); + if (error != null) throw error; + } + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + serverExec.close(); + } + } + + static HttpClient getClient() { + if (client == null) { + serverExec = Executors.newCachedThreadPool(); + clientExec = Executors.newCachedThreadPool(); + client = HttpServerAdapters.createClientBuilderForH3() + .executor(clientExec) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI(boolean altSvc) { + return getURI(altSvc, false); + } + + static URI getURI(boolean altsvc, boolean ping) { + if (altsvc) + return URI.create(https2URIString); + else + return URI.create(ping ? pingURIString: http3URIString); + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static final AtomicInteger count = new AtomicInteger(); + static Http3DiscoveryMode config(boolean http3only) { + if (http3only) return HTTP_3_URI_ONLY; + // if the server supports H3 direct connection, we can + // additionally use HTTP_3_URI_ONLY; Otherwise we can + // only use ALT_SVC - or ANY (given that we should have + // preloaded an ALT_SVC in warmup) + int bound = https2AltSvcServer.supportsH3DirectConnection() ? 4 : 3; + int rand = RANDOM.nextInt(bound); + count.getAndIncrement(); + return switch (rand) { + case 1 -> ANY; + case 2 -> ALT_SVC; + case 3 -> HTTP_3_URI_ONLY; + default -> null; + }; + } + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + static final int LOOPS = 13; + static final int FILESIZE = 64 * 1024 + 200; + + static void streamTest(boolean altSvc) throws Exception { + URI uri = getURI(altSvc); + System.err.printf("streamTest %b to %s\n" , altSvc, uri); + System.out.printf("streamTest %b to %s\n" , altSvc, uri); + + HttpClient client = getClient(); + Path src = createTempFileOfSize(CLASS_NAME, ".dat", FILESIZE * 4); + var http3Only = altSvc == false; + var config = config(http3Only); + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofFile(src)) + .setOption(H3_DISCOVERY, config) + .build(); + + Path dest = Paths.get("streamtest.txt"); + dest.toFile().delete(); + CompletableFuture response = client.sendAsync(req, BodyHandlers.ofFile(dest)) + .thenApply(resp -> { + if (resp.statusCode() != 200) + throw new RuntimeException(); + if (resp.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + resp.version()); + } + return resp.body(); + }); + response.join(); + assertFileContentsEqual(src, dest); + System.err.println("streamTest: DONE"); + } + + static void paramsTest() throws Exception { + URI u = new URI("https://localhost:"+https2Port+"/foo"); + System.out.println("paramsTest: Request to " + u); + https2AltSvcServer.addHandler((t -> { + SSLSession s = t.getSSLSession(); + String prot = s.getProtocol(); + if (prot.equals("TLSv1.3")) { + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Protocols =%s\n", prot); + t.sendResponseHeaders(500, -1); + } + }), "/"); + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(u).build(); + HttpResponse resp = client.send(req, BodyHandlers.ofString()); + int stat = resp.statusCode(); + if (stat != 200) { + throw new RuntimeException("paramsTest failed " + stat); + } + if (resp.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + resp.version()); + } + System.err.println("paramsTest: DONE"); + } + + static void warmup(boolean altSvc) throws Exception { + URI uri = getURI(altSvc); + System.out.println("Warmup: Request to " + uri); + System.err.println("Warmup: Request to " + uri); + + // Do a simple warmup request + + HttpClient client = getClient(); + var http3Only = altSvc == false; + var config = config(http3Only); + + // in the warmup phase, we want to make sure + // to preload the ALT_SVC, otherwise the first + // request that uses ALT_SVC might go through HTTP/2 + if (altSvc) config = ALT_SVC; + + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .setOption(H3_DISCOVERY, config) + .build(); + HttpResponse response = client.send(req, BodyHandlers.ofString()); + checkStatus(200, response.statusCode()); + String responseBody = response.body(); + HttpHeaders h = response.headers(); + checkStrings(SIMPLE_STRING, responseBody); + checkStrings(h.firstValue("x-hello").get(), "world"); + checkStrings(h.firstValue("x-bye").get(), "universe"); + } + + static T logExceptionally(String desc, Throwable t) { + System.out.println(desc + " failed: " + t); + System.err.println(desc + " failed: " + t); + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + throw new CompletionException(t); + } + + static void simpleTest(boolean altSvc, boolean ping) throws Exception { + URI uri = getURI(altSvc, ping); + System.err.printf("simpleTest(altSvc:%s, ping:%s) Request to %s%n", + altSvc, ping, uri); + System.out.printf("simpleTest(altSvc:%s, ping:%s) Request to %s%n", + altSvc, ping, uri); + String type = altSvc ? "altSvc" : (ping ? "ping" : "http3"); + + // Do loops asynchronously + + CompletableFuture>[] responses = new CompletableFuture[LOOPS]; + final Path source = createTempFileOfSize(H3BasicTest.class.getSimpleName(), ".dat", FILESIZE); + var http3Only = altSvc == false; + for (int i = 0; i < LOOPS; i++) { + var config = config(http3Only); + HttpRequest request = HttpRequest.newBuilder(uri) + .header("X-Compare", source.toString()) + .POST(BodyPublishers.ofFile(source)) + .setOption(H3_DISCOVERY, config) + .build(); + String desc = type + ": Loop " + i; + System.out.printf("%s simpleTest(altSvc:%s, ping:%s) config(%s) Request to %s%n", + desc, altSvc, ping, config, uri); + System.err.printf("%s simpleTest(altSvc:%s, ping:%s) config(%s) Request to %s%n", + desc, altSvc, ping, config, uri); + Path requestBodyFile = createTempFile(CLASS_NAME, ".dat"); + responses[i] = client.sendAsync(request, BodyHandlers.ofFile(requestBodyFile)) + //.thenApply(resp -> assertFileContentsEqual(resp.body(), source)); + .exceptionally((t) -> logExceptionally(desc, t)) + .thenApply(resp -> { + System.out.printf("Resp %s status %d body size %d\n", + resp.version(), resp.statusCode(), + resp.body().toFile().length() + ); + assertFileContentsEqual(resp.body(), source); + if (resp.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + resp.version()); + } + return resp; + }); + Thread.sleep(100); + System.out.println(type + ": Loop " + i + " done"); + } + CompletableFuture.allOf(responses).join(); + System.err.println(type + " simpleTest: DONE"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java b/test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java new file mode 100644 index 00000000000..f0a9a947d13 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java @@ -0,0 +1,471 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * -Djdk.httpclient.http3.maxConcurrentPushStreams=45 + * H3ConcurrentPush + * @summary This test exercises some of the HTTP/3 specifities for PushPromises. + * It sends several concurrent requests, and the server sends a bunch of + * identical push promise frames to all of them. That is, there will be + * a push promise frame with the same push ID sent to each exchange. + * The one (and only one) of the handlers will open a push stream for + * that push id. The client checks that the expected HTTP/3 specific + * methods are invoked on the PushPromiseHandler. + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class H3ConcurrentPush implements HttpServerAdapters { + + // dummy hack to prevent the IDE complaining that calling + // println will throw NPE + static final PrintStream err = System.err; + static final PrintStream out = System.out; + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + + HttpTestServer server; + URI uri; + URI headURI; + ServerPushHandler pushHandler; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + static final class TestPushPromiseHandler implements PushPromiseHandler { + record NotifiedPromise(PushId pushId, HttpRequest initiatingRequest) {} + final Map requestToPushId = new ConcurrentHashMap<>(); + final Map pushIdToRequest = new ConcurrentHashMap<>(); + final List errors = new CopyOnWriteArrayList<>(); + final List notified = new CopyOnWriteArrayList<>(); + final ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + final Supplier> bodyHandlerSupplier; + final PushPromiseHandler pph; + TestPushPromiseHandler(Supplier> bodyHandlerSupplier) { + this.bodyHandlerSupplier = bodyHandlerSupplier; + this.pph = PushPromiseHandler.of((r) -> bodyHandlerSupplier.get(), promises); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + errors.add(new AssertionError("no pushID provided for: " + pushPromiseRequest)); + } + + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushid) { + notified.add(new NotifiedPromise(pushid, initiatingRequest)); + out.println("notifyPushPromise: pushId=" + pushid); + pph.notifyAdditionalPromise(initiatingRequest, pushid); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function, CompletableFuture>> acceptor) { + out.println("applyPushPromise: " + pushPromiseRequest + ", pushId=" + pushid); + requestToPushId.putIfAbsent(pushPromiseRequest, pushid); + if (pushIdToRequest.putIfAbsent(pushid, pushPromiseRequest) != null) { + errors.add(new AssertionError("pushId already used: " + pushid)); + } + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, pushid, acceptor); + } + + } + + @Test + public void testConcurrentPushes() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + TestPushPromiseHandler custom = new TestPushPromiseHandler<>(BodyHandlers::ofString); + var promises = custom.promises; + + for (int j=0; j < 2; j++) { + if (j == 0) out.println("\ntestCancel: First time around"); + else out.println("\ntestCancel: Second time around: should be a new connection"); + + // now make sure there's an HTTP/3 connection + client.send(HttpRequest.newBuilder(headURI).version(Version.HTTP_3) + .setOption(H3_DISCOVERY, ALT_SVC).HEAD().build(), BodyHandlers.discarding()); + + int waitForPushId; + List>> responses = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + waitForPushId = Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + CompletableFuture> main = client.sendAsync( + HttpRequest.newBuilder(uri.resolve("?i=%s,j=%s".formatted(i, j))) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + custom); + responses.add(main); + } + CompletableFuture.allOf(responses.toArray(CompletableFuture[]::new)).join(); + responses.forEach(cf -> { + var main = cf.join(); + var old = promises.put(main.request(), CompletableFuture.completedFuture(main)); + assertNull(old, "unexpected mapping for: " + old); + }); + + promises.forEach((key, value) -> out.println(key + ":" + value.join().body())); + + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + + int expectedPushes = Math.min(PUSH_PROMISES.size(), maxPushes) + 5; + assertEquals(promises.size(), expectedPushes); + + promises.clear(); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3ConcurrentPush::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3ConcurrentPush::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes))) { + success = true; + } + if (success) { + out.println("Got expected IOException: " + io); + } else throw io; + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection, + // so we can restart from scratch + pushHandler.reset(); + } + var errors = custom.errors; + errors.forEach(t -> t.printStackTrace(System.out)); + var error = errors.stream().findFirst().orElse(null); + if (error != null) throw error; + var notified = custom.notified; + assertEquals(notified.size(), 9*4*2, "Unexpected notification: " + notified); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + private final ReentrantLock lock = new ReentrantLock(); + private final Map sentPromises = new ConcurrentHashMap<>(); + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + // The assumption is that there will be several concurrent + // exchanges, but all on the same connection + // The first exchange that emits a PushPromise sends + // a push promise frame + open the push response stream. + // The other exchanges will simply send a push promise + // frame, with the pushId allocated by the previous exchange. + // The sentPromises map is used to store that pushId. + // This obviously only works if we have a single HTTP/3 connection. + final AtomicInteger count = new AtomicInteger(); + public void handle(HttpTestExchange exchange) throws IOException { + long count = -1; + try { + count = this.count.incrementAndGet(); + err.println("Server: handle " + exchange + + " on " + exchange.getConnectionKey()); + out.println("Server: handle " + exchange.getRequestURI() + + " on " + exchange.getConnectionKey()); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } catch (ClosedChannelException ex) { + out.printf("handling exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + out.printf("Got closed channel exception sending response after sent=%s allowed=%s%n", + sent, allowed); + } + } finally { + out.printf("handled exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + } + } + + volatile long allowed = -1; + volatile int sent = 0; + volatile int nsent = 0; + void reset() { + lock.lock(); + try { + allowed = -1; + sent = 0; + nsent = 0; + sentPromises.clear(); + } finally { + lock.unlock(); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long usePushId = exchange.getRequestHeaders() + .firstValueAsLong("X-UsePushId").orElse(-1); + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + for (Map.Entry promise : promises.entrySet()) { + // if usePushId != -1 we send a single push promise, + // without checking that it's allowed. + // Otherwise, we stop sending promises when we have consumed + // the whole window + if (usePushId == -1 && allowed > 0 && sent >= allowed) { + err.println("Server: sent all allowed promises: " + sent); + break; + } + + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + if (usePushId == -1) { + long pushId; + boolean send = false; + lock.lock(); + try { + Long usedPushId = sentPromises.get(promise.getKey()); + if (usedPushId == null) { + pushId = exchange.sendHttp3PushPromiseFrame(-1, uri, headers); + waitForPushId = pushId + 1; + sentPromises.put(promise.getKey(), pushId); + sent += 1; + send = true; + } else { + pushId = usedPushId; + exchange.sendHttp3PushPromiseFrame(pushId, uri, headers); + } + } finally { + lock.unlock(); + } + if (send) { + exchange.sendHttp3PushResponse(pushId, uri, headers, headers, is); + err.println("Server: Sent push promise with response: " + pushId); + } else { + err.println("Server: Sent push promise frame: " + pushId); + } + if (pushId >= waitForPushId) waitForPushId = pushId + 1; + } else { + exchange.sendHttp3PushPromiseFrame(usePushId, uri, headers); + err.println("Server: Sent push promise frame: " + usePushId); + exchange.sendHttp3PushResponse(usePushId, uri, headers, headers, is); + err.println("Server: Sent push promise response: " + usePushId); + lock.lock(); + sent += 1; + lock.unlock(); + return; + } + } + err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java b/test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java new file mode 100644 index 00000000000..719067a7c3c --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java @@ -0,0 +1,581 @@ +/* + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors,http3,quic:hs + * -Djdk.internal.httpclient.debug=false + * H3ConnectionPoolTest + */ + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Supplier; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2EchoHandler; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +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; +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertNotEquals; +import static jdk.test.lib.Asserts.assertTrue; + +public class H3ConnectionPoolTest implements HttpServerAdapters { + + private static final String CLASS_NAME = H3ConnectionPoolTest.class.getSimpleName(); + + static int altsvcPort, https2Port, http3Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static volatile HttpClient client = null; + static SSLContext sslContext; + static volatile String http3OnlyURIString, https2URIString, http3AltSvcURIString, http3DirectURIString; + + static void initialize(boolean samePort) throws Exception { + initialize(samePort, Http2EchoHandler::new); + } + + static void initialize(boolean samePort, Supplier handlers) throws Exception { + System.out.println("\nConfiguring for advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = null; + client = getClient(); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, sslContext); + if (samePort) { + System.out.println("Attempting to enable advertised HTTP/3 service on same port"); + https2AltSvcServer.enableH3AltServiceOnSamePort(); + System.out.println("Advertised AltSvc on same port " + + (https2AltSvcServer.supportsH3DirectConnection() ? "enabled" : " not enabled")); + } else { + System.out.println("Attempting to enable advertised HTTP/3 service on different port"); + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/https2/"); + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/h2h3/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + altsvcPort = https2AltSvcServer.getH3AltService() + .map(Http3TestServer::getAddress).stream() + .mapToInt(InetSocketAddress::getPort).findFirst() + .getAsInt(); + // server that only supports HTTP/3 - we attempt to use the same port + // as the HTTP/2 server so that we can pretend that the H2 server as two H3 endpoints: + // one advertised (the alt service endpoint og the HTTP/2 server) + // one non advertised (the direct endpoint, at the same authority as HTTP/2, but which + // is in fact our http3OnlyServer) + try { + http3OnlyServer = new Http3TestServer(sslContext, samePort ? 0 : https2Port); + System.out.println("Unadvertised service enabled on " + + (samePort ? "ephemeral port" : "same port")); + } catch (IOException ex) { + System.out.println("Can't create HTTP/3 server on same port: " + ex); + http3OnlyServer = new Http3TestServer(sslContext, 0); + } + http3OnlyServer.addHandler("/" + CLASS_NAME + "/http3/", handlers.get()); + http3OnlyServer.addHandler("/" + CLASS_NAME + "/h2h3/", handlers.get()); + http3OnlyServer.start(); + http3Port = http3OnlyServer.getQuicServer().getAddress().getPort(); + + if (http3Port == https2Port) { + System.out.println("HTTP/3 server enabled on same port than HTTP/2 server"); + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for HTTP/3 server"); + } + } else { + System.out.println("HTTP/3 server enabled on a different port than HTTP/2 server"); + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for HTTP/3 server"); + } + } + if (altsvcPort == https2Port) { + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for advertised AltSvc"); + } + } else { + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for advertised AltSvc"); + } + } + + http3OnlyURIString = "https://" + http3OnlyServer.serverAuthority() + "/" + CLASS_NAME + "/http3/foo/"; + https2URIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/https2/bar/"; + http3DirectURIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/h2h3/direct/"; + http3AltSvcURIString = https2URIString + .replace(":" + https2Port + "/", ":" + altsvcPort + "/") + .replace("/https2/bar/", "/h2h3/altsvc/"); + System.out.println("HTTP/2 server started at: " + https2AltSvcServer.serverAuthority()); + System.out.println(" with advertised HTTP/3 endpoint at: " + + URI.create(http3AltSvcURIString).getRawAuthority()); + System.out.println("HTTP/3 server started at:" + http3OnlyServer.serverAuthority()); + + https2AltSvcServer.start(); + } catch (Throwable e) { + System.out.println("Configuration failed: " + e); + System.err.println("Throwing now: " + e); + e.printStackTrace(); + throw e; + } + } + + @Test + public static void testH3Only() throws Exception { + System.out.println("\nTesting HTTP/3 only"); + initialize(true); + try (HttpClient client = getClient()) { + var reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(http3OnlyURIString)) + .version(HTTP_3) + .GET(); + HttpRequest request1 = reqBuilder.copy() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + HttpResponse response1 = client.send(request1, BodyHandlers.ofString()); + System.out.printf("First response: (%s): %s%n", response1.connectionLabel(), response1); + response1.headers().map().entrySet().forEach((e) -> { + System.out.printf(" %s: %s%n", e.getKey(), e.getValue()); + }); + // ANY should reuse the same connection + HttpRequest request2 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + HttpRequest request3 = reqBuilder.copy() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + // ANY should reuse the same connection + HttpRequest request4 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + HttpResponse response4 = client.send(request4, BodyHandlers.ofString()); + assertEquals(response1.connectionLabel().get(), response2.connectionLabel().get()); + assertEquals(response2.connectionLabel().get(), response3.connectionLabel().get()); + assertEquals(response3.connectionLabel().get(), response4.connectionLabel().get()); + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testH2H3WithTwoAltSVC() throws Exception { + testH2H3(false); + } + + @Test + public static void testH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3(true); + } + + private static void testH2H3(boolean samePort) throws Exception { + System.out.println("\nTesting with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + HttpResponse response1 = client.send(request1, BodyHandlers.ofString()); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // second request with HTTP3_URI_ONLY should reuse a created connection + // It should reuse the advertised connection (from response2) if same + // origin + HttpRequest request3 = req1Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response1.connectionLabel().get(), response3.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request4 = req2Builder.copy().build(); + HttpResponse response4 = client.send(request4, BodyHandlers.ofString()); + assertEquals(HTTP_3, response4.version()); + checkStatus(200, response4.statusCode()); + assertEquals(response4.connectionLabel().get(), response2.connectionLabel().get()); + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first - make a direct connection + HttpRequest request1 = req1Builder.copy().build(); + HttpResponse response1 = client.send(request1, BodyHandlers.ofString()); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + + // second, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request3 = req2Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response1.connectionLabel().get()); + + // fourth request with HTTP_3_URI_ONLY should reuse the first connection, + // and not reuse the second. + HttpRequest request4 = req1Builder.copy().build(); + HttpResponse response4 = client.send(request1, BodyHandlers.ofString()); + assertEquals(HTTP_3, response4.version()); + assertEquals(response4.connectionLabel().get(), response1.connectionLabel().get()); + assertNotEquals(response4.connectionLabel().get(), response3.connectionLabel().get()); + checkStatus(200, response1.statusCode()); + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request3 = req2Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testParallelH2H3WithTwoAltSVC() throws Exception { + testH2H3Concurrent(false); + } + + @Test + public static void testParallelH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3Concurrent(true); + } + + private static void testH2H3Concurrent(boolean samePort) throws Exception { + System.out.println("\nTesting concurrent connections with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + HttpRequest request2 = req2Builder.copy().build(); + List>> directResponses = new ArrayList<>(); + for (int i=0; i<3; i++) { + directResponses.add(client.sendAsync(request1, BodyHandlers.ofString())); + } + // can't send requests in parallel here because if any establishes + // a connection before the H3 direct are established, then the H3 + // direct might reuse the H3 alt since the service is with same origin + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + String c1Label = null; + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s]: %s%n", i, + response1.connectionLabel(), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + if (i == 0) { + c1Label = response1.connectionLabel().get(); + } + assertEquals(c1Label, response1.connectionLabel().orElse(null)); + } + // first request with ALT_SVC is to get alt service, should be H2 + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + assertNotEquals(c1Label, h2resp2.connectionLabel().orElse(null)); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + List>> altResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + altResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + String c2Label = null; + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s]: %s%n", i, + response2.connectionLabel(), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), c1Label); + if (i == 0) { + c2Label = response2.connectionLabel().get(); + } + assertEquals(c2Label, response2.connectionLabel().orElse(null)); + } + + // second set of requests should reuse a created connection + HttpRequest request3 = req1Builder.copy().build(); + List>> mixResponses = new ArrayList<>(); + for (int i=0; i < 3; i++) { + mixResponses.add(client.sendAsync(request3, BodyHandlers.ofString())); + mixResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + for (int i=0; i < mixResponses.size(); i++) { + HttpResponse response3 = mixResponses.get(i).get(); + System.out.printf("mixed response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + if (response3.request().getOption(H3_DISCOVERY).orElse(null) == ALT_SVC) { + assertEquals(c2Label, response3.connectionLabel().get()); + } else { + assertEquals(c1Label, response3.connectionLabel().get()); + } + } + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first - make a direct connection + HttpRequest request1 = req1Builder.copy().build(); + + // second, use the alt service + HttpRequest request2 = req2Builder.copy().build(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // third, use ANY + HttpRequest request3 = req2Builder.copy().setOption(H3_DISCOVERY, ANY).build(); + + List>> directResponses = new ArrayList<>(); + List>> altResponses = new ArrayList<>(); + List>> anyResponses = new ArrayList<>(); + checkStatus(200, h2resp2.statusCode()); + for (int i=0; i<3; i++) { + anyResponses.add(client.sendAsync(request3, BodyHandlers.ofString())); + directResponses.add(client.sendAsync(request1, BodyHandlers.ofString())); + altResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + String c1Label = null; + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s] %s: %s%n", i, + response1.connectionLabel(), + response1.request().getOption(H3_DISCOVERY), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + if (i == 0) { + c1Label = response1.connectionLabel().get(); + } + assertEquals(c1Label, response1.connectionLabel().orElse(null)); + } + String c2Label = null; + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s] %s: %s%n", i, + response2.connectionLabel(), + response2.request().getOption(H3_DISCOVERY), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + if (i == 0) { + c2Label = response2.connectionLabel().get(); + } + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), c1Label); + assertEquals(c2Label, response2.connectionLabel().orElse(null)); + } + var expectedLabels = Set.of(c1Label, c2Label); + for (int i = 0; i < anyResponses.size(); i++) { + HttpResponse response3 = anyResponses.get(i).get(); + System.out.printf("any response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), h2resp2.connectionLabel().get()); + var label = response3.connectionLabel().orElse(""); + assertTrue(expectedLabels.contains(label), "Unexpected label: %s not in %s" + .formatted(label, expectedLabels)); + } + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request3 = req2Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + static HttpClient getClient() { + if (client == null) { + client = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + + static T logExceptionally(String desc, Throwable t) { + System.out.println(desc + " failed: " + t); + System.err.println(desc + " failed: " + t); + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + throw new CompletionException(t); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java b/test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java new file mode 100644 index 00000000000..0bee94e6e8a --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java @@ -0,0 +1,270 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.System.out; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.assertEquals; + + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.quic.QuicStandaloneServer + * @run testng/othervm/timeout=480 -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djavax.net.debug=all + * H3DataLimitsTest + * @summary Verify handling of MAX_DATA / MAX_STREAM_DATA frames + */ +public class H3DataLimitsTest implements HttpServerAdapters { + + SSLContext sslContext; + HttpTestServer h3TestServer; + String h3URI; + + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + @DataProvider(name = "h3URIs") + public Object[][] versions(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return client; + } + + @Test(dataProvider = "h3URIs") + public void testHugeResponse(final String h3URI) throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI + "?16000000"); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_3, "first response version"); + + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_3, "second response version"); + } + + @Test(dataProvider = "h3URIs") + public void testManySmallResponses(final String h3URI) throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI + "?160000"); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + HttpRequest request = builder.build(); + for (int i=0; i<102; i++) { // more than 100 to exercise MAX_STREAMS + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #" + i + ": " + response); + out.println("Version #" + i + ": " + response.version()); + assertEquals(response.statusCode(), 200, "response status"); + assertEquals(response.version(), HTTP_3, "response version"); + } + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + + // An HTTP/3 server that only supports HTTP/3 + h3TestServer = HttpTestServer.of(new Http3TestServer(sslContext)); + final HttpTestHandler h3Handler = new Handler(); + h3TestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/x"; + + serverCount.addAndGet(1); + h3TestServer.start(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + h3TestServer.stop(); + } + + static class Handler implements HttpTestHandler { + + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + try (InputStream is = t.getRequestBody()) { + is.readAllBytes(); + } + System.out.println("Query: "+uri.getQuery()); + int bytesToProduce = Integer.parseInt(uri.getQuery()); + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Server sending %d - 0 length\n", 200); + t.sendResponseHeaders(200, bytesToProduce); + } + try (OutputStream os = t.getResponseBody()) { + os.write(new byte[bytesToProduce]); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java b/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java new file mode 100644 index 00000000000..10a3a3af8f2 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java @@ -0,0 +1,1063 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.IRetryAnalyzer; +import org.testng.ITestResult; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.OutputStream; +import java.net.ProtocolException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.time.Duration; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @key intermittent + * @comment testResetControlStream may fail if the client doesn't read the stream type + * before the stream is reset, + * testConnectionCloseXXX may fail because connection_close frame is not retransmitted + * @summary Verifies that the HTTP client responds with the right error codes and types + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors H3ErrorHandlingTest + */ +public class H3ErrorHandlingTest implements HttpServerAdapters { + + private SSLContext sslContext; + private QuicStandaloneServer server; + private String requestURIBase; + + @DataProvider + public static Object[][] controlStreams() { + // control / encoder / decoder + return new Object[][] {{(byte)0}, {(byte)2}, {(byte)3}}; + } + + static final byte[] data = new byte[]{(byte)0,(byte)0}; + static final byte[] headers = new byte[]{(byte)1,(byte)0}; + static final byte[] reserved1 = new byte[]{(byte)2,(byte)0}; + static final byte[] cancel_push = new byte[]{(byte)3,(byte)1,(byte)0}; + static final byte[] settings = new byte[]{(byte)4,(byte)0}; + static final byte[] push_promise = new byte[]{(byte)5,(byte)1,(byte)0}; + // 48 bytes, ID 0, 47 byte headers + static final byte[] valid_push_promise = HexFormat.of().parseHex( + "0530000000"+ // push promise, length 48, id 0, section prefix + "508b089d5c0b8170dc702fbce7"+ // :authority + "d1"+ // :method:get + "51856272d141ff"+ // :path + "d7"+ // :scheme:https + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"); //user-agent + static final byte[] reserved2 = new byte[]{(byte)6,(byte)0}; + static final byte[] goaway = new byte[]{(byte)7,(byte)1,(byte)4}; + static final byte[] reserved3 = new byte[]{(byte)8,(byte)0}; + static final byte[] reserved4 = new byte[]{(byte)9,(byte)0}; + static final byte[] max_push_id = new byte[]{(byte)13,(byte)1,(byte)0}; + static final byte[] huge_id_push_promise = new byte[]{(byte)5,(byte)10, + (byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255, + (byte)0, (byte)0}; + + /* + Truncates or expands the frame to the specified length + */ + private static Object[][] chopFrame(byte[] frame, int... lengths) { + var result = new Object[lengths.length][]; + for (int i = 0; i< lengths.length; i++) { + int length = lengths[i]; + byte[] choppedFrame = Arrays.copyOf(frame, length + 2); + choppedFrame[1] = (byte)length; + result[i] = new Object[] {choppedFrame, lengths[i]}; + } + return result; + } + + /* + Truncates or expands the byte array to the specified length + */ + private static Object[][] chopBytes(byte[] bytes, int... lengths) { + var result = new Object[lengths.length][]; + for (int i = 0; i< lengths.length; i++) { + int length = lengths[i]; + byte[] choppedBytes = Arrays.copyOf(bytes, length); + result[i] = new Object[] {choppedBytes, lengths[i]}; + } + return result; + } + + @DataProvider + public static Object[][] malformedSettingsFrames() { + // 2-byte ID, 2-byte value + byte[] settingsFrame = new byte[]{(byte)4,(byte)4,(byte)0x40, (byte)6, (byte)0x40, (byte)6}; + return chopFrame(settingsFrame, 1, 2, 3); + } + + @DataProvider + public static Object[][] malformedCancelPushFrames() { + byte[] cancelPush = new byte[]{(byte)3,(byte)2, (byte)0x40, (byte)0}; + return chopFrame(cancelPush, 0, 1, 3, 9); + } + + @DataProvider + public static Object[][] malformedGoawayFrames() { + byte[] goaway = new byte[]{(byte)7,(byte)2, (byte)0x40, (byte)0}; + return chopFrame(goaway, 0, 1, 3, 9); + } + + @DataProvider + public static Object[][] malformedResponseHeadersFrames() { + byte[] responseHeaders = HexFormat.of().parseHex( + "011a0000"+ // headers, length 26, section prefix + "d9"+ // :status:200 + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"); //user-agent + return chopFrame(responseHeaders, 0, 1, 4, 5, 6, 7); + } + + @DataProvider + public static Object[][] truncatedResponseFrames() { + byte[] response = HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "210100" // reserved, 1 byte + ); + return chopBytes(response, 1, 2, 3, 4, 6, 7, 9, 10); + } + + @DataProvider + public static Object[][] truncatedControlFrames() { + byte[] response = HexFormat.of().parseHex( + "00"+ // stream type: control + "04022100"+ //settings, reserved + "070104"+ //goaway, 4 + "210100" // reserved, 1 byte + ); + return chopBytes(response, 2, 3, 4, 6, 7, 9, 10); + } + + @DataProvider + public static Object[][] malformedPushPromiseFrames() { + return chopFrame(valid_push_promise, 0, 1, 2, 4, 5, 6); + } + + @DataProvider + public static Object[][] invalidControlFrames() { + // frames not valid on the server control stream (after settings) + // all except cancel_push / goaway (max_push_id is client-only) + return new Object[][] {{data}, {headers}, {settings}, {push_promise}, {max_push_id}, + {reserved1}, {reserved2}, {reserved3}, {reserved4}}; + } + + @DataProvider + public static Object[][] invalidResponseFrames() { + // frames not valid on the response stream + // all except headers / push_promise + // data is not valid as the first frame + return new Object[][] {{data}, {cancel_push}, {settings}, {goaway}, {max_push_id}, + {reserved1}, {reserved2}, {reserved3}, {reserved4}}; + } + + @DataProvider + public static Object[][] invalidPushFrames() { + // frames not valid on the push promise stream + // all except headers + // data is not valid as the first frame + return new Object[][] {{data}, {cancel_push}, {settings}, {push_promise}, {goaway}, {max_push_id}, + {reserved1}, {reserved2}, {reserved3}, {reserved4}}; + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .alpn("h3") + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + } + + /** + * Server sends a non-settings frame on the control stream + */ + @Test + public void testNonSettingsFrame() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, reserved frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 0x21, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_MISSING_SETTINGS); + } + + /** + * Server opens 2 control streams + */ + @Test(dataProvider = "controlStreams") + public void testTwoControlStreams(byte type) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + + QuicSenderStream controlStream, controlStream2; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + controlStream2 = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + var writer2 = controlStream2.connectWriter(scheduler); + // control stream + byte[] bytesToWrite = new byte[] { type }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + writer2.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_STREAM_CREATION_ERROR); + } + + /** + * Server closes control stream + */ + @Test(dataProvider = "controlStreams") + public void testCloseControlStream(byte type) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); + var writer = controlStream.connectWriter(controlscheduler); + + byte[] bytesToWrite = new byte[] { type }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), true); + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_CLOSED_CRITICAL_STREAM); + } + + public static class RetryOnce implements IRetryAnalyzer { + boolean retried; + + @Override + public boolean retry(ITestResult iTestResult) { + if (!retried) { + retried = true; + return true; + } + return false; + } + } + + /** + * Server resets control stream + */ + @Test(dataProvider = "controlStreams", retryAnalyzer = RetryOnce.class) + public void testResetControlStream(byte type) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); + var writer = controlStream.connectWriter(controlscheduler); + + byte[] bytesToWrite = new byte[] { type }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // wait for the stream data to be sent before resetting + System.out.println("Server: sending first ping"); + c.requestSendPing().join(); + // sometimes the first ping succeeds before the stream frame is delivered. + // Send another one just in case. + System.out.println("Server: sending second ping"); + c.requestSendPing().join(); + System.out.println("Server: resetting control stream " + writer.stream().streamId()); + // the test may fail if the stream type byte is not processed by HTTP3 + // before the reset is received. + writer.reset(0); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_CLOSED_CRITICAL_STREAM); + } + + /** + * Server sends unexpected frame on control stream + */ + @Test(dataProvider = "invalidControlFrames") + public void testUnexpectedControlFrame(byte[] frame) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 4, 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); + } + + /** + * Server sends malformed settings frame + */ + @Test(dataProvider = "malformedSettingsFrames") + public void testMalformedSettingsFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream + byte[] bytesToWrite = new byte[] { 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends malformed goaway frame + */ + @Test(dataProvider = "malformedGoawayFrames") + public void testMalformedGoawayFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 4, 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends malformed cancel push frame + */ + @Test(dataProvider = "malformedCancelPushFrames") + public void testMalformedCancelPushFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 4, 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends invalid GOAWAY frame sequence + */ + @Test + public void testInvalidGoAwaySequence() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0, GOAWAY, id = 4, GOAWAY, id = 8 + byte[] bytesToWrite = new byte[] { 0, 4, 0, 7, 1, 4, 7, 1, 8}; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends invalid GOAWAY stream ID + */ + @Test + public void testInvalidGoAwayId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0, GOAWAY, id = 7 + byte[] bytesToWrite = new byte[] { 0, 4, 0, 7, 1, 7}; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends invalid CANCEL_PUSH stream ID + */ + @Test + public void testInvalidCancelPushId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0, CANCEL_PUSH, id = MAX_VL_INTEGER + byte[] bytesToWrite = new byte[] { 0, 4, 0, 3, 8, (byte)255, (byte)255, + (byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255}; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends unexpected frame on push stream + */ + @Test(dataProvider = "invalidPushFrames") + public void testUnexpectedPushFrame(byte[] frame) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); + } + + /** + * Server sends malformed frame on push stream + */ + @Test(dataProvider = "malformedResponseHeadersFrames") + public void testMalformedPushStreamFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, frame.length == 2 + ? Http3Error.H3_FRAME_ERROR + : Http3Error.QPACK_DECOMPRESSION_FAILED); + } + + /** + * Server sends malformed frame on push stream + */ + @Test(dataProvider = "malformedPushPromiseFrames") + public void testMalformedPushPromiseFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + // write PUSH_PROMISE frame + s.outputStream().write(frame); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, frame.length <= 3 + ? Http3Error.H3_FRAME_ERROR + : Http3Error.QPACK_DECOMPRESSION_FAILED); + } + + /** + * Server reuses push stream ID + */ + @Test + public void testDuplicatePushStream() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream, pushStream2; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + pushStream2 = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + + writer = pushStream2.connectWriter(scheduler); + // push stream, id 0 + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends push promise with ID > MAX_PUSH_ID + */ + @Test + public void testInvalidPushPromiseId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + // write PUSH_PROMISE frame + s.outputStream().write(huge_id_push_promise); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server opens a push stream ID > MAX_PUSH_ID + */ + @Test + public void testInvalidPushStreamId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = pushStream.connectWriter(scheduler); + // push stream, id MAX_VL_INTEGER + byte[] bytesToWrite = new byte[] { 1, + (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255 }; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends unexpected frame on response stream + */ + @Test(dataProvider = "invalidResponseFrames") + public void testUnexpectedResponseFrame(byte[] frame) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + s.outputStream().write(frame); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); + } + + /** + * Server sends malformed headers frame on response stream + */ + @Test(dataProvider = "malformedResponseHeadersFrames") + public void testMalformedResponseFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + s.outputStream().write(frame); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, frame.length == 2 + ? Http3Error.H3_FRAME_ERROR + : Http3Error.QPACK_DECOMPRESSION_FAILED); + } + + /** + * Server truncates a frame on the response stream + */ + @Test(dataProvider = "truncatedResponseFrames") + public void testTruncatedResponseFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(frame); + } + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server truncates a frame on the control stream + */ + @Test(dataProvider = "truncatedControlFrames") + public void testTruncatedControlFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); + var writer = controlStream.connectWriter(controlscheduler); + + writer.scheduleForWriting(ByteBuffer.wrap(frame), true); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + // H3_CLOSED_CRITICAL_STREAM is also acceptable here + triggerError(errorCF, Http3Error.H3_FRAME_ERROR, Http3Error.H3_CLOSED_CRITICAL_STREAM); + } + + /** + * Server truncates a frame on the push stream + */ + @Test(dataProvider = "truncatedResponseFrames") + public void testTruncatedPushStreamFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, true); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends a settings frame with reserved HTTP2 settings + */ + @Test + public void testReservedSettingsFrames() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 2, setting 4 = 0 + byte[] bytesToWrite = new byte[] { 0, 4, 2, 4, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_SETTINGS_ERROR); + } + + /** + * Server sends a stateless reset + */ + @Test + public void testStatelessReset() throws Exception { + server.addHandler((c,s)-> { + // stateless reset + QuicConnectionId localConnId = c.localConnectionId(); + ByteBuffer resetDatagram = c.endpoint().idFactory().statelessReset(localConnId.asReadOnlyBuffer(), 43); + ((DatagramChannel)c.channel()).send(resetDatagram, c.peerAddress()); + // ignore the request stream; we're expecting the client to close the connection. + // The server won't receive any notification from the client here. + // The connection will leak. + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (Exception e) { + final String expectedMsg = "stateless reset from peer"; + if (e.getMessage() != null && e.getMessage().contains(expectedMsg)) { + // got the expected exception + return; + } + // unexpected exception, throw it back + throw e; + } finally { + client.shutdownNow(); + } + } + + /** + * Server opens a bidi stream + */ + @Test + @Ignore("BiDi streams are rejected by H3 client at QUIC level") + public void testBidiStream() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream bidiStream; + bidiStream = c.openNewLocalBidiStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = bidiStream.connectWriter(scheduler); + // some data + byte[] bytesToWrite = new byte[] { 0, 4, 2, 4, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_STREAM_CREATION_ERROR); + } + + /** + * Server closes the connection with a known QUIC error + */ + @Test + public void testConnectionCloseQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.INTERNAL_ERROR) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("INTERNAL_ERROR", "testtest"); + } + + /** + * Server closes the connection with a known crypto error + */ + @Test + public void testConnectionCloseCryptoQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.CRYPTO_ERROR.from() + 80 /*Alert.INTERNAL_ERROR.id*/, null) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("CRYPTO_ERROR", "internal_error", "testtest"); + } + + /** + * Server closes the connection with an unknown crypto error + */ + @Test + public void testConnectionCloseUnknownCryptoQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.CRYPTO_ERROR.from() + 5, null) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("CRYPTO_ERROR", "5", "testtest"); + } + + /** + * Server closes the connection with an unknown QUIC error + */ + @Test + public void testConnectionCloseUnknownQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.CRYPTO_ERROR.to() + 1 /*0x200*/, null) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("200", "testtest"); + } + + /** + * Server closes the connection with a known H3 error + */ + @Test + public void testConnectionCloseH3() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.appLayerClose(Http3Error.H3_EXCESSIVE_LOAD.code()); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("H3_EXCESSIVE_LOAD", "testtest"); + } + + /** + * Server closes the connection with an unknown H3 error + */ + @Test + public void testConnectionCloseH3Unknown() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.appLayerClose(0x1f21); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("1F21", "testtest"); + } + + + private void triggerClose(String... reasons) throws Exception { + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof IOException, "Expected IOException"); + for (String reason : reasons) { + assertTrue(cause.getMessage().contains(reason), + cause.getMessage() + " does not contain " + reason); + } + } finally { + client.shutdownNow(); + } + } + + + private void triggerError(CompletableFuture errorCF, Http3Error expected) throws Exception { + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); + TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); + final long actual = terminationCause.getCloseCode(); + // expected + assertEquals(actual, expected.code(), "Expected " + toHexString(expected) + " got 0x" + Long.toHexString(actual)); + } finally { + client.shutdownNow(); + } + } + + private void triggerError(CompletableFuture errorCF, Http3Error... expected) throws Exception { + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); + TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); + final long actual = terminationCause.getCloseCode(); + // expected + Optional h3Actual = Http3Error.fromCode(actual); + assertTrue(h3Actual.isPresent(), "Expected HTTP3 error, got 0x" + Long.toHexString(actual)); + Set expectedErrors = Set.of(expected); + assertTrue(expectedErrors.contains(h3Actual.get()), "Expected "+expectedErrors+ + ", got: "+h3Actual); + } finally { + client.shutdownNow(); + } + } + + private void triggerPushError(CompletableFuture errorCF, Http3Error http3Error) throws Exception { + HttpClient client = getHttpClient(); + // close might block; use shutdownNow instead + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding(), + (initiatingRequest, pushPromiseRequest, acceptor) -> + acceptor.apply(BodyHandlers.discarding()) + ).get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); + TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); + final long actual = terminationCause.getCloseCode(); + // expected + assertEquals(actual, http3Error.code(), "Expected " + toHexString(http3Error) + " got 0x" + Long.toHexString(actual)); + } finally { + client.shutdownNow(); + } + } + + private HttpRequest getRequest() throws URISyntaxException { + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + return reqBuilder.build(); + } + + private HttpClient getHttpClient() { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(sslContext).build(); + return client; + } + + private static String toHexString(final Http3Error error) { + return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; + } + + private static void completeUponTermination(final QuicServerConnection serverConnection, + final CompletableFuture cf) { + serverConnection.futureTerminationCause().handle( + (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java b/test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java new file mode 100644 index 00000000000..9e801ac2ea4 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java @@ -0,0 +1,300 @@ +/* + * 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. + * + * 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 8087112 8177935 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.internal.httpclient.debug=err + * -Djdk.httpclient.HttpClient.log=ssl,headers,requests,responses,errors + * H3FixedThreadPoolTest + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.test.lib.Asserts.assertFileContentsEqual; +import static jdk.test.lib.Utils.createTempFileOfSize; + +import org.testng.annotations.Test; + +public class H3FixedThreadPoolTest implements HttpServerAdapters { + + private static final String CLASS_NAME = H3FixedThreadPoolTest.class.getSimpleName(); + + static int http3Port, https2Port; + static HttpTestServer http3Server, https2Server; + static volatile HttpClient client = null; + static ExecutorService exec; + static SSLContext sslContext; + + static String http3URIString, https2URIString; + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + exec = Executors.newCachedThreadPool(); + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, exec); + http3Server.addHandler(new HttpTestFileEchoHandler(), "/H3FixedThreadPoolTest/http3-only/"); + http3Port = http3Server.getAddress().getPort(); + + https2Server = HttpTestServer.create(ALT_SVC, sslContext, exec); + https2Server.addHandler(new HttpTestFileEchoHandler(), "/H3FixedThreadPoolTest/http3-alt-svc/"); + https2Server.addHandler((t) -> { + t.getRequestBody().readAllBytes(); + t.sendResponseHeaders(200, 0); + }, "/H3FixedThreadPoolTest/http3-alt-svc/bar/head"); + https2Port = https2Server.getAddress().getPort(); + http3URIString = "https://" + http3Server.serverAuthority() + "/H3FixedThreadPoolTest/http3-only/foo/"; + https2URIString = "https://" + https2Server.serverAuthority() + "/H3FixedThreadPoolTest/http3-alt-svc/bar/"; + + http3Server.start(); + https2Server.start(); + + // warmup client to populate AltServiceRegistry + var head = HttpRequest.newBuilder(URI.create(https2URIString + "head")) + .setOption(H3_DISCOVERY, ALT_SVC).build(); + var resp = client.send(head, BodyHandlers.ofString()); + assert resp.statusCode() == 200; + + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + @Test + public static void test() throws Exception { + try { + initialize(); + simpleTest(false); + simpleTest(true); + streamTest(false); + streamTest(true); + paramsTest(); + if (client != null) { + ReferenceTracker.INSTANCE.track(client); + client = null; + System.gc(); + var error = ReferenceTracker.INSTANCE.check(4000); + if (error != null) throw error; + } + } catch (Exception | Error tt) { + tt.printStackTrace(); + throw tt; + } finally { + http3Server.stop(); + https2Server.stop(); + exec.shutdownNow(); + } + } + + static HttpClient getClient() { + if (client == null) { + // Executor e1 = Executors.newFixedThreadPool(1); + // Executor e = (Runnable r) -> e1.execute(() -> { + // System.out.println("[" + Thread.currentThread().getName() + // + "] Executing: " + // + r.getClass().getName()); + // r.run(); + // }); + client = HttpServerAdapters.createClientBuilderForH3() + .executor(Executors.newFixedThreadPool(2)) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI(boolean http3Only) { + if (http3Only) + return URI.create(http3URIString); + else + return URI.create(https2URIString); + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string \"%s\" != \"%s\"%n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + static final int LOOPS = 32; + static final int FILESIZE = 64 * 1024 + 200; + + static void streamTest(boolean http3only) throws Exception { + URI uri = getURI(http3only); + System.out.printf("%nstreamTest %b to %s%n" , http3only, uri); + System.err.printf("%nstreamTest %b to %s%n" , http3only, uri); + var config = http3only ? HTTP_3_URI_ONLY : ALT_SVC; + + HttpClient client = getClient(); + Path src = createTempFileOfSize(CLASS_NAME, ".dat", FILESIZE * 4); + HttpRequest req = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, config) + .POST(BodyPublishers.ofFile(src)) + .build(); + + Path dest = Path.of("streamtest.txt"); + dest.toFile().delete(); + CompletableFuture response = client.sendAsync(req, BodyHandlers.ofFile(dest)) + .thenApply(resp -> { + if (resp.statusCode() != 200) + throw new RuntimeException(); + return resp.body(); + }); + response.join(); + assertFileContentsEqual(src, dest); + System.err.println("DONE"); + } + + // expect highest supported version we know about + static String expectedTLSVersion(SSLContext ctx) { + SSLParameters params = ctx.getSupportedSSLParameters(); + String[] protocols = params.getProtocols(); + for (String prot : protocols) { + if (prot.equals("TLSv1.3")) + return "TLSv1.3"; + } + return "TLSv1.2"; + } + + static void paramsTest() throws Exception { + System.out.println("\nparamsTest"); + System.err.println("\nparamsTest"); + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + server.addHandler((t -> { + SSLSession s = t.getSSLSession(); + String prot = s.getProtocol(); + System.err.println("Server: paramsTest: " + prot); + if (prot.equals(expectedTLSVersion(sslContext))) { + t.sendResponseHeaders(200, 0); + } else { + System.err.printf("Protocols =%s%n", prot); + t.sendResponseHeaders(500, 0); + } + }), "/"); + server.start(); + try { + URI u = new URI("https://" + server.serverAuthority() + "/paramsTest"); + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(u).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + HttpResponse resp = client.sendAsync(req, BodyHandlers.ofString()).get(); + int stat = resp.statusCode(); + if (stat != 200) { + throw new RuntimeException("paramsTest failed " + stat); + } + } finally { + server.stop(); + } + } + + static void simpleTest(boolean http3only) throws Exception { + System.out.println("\nsimpleTest http3-only=" + http3only); + System.err.println("\nsimpleTest http3-only=" + http3only); + URI uri = getURI(http3only); + var config = http3only ? HTTP_3_URI_ONLY : ALT_SVC; + System.err.println("Request to " + uri); + + // Do a simple warmup request + + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .setOption(H3_DISCOVERY, config) + .build(); + HttpResponse response = client.sendAsync(req, BodyHandlers.ofString()).get(); + HttpHeaders h = response.headers(); + + checkStatus(200, response.statusCode()); + + String responseBody = response.body(); + checkStrings(SIMPLE_STRING, responseBody); + + checkStrings(h.firstValue("x-hello").get(), "world"); + checkStrings(h.firstValue("x-bye").get(), "universe"); + + // Do loops asynchronously + + CompletableFuture[] responses = new CompletableFuture[LOOPS]; + final Path source = createTempFileOfSize(CLASS_NAME, ".dat", FILESIZE); + HttpRequest request = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, config) + .POST(BodyPublishers.ofFile(source)) + .build(); + for (int i = 0; i < LOOPS; i++) { + Path requestPayloadFile = Utils.createTempFile(CLASS_NAME, ".dat"); + responses[i] = client.sendAsync(request, BodyHandlers.ofFile(requestPayloadFile)) + //.thenApply(resp -> compareFiles(resp.body(), source)); + .thenApply(resp -> { + System.out.printf("Resp status %d body size %d\n", + resp.statusCode(), resp.body().toFile().length()); + assertFileContentsEqual(resp.body(), source); + return null; + }); + } + CompletableFuture.allOf(responses).join(); + System.err.println("DONE"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3GoAwayTest.java b/test/jdk/java/net/httpclient/http3/H3GoAwayTest.java new file mode 100644 index 00000000000..fc6166cb66c --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3GoAwayTest.java @@ -0,0 +1,180 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/* + * @test + * @summary verifies that when the server sends a GOAWAY frame then + * the client correctly handles it and retries unprocessed requests + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * @run junit/othervm + * -Djdk.httpclient.HttpClient.log=errors,headers,quic:hs,http3 + * H3GoAwayTest + */ +public class H3GoAwayTest { + + private static String REQ_URI_BASE; + private static SSLContext sslCtx; + private static Http3TestServer server; + + @BeforeAll + static void beforeAll() throws Exception { + sslCtx = new SimpleSSLContext().get(); + assertNotNull(sslCtx, "SSLContext couldn't be created"); + server = new Http3TestServer(sslCtx); + final RequestApprover reqApprover = new RequestApprover(); + server.setRequestApprover(reqApprover::allowNewRequest); + server.addHandler("/test", new Handler()); + server.start(); + System.out.println("Server started at " + server.getAddress()); + REQ_URI_BASE = URIBuilder.newBuilder().scheme("https") + .loopback() + .port(server.getAddress().getPort()) + .path("/test") + .build().toString(); + } + + @AfterAll + static void afterAll() throws Exception { + if (server != null) { + System.out.println("stopping server at " + server.getAddress()); + server.close(); + } + } + + /** + * Verifies that when several requests are sent using send() and the server + * connection is configured to send a GOAWAY after processing only a few requests, then + * the remaining requests are retried on a different connection + */ + @Test + public void testSequential() throws Exception { + try (final HttpClient client = HttpServerAdapters + .createClientBuilderFor(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(sslCtx).build()) { + final String[] reqMethods = {"HEAD", "GET", "POST"}; + for (final String reqMethod : reqMethods) { + final int numReqs = RequestApprover.MAX_REQS_PER_CONN + 3; + final Set connectionKeys = new LinkedHashSet<>(); + for (int i = 1; i <= numReqs; i++) { + final URI reqURI = new URI(REQ_URI_BASE + "?" + reqMethod + "=" + i); + final HttpRequest req = HttpRequest.newBuilder() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .uri(reqURI) + .method(reqMethod, BodyPublishers.noBody()) + .build(); + System.out.println("initiating request " + req); + final HttpResponse resp = client.send(req, BodyHandlers.ofString()); + final String respBody = resp.body(); + System.out.println("received response: " + respBody); + assertEquals(200, resp.statusCode(), + "unexpected status code for request " + resp.request()); + // response body is the logical key of the connection on which the + // request was handled + connectionKeys.add(respBody); + } + System.out.println("connections involved in handling the requests: " + + connectionKeys); + // all requests have finished, we now just do a basic check that + // more than one connection was involved in processing these requests + assertEquals(2, connectionKeys.size(), + "unexpected number of connections " + connectionKeys); + } + } + } + + private static final class RequestApprover { + private static final int MAX_REQS_PER_CONN = 6; + private final Map numApproved = + new ConcurrentHashMap<>(); + private final Map numDisapproved = + new ConcurrentHashMap<>(); + + public boolean allowNewRequest(final String connKey) { + final AtomicInteger approved = numApproved.computeIfAbsent(connKey, + (k) -> new AtomicInteger()); + int curr = approved.get(); + while (curr < MAX_REQS_PER_CONN) { + if (approved.compareAndSet(curr, curr + 1)) { + return true; // new request allowed + } + curr = approved.get(); + } + final AtomicInteger disapproved = numDisapproved.computeIfAbsent(connKey, + (k) -> new AtomicInteger()); + final int numUnprocessed = disapproved.incrementAndGet(); + System.out.println(approved.get() + " processed, " + + numUnprocessed + " unprocessed requests so far," + + " sending GOAWAY on connection " + connKey); + return false; + } + } + + private static final class Handler implements Http2Handler { + @Override + public void handle(final Http2TestExchange exchange) throws IOException { + final String connectionKey = exchange.getConnectionKey(); + System.out.println(connectionKey + " responding to request: " + exchange.getRequestURI()); + final byte[] response = connectionKey.getBytes(UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java b/test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java new file mode 100644 index 00000000000..7369bb9190e --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java @@ -0,0 +1,162 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.net.ProtocolException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ExecutionException; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.internal.net.http.Http3ConnectionAccess; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.test.lib.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Verifies that the HTTP client respects the SETTINGS_MAX_FIELD_SECTION_SIZE setting on HTTP3 connection + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors H3HeaderSizeLimitTest + */ +public class H3HeaderSizeLimitTest implements HttpServerAdapters { + + private static final long HEADER_SIZE_LIMIT_BYTES = 1024; + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURIBase; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + final QuicServer quicServer = Http3TestServer.quicServerBuilder() + .sslContext(sslContext) + .build(); + h3Server = HttpTestServer.of(new Http3TestServer(quicServer) + .setConnectionSettings(new ConnectionSettings(HEADER_SIZE_LIMIT_BYTES, 0, 0))); + h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(h3Server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + /** + * Issues a HTTP3 request with combined request headers size exceeding the limit set by the + * test server. Verifies that such requests fail. + */ + @Test + public void testLargeHeaderSize() throws Exception { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + // the server drops 1 packet out of two! + .connectTimeout(Duration.ofSeconds(Utils.adjustTimeout(10))) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + // issue a few requests so that enough time has passed to allow the SETTINGS frame from the + // server to have reached the client. + for (int i = 1; i <= 3; i++) { + System.out.println("Issuing warmup request " + i + " to " + reqURI); + final HttpResponse response = client.send( + reqBuilder.build(), + BodyHandlers.discarding()); + Assert.assertEquals(response.statusCode(), 200, "Unexpected status code"); + if (i == 3) { + var cf = Http3ConnectionAccess.peerSettings(client, response); + if (!cf.isDone()) { + System.out.println("Waiting for peer settings"); + cf.join(); + } + System.out.println("Got peer settings: " + cf.get()); + } + } + // at this point the client should have processed the SETTINGS frame the server + // and we expect it to start honouring those settings. We start the real testing now + // create headers that are larger than the headers size limit that has been configured on + // the server by this test + final String headerValue = headerValueOfLargeSize(); + for (int i = 0; i < 10; i++) { + reqBuilder.setHeader("header-" + i, headerValue); + } + final HttpRequest request = reqBuilder.build(); + System.out.println("Issuing request to " + reqURI); + final IOException thrown = Assert.expectThrows(ProtocolException.class, + () -> client.send(request, BodyHandlers.discarding())); + if (!thrown.getMessage().equals("Request headers size exceeds limit set by peer")) { + throw thrown; + } + // test same with async + System.out.println("Issuing async request to " + reqURI); + final ExecutionException asyncThrown = Assert.expectThrows(ExecutionException.class, + () -> client.sendAsync(request, BodyHandlers.discarding()).get()); + if (!(asyncThrown.getCause() instanceof ProtocolException)) { + System.err.println("Received unexpected cause"); + throw asyncThrown; + } + if (!asyncThrown.getCause().getMessage().equals("Request headers size exceeds limit set by peer")) { + System.err.println("Received unexpected message in cause"); + throw asyncThrown; + } + } + + private static String headerValueOfLargeSize() { + return "abcdefgh".repeat(250); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java b/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java new file mode 100644 index 00000000000..e63ae88ed32 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java @@ -0,0 +1,315 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.httpclient.qpack.encoderTableCapacityLimit=4096 + * -Djdk.httpclient.qpack.decoderMaxTableCapacity=4096 + * -Dhttp3.test.server.encoderAllowedHeaders=* + * -Dhttp3.test.server.decoderMaxTableCapacity=4096 + * -Dhttp3.test.server.encoderTableCapacityLimit=4096 + * -Djdk.internal.httpclient.qpack.log.level=NORMAL + * H3HeadersEncoding + * @summary this test verifies that when QPACK dynamic table is enabled multiple + * random headers can be encoded/decoded correctly + */ + +import jdk.test.lib.RandomFactory; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.httpclient.test.lib.common.HttpServerAdapters.*; + +public class H3HeadersEncoding { + + private static final int REQUESTS_COUNT = 500; + private static final int HEADERS_PER_REQUEST = 20; + SSLContext sslContext; + HttpTestServer http3TestServer; + HeadersHandler serverHeadersHandler; + String http3URI; + + @BeforeTest + public void setup() throws Exception { + System.out.println("Creating servers"); + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + serverHeadersHandler = new HeadersHandler(); + http3TestServer.addHandler(serverHeadersHandler, "/http3/headers"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/headers"; + + http3TestServer.start(); + } + + @AfterTest + public void tearDown() { + http3TestServer.stop(); + } + + @Test + public void serialRequests() throws Exception { + try (HttpClient client = newClient()) { + for (int i = 0; i < REQUESTS_COUNT; i++) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(http3URI)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + List rndHeaders = TestHeader.randomHeaders(HEADERS_PER_REQUEST); + String[] requestHeaders = rndHeaders.stream() + .flatMap(th -> Stream.of(th.name(), th.value())) + .toArray(String[]::new); + requestBuilder.headers(requestHeaders); + + // Send client request headers in request body for further check + // on the server handler side + requestBuilder.POST(HttpRequest.BodyPublishers.ofString( + TestHeader.headersToBodyContent(rndHeaders))); + + HttpResponse response = + client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + // Headers received by the client + var serverHeadersClientSide = TestHeader.fromHttpHeaders(response.headers()); + // Headers sent by the server handler + var serverHeadersServerSide = TestHeader.bodyContentToHeaders(response.body()); + + // Check that all headers that server sent are received on client side + checkHeaders(serverHeadersServerSide, serverHeadersClientSide, "client"); + } + } + } + + @Test + public void asyncRequests() throws Exception { + try (HttpClient client = newClient()) { + ArrayList> requestsHeaders = new ArrayList<>(REQUESTS_COUNT); + ArrayList requests = new ArrayList<>(REQUESTS_COUNT); + CopyOnWriteArrayList>> futureReplies + = new CopyOnWriteArrayList<>(); + + // Prepare all requests first + for (int i = 0; i < REQUESTS_COUNT; i++) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(http3URI)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + List rndHeaders = TestHeader.randomHeaders(HEADERS_PER_REQUEST); + String[] requestHeaders = rndHeaders.stream() + .flatMap(th -> Stream.of(th.name(), th.value())) + .toArray(String[]::new); + requestBuilder.headers(requestHeaders); + requestsHeaders.add(i, rndHeaders); + + // Send client request headers in request body for further check + // on the server handler side + requestBuilder.POST(HttpRequest.BodyPublishers.ofString( + TestHeader.headersToBodyContent(rndHeaders))); + requests.add(i, requestBuilder.build()); + } + + // Send async request + for (int i = 0; i < REQUESTS_COUNT; i++) { + CompletableFuture> cf = + client.sendAsync(requests.get(i), + HttpResponse.BodyHandlers.ofString()); + futureReplies.add(i, cf); + } + + // Join on all CF responses + for (int i = 0; i < REQUESTS_COUNT; i++) { + futureReplies.get(i).join(); + } + + // Check the responses + for (int i = 0; i < REQUESTS_COUNT; i++) { + HttpResponse reply = futureReplies.get(i).get(); + // Headers received by the client + var serverHeadersClientSide = TestHeader.fromHttpHeaders(reply.headers()); + // Headers sent by the server handler + var serverHeadersServerSide = TestHeader.bodyContentToHeaders(reply.body()); + // Check that all headers that server sent are received on client side + checkHeaders(serverHeadersServerSide, serverHeadersClientSide, "client"); + } + } + } + + + private static void checkHeaders(List mustPresent, + List allHeaders, + String sideDescription) { + List notFound = new ArrayList<>(); + for (var header : mustPresent) { + if (!allHeaders.contains(header)) { + notFound.add(header); + } + } + if (!notFound.isEmpty()) { + System.err.println("The following headers was not found on " + + sideDescription + " side: " + notFound); + throw new RuntimeException("Headers not found: " + notFound); + } + } + + HttpClient newClient() { + var builder = createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY); + return builder.build(); + } + + private static final Random RND = RandomFactory.getRandom(); + + record TestHeader(String name, String value) { + static TestHeader randomHeader() { + // It is better to have same id generated two or more + // times during the test run, therefore the range below + int headerId = RND.nextInt(10, 10 + HEADERS_PER_REQUEST * 3); + return new TestHeader("test_header" + headerId, "TestValue"); + } + + static List randomHeaders(int count) { + return IntStream + .range(0, count) + .boxed() + .map(ign -> TestHeader.randomHeader()) + .toList(); + } + + static List fromHttpHeaders(HttpHeaders httpHeaders) { + var headersMap = httpHeaders.map(); + return fromHeadersEntrySet(headersMap.entrySet()); + } + + static List fromTestHttpHeaders(HttpTestRequestHeaders requestHeaders) { + var headersSet = requestHeaders.entrySet(); + return fromHeadersEntrySet(headersSet); + } + + private static List fromHeadersEntrySet(Set>> entrySet) { + return entrySet.stream() + .flatMap(entry -> { + var name = entry.getKey(); + return entry.getValue() + .stream() + .map(value -> new TestHeader(name, value)); + }).toList(); + } + + public static String headersToBodyContent(List rndHeaders) { + return rndHeaders.stream() + .map(TestHeader::toString) + .collect(Collectors.joining(System.lineSeparator())); + } + + public static List bodyContentToHeaders(String bodyContent) { + return Arrays.stream(bodyContent.split(System.lineSeparator())) + .filter(Predicate.not(String::isBlank)) + .map(String::strip) + .map(TestHeader::fromBodyHeaderLine) + .toList(); + } + + public static TestHeader fromBodyHeaderLine(String headerLine) { + String[] parts = headerLine.split(":"); + if (parts.length != 2) { + throw new RuntimeException("Internal test error"); + } + return new TestHeader(parts[0], parts[1]); + } + + public String toString() { + return name + ":" + value; + } + } + + + private class HeadersHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + + var clientHeadersServerSide = TestHeader.fromTestHttpHeaders(t.getRequestHeaders()); + + String requestBody; + try (InputStream is = t.getRequestBody()) { + byte[] body = is.readAllBytes(); + requestBody = new String(body); + } + var clientHeadersClientSide = TestHeader.bodyContentToHeaders(requestBody); + + // Check that all headers that client sent are received on tne server side + checkHeaders(clientHeadersClientSide, clientHeadersServerSide, + "server handler"); + + // Response back with a set of random headers + var responseHeaders = t.getResponseHeaders(); + List serverResp = new ArrayList<>(); + for (TestHeader h : TestHeader.randomHeaders(HEADERS_PER_REQUEST)) { + serverResp.add(h); + responseHeaders.addHeader(h.name(), h.value()); + } + + String responseBody = TestHeader.headersToBodyContent(serverResp); + try (OutputStream os = t.getResponseBody()) { + byte[] responseBodyBytes = responseBody.getBytes(); + t.sendResponseHeaders(200, responseBodyBytes.length); + if (!t.getRequestMethod().equals("HEAD")) { + os.write(responseBodyBytes); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java b/test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java new file mode 100644 index 00000000000..735a68c88cf --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java @@ -0,0 +1,258 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * H3ImplicitPushCancel + * @summary This is a clone of http2/ImplicitPushCancel but for HTTP/3 + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; + +public class H3ImplicitPushCancel implements HttpServerAdapters { + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + + HttpTestServer server; + URI uri; + URI headURI; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + HttpTestHandler pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, + PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + System.err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + /* + * With a handler not capable of accepting push promises, then all push + * promises should be rejected / cancelled, without interfering with the + * main response. + */ + @Test + public void test() throws Exception { + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri) + .build(), BodyHandlers.ofString()) + .thenApply(H3ImplicitPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + System.out.println("Got result before error was raised"); + throw new AssertionError("should have failed"); + } catch (CompletionException c) { + Throwable cause = Utils.getCompletionCause(c); + if (cause.getMessage().contains("Max pushId exceeded")) { + System.out.println("Got expected exception: " + cause); + } else throw new AssertionError(cause); + } + + // Send with promise handler + ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + PushPromiseHandler pph = PushPromiseHandler + .of((r) -> BodyHandlers.ofString(), promises); + + HttpResponse main; + try { + main = client.sendAsync( + HttpRequest.newBuilder(uri) + .header("X-WaitForPushId", String.valueOf(1)) + .build(), + BodyHandlers.ofString(), + pph) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + promises.forEach((key, value) -> System.out.println(key + ":" + value.join().body())); + + promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main)); + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + assertEquals(promises.size(), PUSH_PROMISES.size() + 1); + + promises.clear(); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3ImplicitPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + assertEquals(promises.size(), 0); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + public void handle(HttpTestExchange exchange) throws IOException { + System.err.println("Server: handle " + exchange); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long allowed = -1; + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + System.err.println("Got maxPushId: " + allowed); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + for (Map.Entry promise : promises.entrySet()) { + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + exchange.serverPush(uri, headers, is); + } + System.err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java b/test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java new file mode 100644 index 00000000000..7ca07a30ab9 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java @@ -0,0 +1,179 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.test.lib.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Verifies that the HTTP client respects the maxLiteralWithIndexing + * system property value. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm -Djdk.httpclient.qpack.encoderTableCapacityLimit=4096 + * -Djdk.internal.httpclient.qpack.allowBlockingEncoding=true + * -Djdk.httpclient.qpack.decoderMaxTableCapacity=4096 + * -Djdk.httpclient.qpack.decoderBlockedStreams=1024 + * -Dhttp3.test.server.encoderAllowedHeaders=* + * -Dhttp3.test.server.decoderMaxTableCapacity=4096 + * -Dhttp3.test.server.encoderTableCapacityLimit=4096 + * -Djdk.httpclient.maxLiteralWithIndexing=32 + * -Djdk.internal.httpclient.qpack.log.level=EXTRA + * H3InsertionsLimitTest + */ +public class H3InsertionsLimitTest implements HttpServerAdapters { + + private static final long HEADER_SIZE_LIMIT_BYTES = 8192; + private static final long MAX_SERVER_DT_CAPACITY = 4096; + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURIBase; + public static final long MAX_LITERALS_WITH_INDEXING = 32L; + private static final CountDownLatch WAIT_FOR_FAILURE = new CountDownLatch(1); + + private static void handle(HttpTestExchange exchange) throws IOException { + String handlerMsg = "Server handler: " + exchange.getRequestURI(); + long unusedStreamID = 1111; + System.out.println(handlerMsg); + System.err.println(handlerMsg); + + try { + ConnectionSettings settings = exchange.clientHttp3Settings().get(); + System.err.println("Received client connection settings: " + settings); + } catch (Exception e) { + throw new RuntimeException("Test issue: failure awaiting for HTTP/3" + + " connection settings from the client", e); + } + + // Set encoder table capacity explicitly + // to avoid waiting client's settings frame + // that triggers the same DT configuration + Encoder encoder = exchange.qpackEncoder(); + encoder.setTableCapacity(MAX_SERVER_DT_CAPACITY); + // Mimic entry insertions on the server-side + try (Encoder.EncodingContext context = encoder.newEncodingContext( + unusedStreamID, 0, encoder.newHeaderFrameWriter())) { + for (int i = 0; i <= MAX_LITERALS_WITH_INDEXING; i++) { + var entry = new TableEntry("n" + i, "v" + i); + var insertedEntry = context.tryInsertEntry(entry); + if (insertedEntry.index() == -1L) { + throw new RuntimeException("Test issue: cannot insert" + + " entry to the encoder dynamic table"); + } + } + } + try { + WAIT_FOR_FAILURE.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Test Issue: handler interrupted", e); + } + // Send a response + exchange.sendResponseHeaders(200, 0); + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + final QuicServer quicServer = Http3TestServer.quicServerBuilder() + .bindAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)) + .sslContext(sslContext) + .build(); + var http3TestServer = new Http3TestServer(quicServer) + .setConnectionSettings(new ConnectionSettings( + HEADER_SIZE_LIMIT_BYTES, MAX_SERVER_DT_CAPACITY, 0)); + + h3Server = HttpTestServer.of(http3TestServer); + h3Server.addHandler(H3InsertionsLimitTest::handle, "/insertions"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(h3Server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + @Test + public void multipleTableInsertions() throws Exception { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + // the server drops 1 packet out of two! + .connectTimeout(Duration.ofSeconds(Utils.adjustTimeout(10))) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURIBase + "/insertions"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + final HttpRequest request = + reqBuilder.POST(HttpRequest.BodyPublishers.ofString("Hello")).build(); + System.out.println("Issuing request to " + reqURI); + try { + client.send(request, BodyHandlers.discarding()); + Assert.fail("IOException expected"); + } catch (IOException ioe) { + System.out.println("Got IOException: " + ioe); + Assert.assertTrue(ioe.getMessage() + .contains("Too many literal with indexing")); + } finally { + WAIT_FOR_FAILURE.countDown(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java b/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java new file mode 100644 index 00000000000..06b12b6a477 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java @@ -0,0 +1,437 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HexFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @summary Verifies that the HTTP client correctly handles malformed responses + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors H3MalformedResponseTest + */ +public class H3MalformedResponseTest implements HttpServerAdapters { + + private SSLContext sslContext; + private QuicStandaloneServer server; + private String requestURIBase; + + // These responses are malformed and should not be accepted by the client, + // but they should not cause connection closure + @DataProvider + public static Object[][] malformedResponse() { + return new Object[][]{ + new Object[] {"empty", HexFormat.of().parseHex( + "" + )}, + new Object[] {"non-final response", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00" // :status:100 + )}, + new Object[] {"uppercase header name", HexFormat.of().parseHex( + "01090000"+ // headers, length 9, section prefix + "d9"+ // :status:200 + "234147450130"+ // AGE:0 + "000100" // data, 1 byte + )}, + new Object[] {"content too long", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "d9"+ // :status:200 + "c4"+ // content-length:0 + "000100" // data, 1 byte + )}, + new Object[] {"content too short", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "540132"+ // content-length:2 + "000100" // data, 1 byte + )}, + new Object[] {"text in content-length", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "540161"+ // content-length:a + "000100" // data, 1 byte + )}, + new Object[] {"connection: close", HexFormat.of().parseHex( + "01150000"+ // headers, length 21, section prefix + "d9"+ // :status:200 + "2703636F6E6E656374696F6E05636C6F7365"+ // connection:close + "000100" // data, 1 byte + )}, + // request pseudo-headers in response + new Object[] {":method in response", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "d9"+ // :status:200 + "d1"+ // :method:get + "000100" // data, 1 byte + )}, + new Object[] {":authority in response", HexFormat.of().parseHex( + "01100000"+ // headers, length 16, section prefix + "d9"+ // :status:200 + "508b089d5c0b8170dc702fbce7"+ // :authority + "000100" // data, 1 byte + )}, + new Object[] {":path in response", HexFormat.of().parseHex( + "010a0000"+ // headers, length 10, section prefix + "d9"+ // :status:200 + "51856272d141ff"+ // :path + "000100" // data, 1 byte + )}, + new Object[] {":scheme in response", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "d9"+ // :status:200 + "d7"+ // :scheme:https + "000100" // data, 1 byte + )}, + new Object[] {"undefined pseudo-header", HexFormat.of().parseHex( + "01080000"+ // headers, length 8, section prefix + "d9"+ // :status:200 + "223A6D0130"+ // :m:0 + "000100" // data, 1 byte + )}, + new Object[] {"pseudo-header after regular", HexFormat.of().parseHex( + "011a0000"+ // headers, length 26, section prefix + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"+ //user-agent + "d9"+ // :status:200 + "000100" // data, 1 byte + )}, + new Object[] {"trailer", HexFormat.of().parseHex( + "01020000" // headers, length 2, section prefix + )}, + new Object[] {"trailer+data", HexFormat.of().parseHex( + "01020000"+ // headers, length 2, section prefix + "000100" // data, 1 byte + )}, + // valid characters include \t, 0x20-0x7e, 0x80-0xff (RFC 9110, section 5.5) + new Object[] {"invalid character in field value 00", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "570100"+ // etag:\0 + "000100" // data, 1 byte + )}, + new Object[] {"invalid character in field value 0a", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "57010a"+ // etag:\n + "000100" // data, 1 byte + )}, + new Object[] {"invalid character in field value 0d", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "57010d"+ // etag:\r + "000100" // data, 1 byte + )}, + new Object[] {"invalid character in field value 7f", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "57017f"+ // etag: 0x7f + "000100" // data, 1 byte + )}, + }; + } + + // These responses are malformed and should not be accepted by the client. + // They might or might not cause connection closure (H3_FRAME_UNEXPECTED) + @DataProvider + public static Object[][] malformedResponse2() { + // data before headers is covered by H3ErrorHandlingTest + return new Object[][]{ + new Object[] {"100+data", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00"+ // :status:100 + "000100" // data, 1 byte + )}, + new Object[] {"100+data+200", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00"+ // :status:100 + "000100"+ // data, 1 byte + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+data+200", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+data+100", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01040000"+ // headers, length 4, section prefix + "ff00" // :status:100 + )}, + new Object[] {"200+data+trailers+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01020000"+ // trailers, length 2, section prefix + "000100" // data, 1 byte + )}, + new Object[] {"200+trailers+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01020000"+ // trailers, length 2, section prefix + "000100" // data, 1 byte + )}, + new Object[] {"200+200", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+100", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01040000"+ // headers, length 4, section prefix + "ff00" // :status:100 + )}, + }; + } + + @DataProvider + public static Object[][] wellformedResponse() { + return new Object[][]{ + new Object[] {"100+200+data+reserved", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00"+ // :status:100 + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "210100" // reserved, 1 byte + )}, + new Object[] {"200+data+reserved", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "210100" // reserved, 1 byte + )}, + new Object[] {"200+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100" // data, 1 byte + )}, + new Object[] {"200+user-agent+data", HexFormat.of().parseHex( + "011a0000"+ // headers, length 26, section prefix + "d9"+ // :status:200 + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"+ //user-agent + "000100" // data, 1 byte + )}, + new Object[] {"200", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+data+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "000100" // data, 1 byte + )}, + new Object[] {"200+data+trailers", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01020000" // trailers, length 2, section prefix + )}, + new Object[] {"200+trailers", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01020000" // trailers, length 2, section prefix + )}, + }; + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .alpn("h3") + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + } + + /** + * Server sends a well-formed response + */ + @Test(dataProvider = "wellformedResponse") + public void testWellFormedResponse(String desc, byte[] response) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + } + // verify that the connection stays open + completeUponTermination(c, errorCF); + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + assertEquals(response1.statusCode(), 200); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + } finally { + client.shutdownNow(); + } + } + + + /** + * Server sends a malformed response that should not close connection + */ + @Test(dataProvider = "malformedResponse") + public void testMalformedResponse(String desc, byte[] response) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + } + // verify that the connection stays open + completeUponTermination(c, errorCF); + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response1); + } catch (TimeoutException e) { + throw e; + } catch (Exception e) { + System.out.println("Got expected exception: " +e); + e.printStackTrace(); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + } finally { + client.shutdownNow(); + } + } + + /** + * Server sends a malformed response that might close connection + */ + @Test(dataProvider = "malformedResponse2") + public void testMalformedResponse2(String desc, byte[] response) throws Exception { + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + } + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response1); + } catch (TimeoutException e) { + throw e; + } catch (Exception e) { + System.out.println("Got expected exception: " +e); + e.printStackTrace(); + } finally { + client.shutdownNow(); + } + } + + private HttpRequest getRequest() throws URISyntaxException { + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + return reqBuilder.build(); + } + + private HttpClient getHttpClient() { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(sslContext).build(); + return client; + } + + private static String toHexString(final Http3Error error) { + return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; + } + + private static void completeUponTermination(final QuicServerConnection serverConnection, + final CompletableFuture cf) { + serverConnection.futureTerminationCause().handle( + (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java b/test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java new file mode 100644 index 00000000000..30f48c56454 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java @@ -0,0 +1,250 @@ +/* + * 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. + * + * 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. + */ + +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.channels.DatagramChannel; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static java.lang.System.out; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.assertEquals; + + +/* + * @test + * @bug 8342954 + * @summary Verify jdk.httpclient.quic.maxInitialTimeout is taken into account. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.quic.QuicStandaloneServer + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,quic:controls + * -Djdk.httpclient.quic.maxInitialTimeout=1 + * H3MaxInitialTimeoutTest + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,quic:controls + * -Djdk.httpclient.quic.maxInitialTimeout=2 + * H3MaxInitialTimeoutTest + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,quic:controls + * -Djdk.httpclient.quic.maxInitialTimeout=2147483647 + * H3MaxInitialTimeoutTest + */ +public class H3MaxInitialTimeoutTest implements HttpServerAdapters { + + SSLContext sslContext; + DatagramChannel receiver; + String h3URI; + + static final Executor executor = new TestExecutor(Executors.newVirtualThreadPerTaskExecutor()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + @DataProvider(name = "h3URIs") + public Object[][] versions(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient(long connectionTimeout) { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(connectionTimeout)) + .build(); + return client; + } + + @Test(dataProvider = "h3URIs") + public void testTimeout(final String h3URI) throws Exception { + long timeout = Long.getLong("jdk.httpclient.quic.maxInitialTimeout", 30); + long connectionTimeout = timeout == Integer.MAX_VALUE ? 2 : 10 * timeout; + + try (HttpClient client = makeNewClient(connectionTimeout)) { + URI uri = URI.create(h3URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + HttpRequest request = builder.build(); + try { + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_3, "first response version"); + throw new AssertionError("Expected ConnectException not thrown"); + } catch (ConnectException c) { + String msg = c.getMessage(); + if (timeout != Integer.MAX_VALUE) { + if (msg != null && msg.contains("No response from peer")) { + out.println("Got expected exception: " + c); + } else throw c; + } else throw c; + } catch (HttpConnectTimeoutException hc) { + String msg = hc.getMessage(); + if (timeout == Integer.MAX_VALUE) { + if (msg != null && msg.contains("No response from peer")) { + throw new AssertionError("Unexpected message: " + msg, hc); + } else { + out.println("Got expected exception: " + hc); + return; + } + } else throw hc; + } + } + + } + + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + receiver = DatagramChannel.open(); + receiver.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + h3URI = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(((InetSocketAddress)receiver.getLocalAddress()).getPort()) + .path("/") + .build() + .toString(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + receiver.close(); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java b/test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java new file mode 100644 index 00000000000..f270297d85c --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java @@ -0,0 +1,232 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HexFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @summary Verifies that the HTTP client does not buffer excessive amounts of data + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djdk.httpclient.quic.maxStreamInitialData=16384 + * -Djdk.httpclient.quic.streamBufferSize=2048 H3MemoryHandlingTest + */ +public class H3MemoryHandlingTest implements HttpServerAdapters { + + private SSLContext sslContext; + private QuicStandaloneServer server; + private String requestURIBase; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .alpn("h3") + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + } + + /** + * Server sends a large response, and the user code does not read from the input stream. + * Writing on the server side should block once the buffers are full. + */ + @Test + public void testOfInputStreamBlocks() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + byte[] response = HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "00ffffffffffffffff"); // data, 2^62 - 1 bytes + byte[] kilo = new byte[1024]; + final CompletableFuture serverAllWritesDone = new CompletableFuture<>(); + server.addHandler((c,s)-> { + // verify that the connection stays open + completeUponTermination(c, errorCF); + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + for (int i = 0; i < 20; i++) { + // 18 writes should succeed, 19th should block + outputStream.write(kilo); + System.out.println("Wrote "+(i+1)+"KB"); + } + // all 20 writes unexpectedly completed + serverAllWritesDone.complete(true); + } catch(IOException ex) { + System.out.println("Got expected exception: " + ex); + serverAllWritesDone.complete(false); + } + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.send( + request, BodyHandlers.ofInputStream()); + assertEquals(response1.statusCode(), 200); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + assertFalse(serverAllWritesDone.isDone()); + response1.body().close(); + final boolean done = serverAllWritesDone.get(10, TimeUnit.SECONDS); + assertFalse(done, "Too much data was buffered by the client"); + } finally { + client.close(); + } + } + + /** + * Server sends a large response, and the user code does not read from the input stream. + * Writing on the server side should unblock once the client starts receiving. + */ + @Test + public void testOfInputStreamUnblocks() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + CompletableFuture handlerCF = new CompletableFuture<>(); + byte[] response = HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "0080008000"); // data, 32 KB + byte[] kilo = new byte[1024]; + CountDownLatch writerBlocked = new CountDownLatch(1); + + server.addHandler((c,s)-> { + // verify that the connection stays open + completeUponTermination(c, errorCF); + QuicBidiStream qs = s.underlyingBidiStream(); + + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + for (int i = 0;i < 32;i++) { + // 18 writes should succeed, 19th should block + if (i == 18) { + writerBlocked.countDown(); + } + outputStream.write(kilo); + System.out.println("Wrote "+(i+1)+"KB"); + } + handlerCF.complete(true); + } catch (IOException e) { + handlerCF.completeExceptionally(e); + } + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.send( + request, BodyHandlers.ofInputStream()); + assertEquals(response1.statusCode(), 200); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + assertFalse(handlerCF.isDone()); + assertTrue(writerBlocked.await(10, TimeUnit.SECONDS), + "write of 18 KB should have succeeded"); + System.out.println("Wait completed, receiving response"); + byte[] receivedResponse; + try (InputStream body = response1.body()) { + receivedResponse = body.readAllBytes(); + } + assertEquals(receivedResponse.length, 32768, + "Unexpected response length"); + } finally { + client.close(); + } + assertTrue(handlerCF.get(10, TimeUnit.SECONDS), + "Unexpected result"); + } + + private HttpRequest getRequest() throws URISyntaxException { + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + return reqBuilder.build(); + } + + private HttpClient getHttpClient() { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(sslContext).build(); + return client; + } + + private static String toHexString(final Http3Error error) { + return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; + } + + private static void completeUponTermination(final QuicServerConnection serverConnection, + final CompletableFuture cf) { + serverConnection.futureTerminationCause().handle( + (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java b/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java new file mode 100644 index 00000000000..45024c58e1f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java @@ -0,0 +1,338 @@ +/* + * 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 + * 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 id=with-continuations + * @bug 8087112 + * @requires os.family != "windows" | ( os.name != "Windows 10" & os.name != "Windows Server 2016" + * & os.name != "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=90 + * -Djdk.httpclient.quic.maxPtoBackoff=10 + * -Djdk.internal.httpclient.quic.useNioSelector=false + * -Djdk.internal.httpclient.quic.poller.usePlatformThreads=false + * -Djdk.httpclient.quic.maxEndpoints=-1 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:retransmit + * -Dsimpleget.requests=100 + * H3MultipleConnectionsToSameHost + * @summary test multiple connections and concurrent requests with blocking IO and virtual threads + */ +/* + * @test id=without-continuations + * @bug 8087112 + * @requires os.family == "windows" & ( os.name == "Windows 10" | os.name == "Windows Server 2016" + * | os.name == "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.minPtoBackoffTime=45 + * -Djdk.httpclient.quic.maxPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.internal.httpclient.quic.useNioSelector=false + * -Djdk.internal.httpclient.quic.poller.usePlatformThreads=false + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * -Djdk.httpclient.quic.maxEndpoints=-1 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:retransmit + * -Dsimpleget.requests=100 + * H3MultipleConnectionsToSameHost + * @summary test multiple connections and concurrent requests with blocking IO and virtual threads + * on windows 10 and windows 2016 - but with -XX:-VMContinuations + */ +/* + * @test id=useNioSelector + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.idleTimeout=120 + * -Djdk.httpclient.keepalive.timeout.h3=120 + * -Djdk.test.server.quic.idleTimeout=90 + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=120 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.internal.httpclient.quic.useNioSelector=true + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxEndpoints=1 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:hs:retransmit + * -Dsimpleget.requests=100 + * H3MultipleConnectionsToSameHost + * @summary Send 100 large concurrent requests, with connections whose max stream + * limit is artificially low, in order to cause concurrent connections + * to the same host to be created, with non-blocking IO and selector + */ + +// Interesting additional settings for debugging and manual testing: +// ----------------------------------------------------------------- +// -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations +// -Djdk.httpclient.HttpClient.log=errors,requests,http3,quic +// -Djdk.httpclient.HttpClient.log=requests,errors,quic:retransmit:control,http3 +// -Djdk.httpclient.HttpClient.log=errors,requests,quic:all +// -Djdk.httpclient.quic.defaultMTU=64000 +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.httpclient.quic.defaultMTU=4096 +// -Djdk.httpclient.http3.maxStreamLimitTimeout=1375 +// -Xmx16g +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.internal.httpclient.debug=err +// -XX:+HeapDumpOnOutOfMemoryError +// -Djdk.httpclient.HttpClient.log=errors,quic:cc +// -Djdk.httpclient.quic.sendBufferSize=16384 +// -Djdk.httpclient.quic.receiveBufferSize=16384 + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.internal.net.http.Http3ClientProperties.MAX_STREAM_LIMIT_WAIT_TIMEOUT; + +public class H3MultipleConnectionsToSameHost implements HttpServerAdapters { + static HttpTestServer httpsServer; + static HttpClient client = null; + static SSLContext sslContext; + static String httpsURIString; + static ExecutorService serverExec = + Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("server-vt-worker-", 1).factory()); + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + httpsServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, serverExec); + httpsServer.addHandler(new TestHandler(), "/"); + httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + + httpsServer.start(); + warmup(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + private static void warmup() throws Exception { + SimpleSSLContext sslct = new SimpleSSLContext(); + var sslContext = sslct.get(); + + // warmup server + try (var client2 = createClient(sslContext, Executors.newVirtualThreadPerTaskExecutor())) { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client2.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } + + // warmup client + var httpsServer2 = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, + Executors.newVirtualThreadPerTaskExecutor()); + httpsServer2.addHandler(new TestHandler(), "/"); + var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/"; + httpsServer2.start(); + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } finally { + httpsServer2.stop(); + } + } + + public static void main(String[] args) throws Exception { + test(); + } + + @Test + public static void test() throws Exception { + try { + long prestart = System.nanoTime(); + initialize(); + long done = System.nanoTime(); + System.out.println("Initialization and warmup took "+ TimeUnit.NANOSECONDS.toMillis(done-prestart)+" millis"); + // Thread.sleep(30000); + int maxBidiStreams = Utils.getIntegerNetProperty("jdk.httpclient.quic.maxBidiStreams", 100); + long timeout = MAX_STREAM_LIMIT_WAIT_TIMEOUT; + + Set connections = new ConcurrentSkipListSet<>(); + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET().build(); + long start = System.nanoTime(); + var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("First request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = property("simpleget.requests", 50); + List>> list = new ArrayList<>(max); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + int rqNum = i; + var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b-> {})) + .whenComplete((r, t) -> { + Optional.ofNullable(r) + .flatMap(HttpResponse::connectionLabel) + .ifPresent(connections::add); + if (r != null) { + System.out.println(rqNum + " completed: " + r.connectionLabel()); + } else { + System.out.println(rqNum + " failed: " + t); + } + }); + list.add(cf); + //cf.get(); // uncomment to test with serial instead of concurrent requests + } + try { + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + } finally { + long elapsed2 = System.nanoTime() - start2; + long completed = list.stream().filter(CompletableFuture::isDone) + .filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count(); + if (completed > 0) { + System.out.println("Next " + completed + " requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): " + + elapsed2 / completed + " nanos per request (" + TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + + " ms) on " + connections.size() + " connections"); + } + if (completed == list.size()) { + long msPerRequest = TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed; + if (timeout == 0 || timeout < msPerRequest) { + int expectedCount = max / maxBidiStreams; + if (expectedCount > 2) { + if (connections.size() < expectedCount - Math.max(1, expectedCount/5)) { + throw new AssertionError( + "Too few connections: %s for %s requests with %s streams/connection (timeout %s ms)" + .formatted(connections.size(), max, maxBidiStreams, timeout)); + } + } + } + if (connections.size() > max - Math.max(1, max/5)) { + throw new AssertionError( + "Too few connections: %s for %s requests with %s streams/connection (timeout %s ms)" + .formatted(connections.size(), max, maxBidiStreams, timeout)); + } + + } + } + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + client.close(); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext, ExecutorService clientExec) { + var builder = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(Builder.NO_PROXY); + if (clientExec != null) { + builder = builder.executor(clientExec); + } + return builder.build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext, null); + } + return client; + } + + static int property(String name, int defaultValue) { + return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); + } + + // 32 * 32 * 1024 * 10 chars = 10Mb responses + // 50 requests => 500Mb + // 100 requests => 1Gb + // 1000 requests => 10Gb + private final static int REPEAT = property("simpleget.repeat", 32); + private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32)); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + try (var in = t.getRequestBody()) { + byte[] input = in.readAllBytes(); + t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT); + try (var out = t.getResponseBody()) { + if (t.getRequestMethod().equals("HEAD")) return; + for (int i=0; i { + he.getResponseHeaders().addHeader("encoding", "UTF-8"); + he.sendResponseHeaders(200, RESPONSE.length()); + if (!he.getRequestMethod().equals("HEAD")) { + he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); + } + he.close(); + }, PATH); + + return server; + } + + static HttpTestServer createHttp3Server() throws Exception { + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + server.addHandler(he -> { + he.getResponseHeaders().addHeader("encoding", "UTF-8"); + he.sendResponseHeaders(200, RESPONSE.length()); + if (!he.getRequestMethod().equals("HEAD")) { + he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); + } + he.close(); + }, PATH); + + return server; + } + + public static void main(String[] args) + throws Exception + { + HttpTestServer server = createHttps2Server(); + HttpTestServer server3 = createHttp3Server(); + server.start(); + server3.start(); + try { + if (server.supportsH3DirectConnection()) { + test(server, ANY); + try { + test(server, HTTP_3_URI_ONLY); + throw new AssertionError("expected UnsupportedProtocolVersionException not raised"); + } catch (UnsupportedProtocolVersionException upve) { + System.out.printf("%nGot expected exception: %s%n%n", upve); + } + } + test(server, ALT_SVC); + try { + test(server3, HTTP_3_URI_ONLY); + throw new AssertionError("expected UnsupportedProtocolVersionException not raised"); + } catch (UnsupportedProtocolVersionException upve) { + System.out.printf("%nGot expected exception: %s%n%n", upve); + } + } finally { + server.stop(); + server3.stop(); + System.out.println("Server stopped"); + } + } + + public static void test(HttpTestServer server, + Http3DiscoveryMode config) + throws Exception + { + System.out.println(""" + + # -------------------------------------------------- + # Server is %s + # Config is %s + # -------------------------------------------------- + """.formatted(server.getAddress(), config)); + + URI uri = new URI("https://" + server.serverAuthority() + PATH + "x"); + TunnelingProxy proxy = new TunnelingProxy(server); + proxy.start(); + try { + System.out.println("Proxy started"); + System.out.println("\nSetting up request with HttpClient for version: " + + config.name() + " URI=" + uri); + ProxySelector ps = ProxySelector.of( + InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); + HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .proxy(ps) + .build(); + try (client) { + if (config == ALT_SVC) { + System.out.println("\nSending HEAD request to preload AltServiceRegistry"); + HttpRequest head = HttpRequest.newBuilder() + .uri(uri) + .HEAD() + .version(Version.HTTP_2) + .build(); + var headResponse = client.send(head, BodyHandlers.ofString()); + System.out.println("Got head response: " + headResponse); + if (headResponse.statusCode() != 200) { + throw new AssertionError("bad status code: " + headResponse); + } + if (!headResponse.version().equals(Version.HTTP_2)) { + throw new AssertionError("bad protocol version: " + headResponse.version()); + } + + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .GET() + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, config) + .build(); + + System.out.println("\nSending request with HttpClient: " + config); + HttpResponse response + = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("Got response"); + if (response.statusCode() != 200) { + throw new AssertionError("bad status code: " + response); + } + if (!response.version().equals(Version.HTTP_2)) { + throw new AssertionError("bad protocol version: " + response.version()); + } + + String resp = response.body(); + System.out.println("Received: " + resp); + if (!RESPONSE.equals(resp)) { + throw new AssertionError("Unexpected response"); + } + } + } catch (Throwable t) { + System.out.println("Error: " + t); + throw t; + } finally { + System.out.println("Stopping proxy"); + proxy.stop(); + System.out.println("Proxy stopped"); + } + } + + static class TunnelingProxy { + final Thread accept; + final ServerSocket ss; + final boolean DEBUG = false; + final HttpTestServer serverImpl; + final CopyOnWriteArrayList> connectionCFs + = new CopyOnWriteArrayList<>(); + private volatile boolean stopped; + TunnelingProxy(HttpTestServer serverImpl) throws IOException { + this.serverImpl = serverImpl; + ss = new ServerSocket(); + accept = new Thread(this::accept); + accept.setDaemon(true); + } + + void start() throws IOException { + ss.setReuseAddress(false); + ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + accept.start(); + } + + // Pipe the input stream to the output stream. + private synchronized Thread pipe(InputStream is, OutputStream os, + char tag, CompletableFuture end) { + return new Thread("TunnelPipe("+tag+")") { + @Override + public void run() { + try { + try (os) { + int c; + while ((c = is.read()) != -1) { + os.write(c); + os.flush(); + // if DEBUG prints a + or a - for each transferred + // character. + if (DEBUG) System.out.print(tag); + } + is.close(); + } + } catch (IOException ex) { + if (DEBUG) ex.printStackTrace(System.out); + } finally { + end.complete(null); + } + } + }; + } + + public InetSocketAddress getAddress() { + return new InetSocketAddress( InetAddress.getLoopbackAddress(), ss.getLocalPort()); + } + + // This is a bit shaky. It doesn't handle continuation + // lines, but our client shouldn't send any. + // Read a line from the input stream, swallowing the final + // \r\n sequence. Stops at the first \n, doesn't complain + // if it wasn't preceded by '\r'. + // + String readLine(InputStream r) throws IOException { + StringBuilder b = new StringBuilder(); + int c; + while ((c = r.read()) != -1) { + if (c == '\n') break; + b.appendCodePoint(c); + } + if (b.codePointAt(b.length() -1) == '\r') { + b.delete(b.length() -1, b.length()); + } + return b.toString(); + } + + public void accept() { + Socket clientConnection; + try { + while (!stopped) { + System.out.println("Tunnel: Waiting for client"); + Socket toClose; + try { + toClose = clientConnection = ss.accept(); + } catch (IOException io) { + if (DEBUG) io.printStackTrace(System.out); + break; + } + System.out.println("Tunnel: Client accepted"); + Socket targetConnection; + InputStream ccis = clientConnection.getInputStream(); + OutputStream ccos = clientConnection.getOutputStream(); + Writer w = new OutputStreamWriter(ccos, StandardCharsets.UTF_8); + PrintWriter pw = new PrintWriter(w); + System.out.println("Tunnel: Reading request line"); + String requestLine = readLine(ccis); + System.out.println("Tunnel: Request status line: " + requestLine); + if (requestLine.startsWith("CONNECT ")) { + // We should probably check that the next word following + // CONNECT is the host:port of our HTTPS serverImpl. + // Some improvement for a followup! + + // Read all headers until we find the empty line that + // signals the end of all headers. + while(!requestLine.equals("")) { + System.out.println("Tunnel: Reading header: " + + (requestLine = readLine(ccis))); + } + + // Open target connection + targetConnection = new Socket( + InetAddress.getLoopbackAddress(), + serverImpl.getAddress().getPort()); + + // Then send the 200 OK response to the client + System.out.println("Tunnel: Sending " + + "HTTP/1.1 200 OK\r\n\r\n"); + pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + pw.flush(); + } else { + // This should not happen. If it does then just print an + // error - both on out and err, and close the accepted + // socket + System.out.println("WARNING: Tunnel: Unexpected status line: " + + requestLine + " received by " + + ss.getLocalSocketAddress() + + " from " + + toClose.getRemoteSocketAddress() + + " - closing accepted socket"); + // Print on err + System.err.println("WARNING: Tunnel: Unexpected status line: " + + requestLine + " received by " + + ss.getLocalSocketAddress() + + " from " + + toClose.getRemoteSocketAddress()); + // close accepted socket. + toClose.close(); + System.err.println("Tunnel: accepted socket closed."); + continue; + } + + // Pipe the input stream of the client connection to the + // output stream of the target connection and conversely. + // Now the client and target will just talk to each other. + System.out.println("Tunnel: Starting tunnel pipes"); + CompletableFuture end, end1, end2; + Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+', + end1 = new CompletableFuture<>()); + Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-', + end2 = new CompletableFuture<>()); + end = CompletableFuture.allOf(end1, end2); + end.whenComplete( + (r,t) -> { + try { toClose.close(); } catch (IOException x) { } + finally {connectionCFs.remove(end);} + }); + connectionCFs.add(end); + t1.start(); + t2.start(); + } + } catch (Throwable ex) { + try { + ss.close(); + } catch (IOException ex1) { + ex.addSuppressed(ex1); + } + ex.printStackTrace(System.err); + } finally { + System.out.println("Tunnel: exiting (stopped=" + stopped + ")"); + connectionCFs.forEach(cf -> cf.complete(null)); + } + } + + public void stop() throws IOException { + stopped = true; + ss.close(); + try { + if (accept != Thread.currentThread()) accept.join(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3PushCancel.java b/test/jdk/java/net/httpclient/http3/H3PushCancel.java new file mode 100644 index 00000000000..a5293444ade --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3PushCancel.java @@ -0,0 +1,508 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * -Djdk.httpclient.http3.maxConcurrentPushStreams=5 + * H3PushCancel + * @summary This test checks that not accepting one of the push promise + * will cancel it. It also verifies that receiving a pushId bigger + * than the max push ID allowed on the connection will cause + * the exchange to fail and the connection to get closed. + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.nio.channels.ClosedChannelException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class H3PushCancel implements HttpServerAdapters { + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + + HttpTestServer server; + URI uri; + URI headURI; + ServerPushHandler pushHandler; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + System.err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + @Test + public void testNoCancel() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + System.out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + PushPromiseHandler pph = PushPromiseHandler + .of((r) -> BodyHandlers.ofString(), promises); + + for (int j=0; j < 2; j++) { + if (j == 0) System.out.println("\ntestNoCancel: First time around"); + else System.out.println("\ntestNoCancel: Second time around: should be a new connection"); + + int waitForPushId; + for (int i = 0; i < 3; i++) { + HttpResponse main; + waitForPushId = i * Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + try { + main = client.sendAsync( + HttpRequest.newBuilder(uri) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + pph) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + promises.forEach((key, value1) -> System.out.println(key + ":" + value1.join().body())); + + promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main)); + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + assertEquals(promises.size(), Math.min(PUSH_PROMISES.size(), maxPushes) + 1); + + promises.clear(); + } + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes * 3))) { + success = true; + } + if (success) { + System.out.println("Got expected IOException: " + io); + } else throw io; + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection + // so we can restart from scratch + pushHandler.reset(); + } + } + } + + @Test + public void testCancel() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + System.out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + PushPromiseHandler pph = PushPromiseHandler + .of((r) -> BodyHandlers.ofString(), promises); + record NotifiedPromise(PushId pushId, HttpRequest initiatingRequest) {} + final Map requestToPushId = new ConcurrentHashMap<>(); + final Map pushIdToRequest = new ConcurrentHashMap<>(); + final List errors = new CopyOnWriteArrayList<>(); + final List notified = new CopyOnWriteArrayList<>(); + PushPromiseHandler custom = new PushPromiseHandler<>() { + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor); + } + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushid) { + notified.add(new NotifiedPromise(pushid, initiatingRequest)); + pph.notifyAdditionalPromise(initiatingRequest, pushid); + } + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function, CompletableFuture>> acceptor) { + System.out.println("applyPushPromise: " + pushPromiseRequest + ", pushId=" + pushid); + requestToPushId.putIfAbsent(pushPromiseRequest, pushid); + if (pushIdToRequest.putIfAbsent(pushid, pushPromiseRequest) != null) { + errors.add(new AssertionError("pushId already used: " + pushid)); + } + if (pushid instanceof Http3PushId http3PushId) { + if (http3PushId.pushId() == 1) { + System.out.println("Cancelling: " + http3PushId); + return; // cancel pushId == 1 + } + } + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, pushid, acceptor); + } + }; + + for (int j=0; j < 2; j++) { + if (j == 0) System.out.println("\ntestCancel: First time around"); + else System.out.println("\ntestCancel: Second time around: should be a new connection"); + + int waitForPushId; + for (int i = 0; i < 3; i++) { + HttpResponse main; + waitForPushId = i * Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + try { + main = client.sendAsync( + HttpRequest.newBuilder(uri) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + custom) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + promises.forEach((key, value) -> System.out.println(key + ":" + value.join().body())); + + promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main)); + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + int expectedPushes = Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + if (i == 0) expectedPushes--; // pushId == 1 was cancelled + assertEquals(promises.size(), expectedPushes); + + promises.clear(); + } + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body ->assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes * 3))) { + success = true; + } + if (success) { + System.out.println("Got expected IOException: " + io); + } else throw io; + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection + // so we can restart from scratch + pushHandler.reset(); + } + errors.forEach(t -> t.printStackTrace(System.out)); + var error = errors.stream().findFirst().orElse(null); + if (error != null) throw error; + assertEquals(notified.size(), 0, "Unexpected notification: " + notified); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + private final ReentrantLock lock = new ReentrantLock(); + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + final AtomicInteger count = new AtomicInteger(); + public void handle(HttpTestExchange exchange) throws IOException { + long count = -1; + lock.lock(); + try { + count = this.count.incrementAndGet(); + System.err.println("Server: handle " + exchange); + System.out.println("Server: handle " + exchange.getRequestURI()); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } catch (ClosedChannelException ex) { + System.out.printf("handling exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + System.out.printf("Got closed channel exception sending response after sent=%s allowed=%s%n", + sent, allowed); + } + } finally { + lock.unlock(); + System.out.printf("handled exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + } + } + + volatile long allowed = -1; + volatile int sent = 0; + void reset() { + lock.lock(); + try { + allowed = -1; + sent = 0; + } finally { + lock.unlock(); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long usePushId = exchange.getRequestHeaders() + .firstValueAsLong("X-UsePushId").orElse(-1); + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + System.err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + System.err.println("Server: Got maxPushId: " + allowed); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + for (Map.Entry promise : promises.entrySet()) { + // if usePushId != -1 we send a single push promise, + // without checking that's it's allowed. + // Otherwise, we stop sending promises when we have consumed + // the whole window + if (usePushId == -1 && allowed > 0 && sent >= allowed) { + System.err.println("Server: sent all allowed promises: " + sent); + break; + } + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + System.err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + System.err.println("Server: Got maxPushId: " + allowed); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + if (usePushId == -1) { + long pushId = exchange.http3ServerPush(uri, headers, headers, is); + System.err.println("Server: Sent push promise with response: " + pushId); + waitForPushId = pushId + 1; // assuming no concurrent requests... + sent += 1; + } else { + exchange.sendHttp3PushPromiseFrame(usePushId, uri, headers); + System.err.println("Server: Sent push promise frame: " + usePushId); + exchange.sendHttp3PushResponse(usePushId, uri, headers, headers, is); + System.err.println("Server: Sent push promise response: " + usePushId); + sent += 1; + return; + } + } + System.err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java b/test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java new file mode 100644 index 00000000000..cd699e7774d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java @@ -0,0 +1,363 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.UnsupportedProtocolVersionException; +import java.util.List; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.test.lib.net.SimpleSSLContext; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + + +/* + * @test + * @summary verifies that the SSLParameters configured with specific cipher suites + * and TLS protocol versions gets used by the HttpClient for HTTP/3 + * @library /test/jdk/java/net/httpclient/lib /test/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @run main/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=all + * H3QuicTLSConnection + */ +public class H3QuicTLSConnection { + + private static final SSLParameters DEFAULT_SSL_PARAMETERS = new SSLParameters(); + + // expect highest supported version we know about + private static String expectedTLSVersion(SSLContext ctx) throws Exception { + if (ctx == null) { + ctx = SSLContext.getDefault(); + } + SSLParameters params = ctx.getSupportedSSLParameters(); + String[] protocols = params.getProtocols(); + for (String prot : protocols) { + if (prot.equals("TLSv1.3")) + return "TLSv1.3"; + } + return "TLSv1.2"; + } + + public static void main(String[] args) throws Exception { + // create and set the default SSLContext + SSLContext context = new SimpleSSLContext().get(); + SSLContext.setDefault(context); + + Handler handler = new Handler(); + + try (HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault())) { + server.addHandler(handler, "/"); + server.start(); + + String uriString = "https://" + server.serverAuthority(); + + // run test cases + boolean success = true; + + SSLParameters parameters = null; + success &= expectFailure( + "---\nTest #1: SSL parameters is null, expect NPE", + () -> connect(uriString, parameters), + NullPointerException.class, + Optional.empty()); + + success &= expectSuccess( + "---\nTest #2: default SSL parameters, " + + "expect successful connection", + () -> connect(uriString, DEFAULT_SSL_PARAMETERS)); + success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null)); + + success &= expectFailure( + "---\nTest #3: SSL parameters with " + + "TLS_AES_128_GCM_SHA256 cipher suite, but TLSv1.2 " + + "expect UnsupportedProtocolVersionException", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_128_GCM_SHA256"}, + new String[]{"TLSv1.2"})), + UnsupportedProtocolVersionException.class, + Optional.empty()); + + // set TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 and expect it to fail since + // it's not supported with TLS v1.3 + success &= expectFailure( + "---\nTest #4: SSL parameters with " + + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipher suite, " + + "expect No appropriate protocol " + + "(protocol is disabled or cipher suites are inappropriate)", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}, + new String[]{"TLSv1.3"})), + SSLHandshakeException.class, + Optional.of("protocol is disabled or cipher suites are inappropriate")); + + // set TLS_AES_128_CCM_8_SHA256 cipher suite + // which is not supported by the (default) SunJSSE provider + // and forbidden in QUIC in general + success &= expectFailure( + "---\nTest #5: SSL parameters with " + + "TLS_AES_128_CCM_8_SHA256 cipher suite, " + + "expect IllegalArgumentException: Unsupported CipherSuite", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_128_CCM_8_SHA256"}, + new String[]{"TLSv1.3"})), + IllegalArgumentException.class, + Optional.of("Unsupported CipherSuite")); + + // set TLS_AES_128_GCM_SHA256 and TLS_AES_256_GCM_SHA384 cipher suite + var suites = List.of("TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256"); + success &= expectSuccess( + "---\nTest #6: SSL parameters with " + + "TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, " + + "and TLS_CHACHA20_POLY1305_SHA256 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + suites.toArray(new String[0]), + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), suites); + + // set TLS_AES_128_GCM_SHA256 cipher suite + success &= expectSuccess( + "---\nTest #7: SSL parameters with " + + "TLS_AES_128_GCM_SHA256 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_128_GCM_SHA256"}, + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), + "TLS_AES_128_GCM_SHA256"); + + // set TLS_AES_256_GCM_SHA384 cipher suite + success &= expectSuccess( + "---\nTest #8: SSL parameters with " + + "TLS_AES_256_GCM_SHA384 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_256_GCM_SHA384"}, + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), + "TLS_AES_256_GCM_SHA384"); + + // set TLS_CHACHA20_POLY1305_SHA256 cipher suite + success &= expectSuccess( + "---\nTest #9: SSL parameters with " + + "TLS_CHACHA20_POLY1305_SHA256 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_CHACHA20_POLY1305_SHA256"}, + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), + "TLS_CHACHA20_POLY1305_SHA256"); + + if (success) { + System.out.println("Test passed"); + } else { + throw new RuntimeException("At least one test case failed"); + } + } + } + + private interface Test { + void run() throws Exception; + } + + private static class Handler implements HttpTestHandler { + + private static final byte[] BODY = "Test response".getBytes(); + + private volatile SSLSession sslSession; + + @Override + public void handle(HttpTestExchange t) throws IOException { + System.out.println("Handler: received request to " + + t.getRequestURI()); + + try (InputStream is = t.getRequestBody()) { + byte[] body = is.readAllBytes(); + System.out.println("Handler: read " + body.length + + " bytes of body: "); + System.out.println(new String(body)); + } + + sslSession = t.getSSLSession(); + + try (OutputStream os = t.getResponseBody()) { + t.sendResponseHeaders(200, BODY.length); + os.write(BODY); + } + + } + + SSLSession getSSLSession() { + return sslSession; + } + } + + private static void connect(String uriString, SSLParameters sslParameters) + throws URISyntaxException, IOException, InterruptedException { + HttpClient.Builder builder = HttpServerAdapters.createClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(HttpClient.Version.HTTP_3); + if (sslParameters != DEFAULT_SSL_PARAMETERS) + builder.sslParameters(sslParameters); + try (final HttpClient client = builder.build()) { + HttpRequest request = HttpRequest.newBuilder(new URI(uriString)) + .POST(BodyPublishers.ofString("body")) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .version(Version.HTTP_3) + .build(); + String body = client.send(request, BodyHandlers.ofString()).body(); + System.out.println("Response: " + body); + } catch (UncheckedIOException uio) { + throw uio.getCause(); + } + } + + private static boolean checkProtocol(SSLSession session, String protocol) { + if (session == null) { + System.out.println("Check protocol: no session provided"); + return false; + } + + System.out.println("Check protocol: negotiated protocol: " + + session.getProtocol()); + System.out.println("Check protocol: expected protocol: " + + protocol); + if (!protocol.equals(session.getProtocol())) { + System.out.println("Check protocol: unexpected negotiated protocol"); + return false; + } + + return true; + } + + private static boolean checkCipherSuite(SSLSession session, String ciphersuite) { + if (session == null) { + System.out.println("Check protocol: no session provided"); + return false; + } + + System.out.println("Check protocol: negotiated ciphersuite: " + + session.getCipherSuite()); + System.out.println("Check protocol: expected ciphersuite: " + + ciphersuite); + if (!ciphersuite.equals(session.getCipherSuite())) { + System.out.println("Check protocol: unexpected negotiated ciphersuite"); + return false; + } + + return true; + } + + private static boolean checkCipherSuite(SSLSession session, List ciphersuites) { + if (session == null) { + System.out.println("Check protocol: no session provided"); + return false; + } + + System.out.println("Check protocol: negotiated ciphersuite: " + + session.getCipherSuite()); + System.out.println("Check protocol: expected ciphersuite in: " + + ciphersuites); + if (!ciphersuites.contains(session.getCipherSuite())) { + System.out.println("Check protocol: unexpected negotiated ciphersuite"); + return false; + } + + return true; + } + + private static boolean expectSuccess(String message, Test test) { + System.out.println(message); + try { + test.run(); + System.out.println("Passed"); + return true; + } catch (Exception e) { + System.out.println("Failed: unexpected exception:"); + e.printStackTrace(System.out); + return false; + } + } + + private static boolean expectFailure(String message, Test test, + Class expectedException, + Optional exceptionMsg) { + + System.out.println(message); + try { + test.run(); + System.out.println("Failed: unexpected successful connection"); + return false; + } catch (Exception e) { + System.out.println("Got an exception:"); + e.printStackTrace(System.out); + if (expectedException != null + && !expectedException.isAssignableFrom(e.getClass())) { + System.out.printf("Failed: expected %s, but got %s%n", + expectedException.getName(), + e.getClass().getName()); + return false; + } + if (exceptionMsg.isPresent()) { + final String actualMsg = e.getMessage(); + if (actualMsg == null || !actualMsg.contains(exceptionMsg.get())) { + System.out.printf("Failed: exception message was expected" + + " to contain \"%s\", but got \"%s\"%n", + exceptionMsg.get(), actualMsg); + return false; + } + } + System.out.println("Passed: expected exception"); + return true; + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3RedirectTest.java b/test/jdk/java/net/httpclient/http3/H3RedirectTest.java new file mode 100644 index 00000000000..4f6fbaeef6f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3RedirectTest.java @@ -0,0 +1,260 @@ +/* + * 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. + * + * 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 8156514 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm + * -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors + * -Djdk.internal.httpclient.debug=true + * H3RedirectTest + */ + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +public class H3RedirectTest implements HttpServerAdapters { + static int httpPort; + static SSLContext sslContext; + static HttpTestServer http3Server; + static HttpClient client; + + static String httpURIString, altURIString1, altURIString2; + static URI httpURI, altURI1, altURI2; + + static Supplier sup(String... args) { + Iterator i = Arrays.asList(args).iterator(); + // need to know when to stop calling it. + return i::next; + } + + static class Redirector extends HttpTestRedirectHandler { + private InetSocketAddress remoteAddr; + private boolean error = false; + + Redirector(Supplier supplier) { + super(supplier); + } + + @Override + protected synchronized void examineExchange(HttpTestExchange ex) { + InetSocketAddress addr = ex.getRemoteAddress(); + if (remoteAddr == null) { + remoteAddr = addr; + return; + } + // check that the client addr/port stays the same, proving + // that the connection didn't get dropped. + if (!remoteAddr.equals(addr)) { + System.err.printf("Error %s/%s\n", remoteAddr, + addr.toString()); + error = true; + } + } + + @Override + protected int redirectCode() { + return 308; // we need to use a code that preserves the body + } + + public synchronized boolean error() { + return error; + } + } + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + httpPort = http3Server.getAddress().getPort(); + String serverAuth = http3Server.serverAuthority(); + + // urls are accessed in sequence below. The first two are on + // different servers. Third on same server as second. So, the + // client should use the same http connection. + httpURIString = "https://" + serverAuth + "/foo/"; + httpURI = URI.create(httpURIString); + altURIString1 = "https://" + serverAuth + "/redir"; + altURI1 = URI.create(altURIString1); + altURIString2 = "https://" + serverAuth + "/redir_again"; + altURI2 = URI.create(altURIString2); + + Redirector r = new Redirector(sup(altURIString1, altURIString2)); + http3Server.addHandler(r, "/foo"); + http3Server.addHandler(r, "/redir"); + http3Server.addHandler(new HttpTestEchoHandler(), "/redir_again"); + + http3Server.start(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + @Test + public static void test() throws Exception { + try { + initialize(); + simpleTest(); + ReferenceTracker.INSTANCE.track(client); + client = null; + System.gc(); + var error = ReferenceTracker.INSTANCE.check(1500); + if (error != null) throw error; + } finally { + http3Server.stop(); + } + } + + static HttpClient getClient() { + if (client == null) { + client = HttpServerAdapters.createClientBuilderForH3() + .followRedirects(HttpClient.Redirect.ALWAYS) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI() { + return URI.create(httpURIString); + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkURIs(URI expected, URI found) throws Exception { + System.out.printf ("Expected: %s, Found: %s\n", expected, found); + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong URI %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void check(boolean cond, Object... msg) { + if (cond) + return; + StringBuilder sb = new StringBuilder(); + for (Object o : msg) + sb.append(o); + throw new RuntimeException(sb.toString()); + } + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + static void simpleTest() throws Exception { + URI uri = getURI(); + System.err.println("Request to " + uri); + + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .build(); + CompletableFuture> cf = client.sendAsync(req, BodyHandlers.ofString()); + HttpResponse response = cf.join(); + + checkStatus(200, response.statusCode()); + String responseBody = response.body(); + checkStrings(SIMPLE_STRING, responseBody); + checkURIs(response.uri(), altURI2); + + // check two previous responses + HttpResponse prev = response.previousResponse() + .orElseThrow(() -> new RuntimeException("no previous response")); + checkURIs(prev.uri(), altURI1); + + prev = prev.previousResponse() + .orElseThrow(() -> new RuntimeException("no previous response")); + checkURIs(prev.uri(), httpURI); + + checkPreviousRedirectResponses(req, response); + + System.err.println("DONE"); + } + + static void checkPreviousRedirectResponses(HttpRequest initialRequest, + HttpResponse finalResponse) { + // there must be at least one previous response + finalResponse.previousResponse() + .orElseThrow(() -> new RuntimeException("no previous response")); + + HttpResponse response = finalResponse; + do { + URI uri = response.uri(); + response = response.previousResponse().get(); + check(300 <= response.statusCode() && response.statusCode() <= 309, + "Expected 300 <= code <= 309, got:" + response.statusCode()); + check(response.body() == null, "Unexpected body: " + response.body()); + String locationHeader = response.headers().firstValue("Location") + .orElseThrow(() -> new RuntimeException("no previous Location")); + check(uri.toString().endsWith(locationHeader), + "URI: " + uri + ", Location: " + locationHeader); + } while (response.previousResponse().isPresent()); + + // initial + check(initialRequest.equals(response.request()), + "Expected initial request [%s] to equal last prev req [%s]", + initialRequest, response.request()); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPush.java b/test/jdk/java/net/httpclient/http3/H3ServerPush.java new file mode 100644 index 00000000000..4b271320b0d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPush.java @@ -0,0 +1,396 @@ +/* + * 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. + * + * 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 8087112 8159814 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.httpclient.test.lib.http2.PushHandler + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm/timeout=960 + * -Djdk.httpclient.HttpClient.log=errors,requests,headers + * -Djdk.internal.httpclient.debug=false + * H3ServerPush + * @summary This is a clone of http2/ServerPush but for HTTP/3 + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.BodySubscribers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.PushHandler; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.test.lib.Utils.createTempFileOfSize; +import static org.testng.Assert.assertEquals; + +public class H3ServerPush implements HttpServerAdapters { + + private static final String CLASS_NAME = H3ServerPush.class.getSimpleName(); + + static final int LOOPS = 13; + static final int FILE_SIZE = 512 * 1024 + 343; + + static Path tempFile; + + HttpTestServer server; + URI uri; + URI headURI; + + @BeforeTest + public void setup() throws Exception { + tempFile = createTempFileOfSize(CLASS_NAME, ".dat", FILE_SIZE); + var sslContext = new SimpleSSLContext().get(); + var h2Server = new Http2TestServer(true, sslContext); + h2Server.enableH3AltServiceOnSamePort(); + h2Server.addHandler(new PushHandler(tempFile, LOOPS), "/foo/"); + System.out.println("Using temp file:" + tempFile); + server = HttpTestServer.of(h2Server); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + uri = new URI("https://" + server.serverAuthority() + "/foo/a/b/c"); + + System.err.println("Server listening at " + server.serverAuthority()); + server.start(); + + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + // Test 1 - custom written push promise handler, everything as a String + @Test + public void testTypeString() throws Exception { + System.out.println("\n**** testTypeString\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> + resultMap = new ConcurrentHashMap<>(); + + PushPromiseHandler pph = (initial, pushRequest, acceptor) -> { + BodyHandler s = BodyHandlers.ofString(UTF_8); + CompletableFuture> cf = acceptor.apply(s); + resultMap.put(pushRequest, cf); + }; + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET() + .build(); + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph); + resultMap.put(request, cf); + System.out.println("waiting for response"); + var resp = cf.join(); + assertEquals(resp.version(), Version.HTTP_3); + var seen = new HashSet<>(); + resultMap.forEach((k, v) -> { + if (seen.add(k)) { + System.out.println("Got " + v.join()); + } + }); + + // waiting for all promises to reach us + System.out.println("waiting for promises"); + System.out.println("results.size: " + resultMap.size()); + for (HttpRequest r : resultMap.keySet()) { + System.out.println("Checking " + r); + HttpResponse response = resultMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + assertEquals(response.body(), tempFileAsString); + } + resultMap.forEach((k, v) -> { + if (seen.add(k)) { + System.out.println("Got " + v.join()); + } + }); + assertEquals(resultMap.size(), LOOPS + 1); + } + } + + // Test 2 - of(...) populating the given Map, everything as a String + @Test + public void testTypeStringOfMap() throws Exception { + System.out.println("\n**** testTypeStringOfMap\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> + resultMap = new ConcurrentHashMap<>(); + + PushPromiseHandler pph = + PushPromiseHandler.of(pushPromise -> BodyHandlers.ofString(UTF_8), resultMap); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph); + cf.join(); + resultMap.put(request, cf); + System.err.println("results.size: " + resultMap.size()); + for (HttpRequest r : resultMap.keySet()) { + HttpResponse response = resultMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + assertEquals(response.body(), tempFileAsString); + } + assertEquals(resultMap.size(), LOOPS + 1); + } + } + + // --- Path --- + + static final Path dir = Paths.get(".", "serverPush"); + static BodyHandler requestToPath(HttpRequest req) { + URI u = req.uri(); + Path path = Paths.get(dir.toString(), u.getPath()); + try { + Files.createDirectories(path.getParent()); + } catch (IOException ee) { + throw new UncheckedIOException(ee); + } + return BodyHandlers.ofFile(path); + } + + // Test 3 - custom written push promise handler, everything as a Path + @Test + public void testTypePath() throws Exception { + System.out.println("\n**** testTypePath\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + + PushPromiseHandler pushPromiseHandler = (initial, pushRequest, acceptor) -> { + BodyHandler pp = requestToPath(pushRequest); + CompletableFuture> cf = acceptor.apply(pp); + resultsMap.put(pushRequest, cf); + }; + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + CompletableFuture> cf = + client.sendAsync(request, requestToPath(request), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + String fileAsString = Files.readString(response.body()); + assertEquals(fileAsString, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } + + // Test 4 - of(...) populating the given Map, everything as a Path + @Test + public void testTypePathOfMap() throws Exception { + System.out.println("\n**** testTypePathOfMap\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + + PushPromiseHandler pushPromiseHandler = + PushPromiseHandler.of(H3ServerPush::requestToPath, resultsMap); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + CompletableFuture> cf = + client.sendAsync(request, requestToPath(request), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + String fileAsString = Files.readString(response.body()); + assertEquals(fileAsString, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } + + // --- Consumer --- + + static class ByteArrayConsumer implements Consumer> { + volatile List listByteArrays = new ArrayList<>(); + volatile byte[] accumulatedBytes; + + public byte[] getAccumulatedBytes() { return accumulatedBytes; } + + @Override + public void accept(Optional optionalBytes) { + assert accumulatedBytes == null; + if (optionalBytes.isEmpty()) { + int size = listByteArrays.stream().mapToInt(ba -> ba.length).sum(); + ByteBuffer bb = ByteBuffer.allocate(size); + listByteArrays.forEach(bb::put); + accumulatedBytes = bb.array(); + } else { + listByteArrays.add(optionalBytes.get()); + } + } + } + + // Test 5 - custom written handler, everything as a consumer of optional byte[] + @Test + public void testTypeByteArrayConsumer() throws Exception { + System.out.println("\n**** testTypeByteArrayConsumer\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + Map byteArrayConsumerMap + = new ConcurrentHashMap<>(); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + ByteArrayConsumer bac = new ByteArrayConsumer(); + byteArrayConsumerMap.put(request, bac); + + PushPromiseHandler pushPromiseHandler = (initial, pushRequest, acceptor) -> { + CompletableFuture> cf = acceptor.apply( + (info) -> { + ByteArrayConsumer bc = new ByteArrayConsumer(); + byteArrayConsumerMap.put(pushRequest, bc); + return BodySubscribers.ofByteArrayConsumer(bc); + }); + resultsMap.put(pushRequest, cf); + }; + + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes(); + String result = new String(ba, UTF_8); + assertEquals(result, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } + + // Test 6 - of(...) populating the given Map, everything as a consumer of optional byte[] + @Test + public void testTypeByteArrayConsumerOfMap() throws Exception { + System.out.println("\n**** testTypeByteArrayConsumerOfMap\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + Map byteArrayConsumerMap + = new ConcurrentHashMap<>(); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + ByteArrayConsumer bac = new ByteArrayConsumer(); + byteArrayConsumerMap.put(request, bac); + + PushPromiseHandler pushPromiseHandler = + PushPromiseHandler.of( + pushRequest -> { + ByteArrayConsumer bc = new ByteArrayConsumer(); + byteArrayConsumerMap.put(pushRequest, bc); + return BodyHandlers.ofByteArrayConsumer(bc); + }, + resultsMap); + + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes(); + String result = new String(ba, UTF_8); + assertEquals(result, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java b/test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java new file mode 100644 index 00000000000..32ca87e20f5 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java @@ -0,0 +1,607 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * -Djdk.httpclient.http3.maxConcurrentPushStreams=45 + * H3ServerPushCancel + * @summary This test checks that the client deals correctly with a + * CANCEL_PUSH frame sent by the server + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +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.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class H3ServerPushCancel implements HttpServerAdapters { + + // dummy hack to prevent the IDE complaining that calling + // println will throw NPE + static final PrintStream err = System.err; + static final PrintStream out = System.out; + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + static final int REQUESTS = 5; + + HttpTestServer server; + URI uri; + URI headURI; + ServerPushHandler pushHandler; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), HTTP_2); + } + + static final class TestPushPromiseHandler implements PushPromiseHandler { + record NotifiedPromise(PushId pushId, HttpRequest initiatingRequest) {} + final Map requestToPushId = new ConcurrentHashMap<>(); + final Map pushIdToRequest = new ConcurrentHashMap<>(); + final List errors = new CopyOnWriteArrayList<>(); + final List notified = new CopyOnWriteArrayList<>(); + final ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + final Supplier> bodyHandlerSupplier; + final PushPromiseHandler pph; + TestPushPromiseHandler(Supplier> bodyHandlerSupplier) { + this.bodyHandlerSupplier = bodyHandlerSupplier; + this.pph = PushPromiseHandler.of((r) -> bodyHandlerSupplier.get(), promises); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + errors.add(new AssertionError("no pushID provided for: " + pushPromiseRequest)); + } + + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushid) { + notified.add(new NotifiedPromise(pushid, initiatingRequest)); + out.println("notifyPushPromise: pushId=" + pushid); + pph.notifyAdditionalPromise(initiatingRequest, pushid); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function, CompletableFuture>> acceptor) { + out.println("applyPushPromise: " + pushPromiseRequest + ", pushId=" + pushid); + requestToPushId.putIfAbsent(pushPromiseRequest, pushid); + if (pushIdToRequest.putIfAbsent(pushid, pushPromiseRequest) != null) { + errors.add(new AssertionError("pushId already used: " + pushid)); + } + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, pushid, acceptor); + } + + } + + T join(CompletableFuture cf) { + try { + return cf.join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + + String describeBody(CompletableFuture> cf) { + return cf.thenApply(HttpResponse::body).exceptionally(Throwable::toString).join(); + } + + String describeKey(TestPushPromiseHandler pph, HttpRequest request) { + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + var pushId = pph.requestToPushId.get(request); + if (pushId instanceof Http3PushId h3id) { + return "[push=%s]".formatted(h3id.pushId()) + request; + } else return "[push=%s]".formatted(pushId) + request; + } else { + return "[main] " + request; + } + } + + @Test + public void testServerCancelPushes() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + TestPushPromiseHandler custom = new TestPushPromiseHandler<>(BodyHandlers::ofString); + var promises = custom.promises; + Set expectedPushIds = new HashSet<>(); + + for (int j=0; j < 2; j++) { + if (j == 0) out.println("\ntestCancel: First time around"); + else out.println("\ntestCancel: Second time around: should be a new connection"); + + // now make sure there's an HTTP/3 connection + client.send(HttpRequest.newBuilder(headURI).version(HTTP_3) + .setOption(H3_DISCOVERY, ALT_SVC) + .HEAD().build(), BodyHandlers.discarding()); + + int waitForPushId; + List>> responses = new ArrayList<>(); + for (int i = 0; i < REQUESTS; i++) { + waitForPushId = Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + CompletableFuture> main = client.sendAsync( + HttpRequest.newBuilder(uri.resolve("?i=%s,j=%s".formatted(i, j))) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + custom); + responses.add(main); + } + + join(CompletableFuture.allOf(responses.toArray(CompletableFuture[]::new))); + + responses.forEach(cf -> { + var main = join(cf); + var old = promises.put(main.request(), CompletableFuture.completedFuture(main)); + assertNull(old, "unexpected mapping for: " + old); + }); + + promises.forEach((key, value) -> out.println(describeKey(custom, key) + ":" + describeBody(value))); + + promises.forEach((request, value) -> { + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + var pushId = custom.requestToPushId.get(request); + assertNotNull(pushId, "no pushId for " + request); + if (pushId instanceof Http3PushId h3id) { + long id = h3id.pushId(); + long mod = id % PUSH_PROMISES.size(); + if (mod > 0 && mod < 4) { + // should have been cancelled by server, so + // we should have an IO for these + Throwable ex = value.exceptionNow(); + var msg = ex.getMessage(); + var expected = "Push promise cancelled: %s".formatted(id); + if (!(ex instanceof IOException)) { + throw new AssertionError(ex); + } else if (!msg.contains(expected)) { + throw new AssertionError("Unexpected message: " + msg, ex); + } + } else { + assertEquals(join(value).body(), PUSH_PROMISES.get(request.uri().getPath())); + } + expectedPushIds.add(pushId); + } else assertEquals(pushId.getClass(), Http3PushId.class); + } else { + HttpResponse response = join(value); + assertEquals(response.statusCode(), 200); + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + + int maxExpectedPushes = Math.min(PUSH_PROMISES.size(), maxPushes); + int minExpectedPushes = maxExpectedPushes - 3; + int countpushes = promises.size() - REQUESTS; + assert countpushes >= 0; + if (maxExpectedPushes < countpushes || minExpectedPushes > countpushes) { + throw new AssertionError("unexpected number of pushes %s should be in [%s,%s]" + .formatted(countpushes, minExpectedPushes, maxExpectedPushes)); + } + + promises.clear(); + custom.requestToPushId.clear(); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3ServerPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3ServerPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes))) { + success = true; + } + if (success) { + out.println("Got expected IOException: " + io); + } else throw new AssertionError(io); + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection, + // so we can restart from scratch + pushHandler.reset(); + } + var errors = custom.errors; + errors.forEach(t -> t.printStackTrace(System.out)); + var error = errors.stream().findFirst().orElse(null); + if (error != null) throw error; + var notified = custom.notified; + int count = 0; + Set uniqueIds = new HashSet<>(); + Set cIds = new HashSet<>(); + for (var npp : notified) { + uniqueIds.add(npp.pushId); + if (npp.pushId instanceof Http3PushId h3id) { + long id = h3id.pushId(); + cIds.add(h3id.connectionLabel()); + long mod = id % PUSH_PROMISES.size(); + // we can't count the cancelled pushes as + // how many notifs we might get for those is racy. + if (mod == 0 || mod >= 4) { + // was not cancelled + count++; + } + } + } + + if (!uniqueIds.equals(expectedPushIds)) { + int problems = 0; + int missed = 0; + for (var id : uniqueIds) { + if (!expectedPushIds.contains(id)) { + problems++; + out.printf("%s was not expected%n", id); + } + } + for (var id : expectedPushIds) { + if (!uniqueIds.contains(id)) { + if (id instanceof Http3PushId h3id) { + long mod = h3id.pushId() % PUSH_PROMISES.size(); + if (mod > 0 && mod < 4) { + // this one was cancelled, so it might not + // have been notified + missed++; + continue; + } + } + problems++; + out.printf("%s was expected but not notified%n", id); + } + } + if (problems > 0) { + throw new AssertionError("%s unexpected problems with ids have been found" + .formatted(problems)); + } + } + // excluding those that got cancelled, + // we should have received REQUEST-1 notifications + // per push promise and per connection + assertEquals(count, (PUSH_PROMISES.size()-3)*2*(REQUESTS-1), + "Unexpected notification: " + notified); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + private final ReentrantLock lock = new ReentrantLock(); + private final Map sentPromises = new ConcurrentHashMap<>(); + record PendingPromise(long pushId, CountDownLatch latch) { + PendingPromise(long pushId) { + this(pushId, new CountDownLatch(REQUESTS)); + } + } + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + // The assumption is that there will be several concurrent + // exchanges, but all on the same connection + // The first exchange that emits a PushPromise sends + // a push promise frame + open the push response stream. + // The other exchanges will simply send a push promise + // frame, with the pushId allocated by the previous exchange. + // The sentPromises map is used to store that pushId. + // This obviously only works if we have a single HTTP/3 connection. + final AtomicInteger count = new AtomicInteger(); + public void handle(HttpTestExchange exchange) throws IOException { + long count = -1; + try { + count = this.count.incrementAndGet(); + err.println("Server: handle " + exchange + + " on " + exchange.getConnectionKey()); + out.println("Server: handle " + exchange.getRequestURI() + + " on " + exchange.getConnectionKey()); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } catch (ClosedChannelException ex) { + out.printf("handling exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + out.printf("Got closed channel exception sending response after sent=%s allowed=%s%n", + sent, allowed); + } + } finally { + out.printf("handled exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + } + } + + volatile long allowed = -1; + volatile int sent = 0; + volatile int nsent = 0; + void reset() { + lock.lock(); + try { + allowed = -1; + sent = 0; + nsent = 0; + sentPromises.clear(); + } finally { + lock.unlock(); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long usePushId = exchange.getRequestHeaders() + .firstValueAsLong("X-UsePushId").orElse(-1); + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + for (Map.Entry promise : promises.entrySet()) { + // if usePushId != -1 we send a single push promise, + // without checking that it's allowed. + // Otherwise, we stop sending promises when we have consumed + // the whole window + if (usePushId == -1 && allowed > 0 && sent >= allowed) { + err.println("Server: sent all allowed promises: " + sent); + break; + } + + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + if (usePushId == -1) { + long pushId; + boolean send = false; + lock.lock(); + PendingPromise pendingPromise; + try { + pendingPromise = sentPromises.get(promise.getKey()); + if (pendingPromise == null) { + pushId = exchange.sendHttp3PushPromiseFrame(-1, uri, headers); + waitForPushId = pushId + 1; + pendingPromise = new PendingPromise(pushId); + sentPromises.put(promise.getKey(), pendingPromise); + sent += 1; + send = true; + } else { + pushId = pendingPromise.pushId; + exchange.sendHttp3PushPromiseFrame(pushId, uri, headers); + } + } finally { + lock.unlock(); + } + pendingPromise.latch.countDown(); + long mod = pushId % promises.size(); + if (send) { + if (mod == 1) { + // var stream = exchange.openPushIdStream(pushId); + // err.println("Server: Opened push stream: " + pushId); + } + if (mod > 0 && mod < 4) { + try { + pendingPromise.latch.await(); + } catch (InterruptedException e) { + throw new InterruptedIOException("" + e); + } + exchange.sendHttp3CancelPushFrame(pendingPromise.pushId); + err.println("Server: Cancelled push promise: " + pushId); + out.println("Server: Cancelled push promise: " + pushId); + } else { + exchange.sendHttp3PushResponse(pushId, uri, headers, headers, is); + out.println("Server: Sent push promise with response: " + pushId); + err.println("Server: Sent push promise with response: " + pushId); + } + } else { + err.println("Server: Sent push promise frame: " + pushId); + } + if (pushId >= waitForPushId) waitForPushId = pushId + 1; + } else { + exchange.sendHttp3PushPromiseFrame(usePushId, uri, headers); + err.println("Server: Sent push promise frame: " + usePushId); + exchange.sendHttp3PushResponse(usePushId, uri, headers, headers, is); + err.println("Server: Sent push promise response: " + usePushId); + lock.lock(); + sent += 1; + lock.unlock(); + return; + } + } + err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java b/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java new file mode 100644 index 00000000000..9b4858f50c9 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java @@ -0,0 +1,1224 @@ +/* + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import javax.net.ssl.SSLContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @summary Verifies the HTTP/3 server push handling of the HTTP client + * @library /test/jdk/java/net/httpclient/lib + * /test/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.net.SimpleSSLContext + * @run junit H3ServerPushTest + */ + +/** + * Verifies the HTTP/3 server push handling of {@link HttpClient}. + * + * @implNote + * Some tests deliberately corrupt the HTTP/3 stream state. Hence, instead of + * creating a single {@link HttpTestServer}-{@link HttpClient} pair attached + * to the class, and sharing it across tests, each test creates its own + * server/client pair. + */ +@TestMethodOrder(OrderAnnotation.class) +class H3ServerPushTest { + + private static final HttpHeaders EMPTY_HEADERS = HttpHeaders.of(Map.of(), (_, _) -> false); + + private static final SSLContext SSL_CONTEXT = createSslContext(); + + private static SSLContext createSslContext() { + try { + return new SimpleSSLContext().get(); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + @Test + @Order(1) + void testBasicRequestResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler( + exchange -> { + try (exchange) { + exchange.sendResponseHeaders(200, 0); + } + }, + uri.getPath()); + + // Send the request and verify its response + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.discarding()); + assertEquals(200, response.statusCode()); + + } + } + + @Test + @Order(2) + void testTwoConsecutiveRequestsToSameServer(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler(new PushSender(), uri.getPath()); + + // Send the 1st request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response1 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 1st response + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + String connectionLabel = response1.connectionLabel().orElseThrow(); + final long initialPushId; + { + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response, since this is the very first request + ReceivedPush.Promise[] push3Ref = {null}; // 3. Push(initialPushId+1) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref, push3Ref); + initialPushId = push1Ref[0].pushId.pushId(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + assertEquals(initialPushId + 1, push3Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push3Ref[0].pushId.connectionLabel()); + } + + // Send the 2nd request + log("requesting `%s`...", request.uri()); + HttpResponse response2 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 2nd response + assertEquals(200, response2.statusCode()); + assertEquals("response1", response2.body()); + { + ReceivedPush.AdditionalPromise[] push1Ref = {null}; // 1. Push(initialPushId) additional promise + ReceivedPush.Promise[] push2Ref = {null}; // 2. Push(initialPushId+2) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref); + assertEquals(initialPushId, push1Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId + 2, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + } + + } + } + + @Test + @Order(3) + void testTwoConsecutiveRequestsToDifferentServers(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server1 = createServer(); + HttpTestServer server2 = createServer()) { + + // Configure the server handlers + URI uri1 = createUri(server1, testInfo); + server1.addHandler(new PushSender(), uri1.getPath()); + URI uri2 = createUri(server2, testInfo); + server2.addHandler(new PushSender(), uri2.getPath()); + + // Send a request to the 1st server + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request1 = createRequest(uri1); + log("requesting `%s`...", request1.uri()); + HttpResponse response1 = client + .sendAsync(request1, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response from the 1st server + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + String connectionLabel1 = response1.connectionLabel().orElseThrow(); + { + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response, since this is the very first request + ReceivedPush.Promise[] push3Ref = {null}; // 3. Push(initialPushId+1) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref, push3Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + assertEquals(connectionLabel1, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel1, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + assertEquals(initialPushId + 1, push3Ref[0].pushId.pushId()); + assertEquals(connectionLabel1, push3Ref[0].pushId.connectionLabel()); + } + + // Send a request to the 2nd server + HttpRequest request2 = createRequest(uri2); + log("requesting `%s`...", request2.uri()); + HttpResponse response2 = client + .sendAsync(request2, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response from the 2nd server + assertEquals(200, response2.statusCode()); + assertEquals("response0", response2.body()); + String connectionLabel2 = response2.connectionLabel().orElseThrow(); + { + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response, since this is the very first request + ReceivedPush.Promise[] push3Ref = {null}; // 3. Push(initialPushId+1) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref, push3Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + assertEquals(connectionLabel2, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel2, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + assertEquals(initialPushId + 1, push3Ref[0].pushId.pushId()); + assertEquals(connectionLabel2, push3Ref[0].pushId.connectionLabel()); + } + + // Verify that connection labels differ + assertNotEquals(connectionLabel1, connectionLabel2); + + } + } + + /** + * A server handler responding to all requests as follows: + *

      + *
    1. push(initialPushId) promise
    2. + *
    3. push(initialPushId) response: "pushResponse" + responseIndex,
      + * iff responseIndex == 0
    4. + *
    5. push(pushId++) promise (an orphan push promise)
    6. + *
    7. response: "response" + responseIndex
    8. + *
    + */ + private static final class PushSender implements HttpTestHandler { + + private int responseIndex = 0; + + private long initialPushId = -1; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Start with the push promise + assertTrue(exchange.serverPushAllowed()); + log(">>> sending push promise (responseIndex=%d, pushId=%d)", responseIndex, initialPushId); + long newInitialPushId = exchange.sendHttp3PushPromiseFrame( + initialPushId, + exchange.getRequestURI(), + EMPTY_HEADERS); + if (initialPushId != newInitialPushId) { + log(">>> updated initial pushId=%d (responseIndex=%d)", newInitialPushId, responseIndex); + initialPushId = newInitialPushId; + } + + // Send the push response iff it is the very first request + if (responseIndex == 0) { + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, initialPushId); + byte[] pushResponseBody = "pushResponse%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + initialPushId, + exchange.getRequestURI(), + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + + // Send the orphan push promise + log(">>> sending an orphan push promise (responseIndex=%d)", responseIndex); + long orphanPushId = exchange.sendHttp3PushPromiseFrame(-1, exchange.getRequestURI(), EMPTY_HEADERS); + log(">>> sent the orphan push promise (responseIndex=%d, pushId=%d)", responseIndex, orphanPushId); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + } + + @Test + @Order(4) + void testTwoPushPromisesWithSameIdInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + HttpTestHandler pushSender = new HttpTestHandler() { + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the 1st push promise and receive the push ID + log(">>> sending push promise (responseIndex=%d)", responseIndex); + long pushId = exchange.sendHttp3PushPromiseFrame(-1, uri, EMPTY_HEADERS); + + // Send the 2nd push promise using the same ID + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId); + exchange.sendHttp3PushPromiseFrame(pushId, uri, EMPTY_HEADERS); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.AdditionalPromise[] push2Ref = {null}; // 2. Push(initialPushId) promise, again + pushReceiver.consume(push1Ref, push2Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + String connectionLabel = response.connectionLabel().orElseThrow(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + + } + } + + @Test + @Order(5) + void testTwoPushPromisesWithSameIdButDifferentHeadersInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + CountDownLatch responseBodyWriteLatch = new CountDownLatch(1); + var pushSender = new HttpTestHandler() { + + private long pushId = -1; + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the 1st push promise and receive the push ID + log(">>> sending push promise (responseIndex=%d)", responseIndex); + pushId = exchange.sendHttp3PushPromiseFrame(pushId, uri, EMPTY_HEADERS); + + // Send the 2nd push promise using the same ID, but different headers + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId); + HttpHeaders nonEmptyHeaders = HttpHeaders.of(Map.of("Foo", List.of("Bar")), (_, _) -> true); + assertNotEquals(EMPTY_HEADERS, nonEmptyHeaders); + exchange.sendHttp3PushPromiseFrame(pushId, uri, nonEmptyHeaders); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + // Block to ensure the bad push stream is received before the response + awaitLatch(responseBodyWriteLatch); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + ExecutionException exception = assertThrows(ExecutionException.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + responseBodyWriteLatch.countDown(); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertInstanceOf(IOException.class, cause); + String actualMessage = cause.getMessage(); + String expectedMessage = "push headers do not match with previous promise for %d".formatted(pushSender.pushId); + assertEquals(expectedMessage, actualMessage); + + } + } + + @Test + @Order(6) + void testTwoPushPromisesWithSameIdInTwoResponsesOverOneConnection(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + HttpTestHandler pushSender = new HttpTestHandler() { + + private long pushId = -1; + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the push promise, and receive the push ID, if necessary + log(">>> sending push promise (responseIndex=%d, pushId=%d)", responseIndex, pushId); + pushId = exchange.sendHttp3PushPromiseFrame(pushId, uri, EMPTY_HEADERS); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the 1st request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response1 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 1st response + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + String connectionLabel = response1.connectionLabel().orElseThrow(); + final long initialPushId; + { + ReceivedPush.Promise[] pushRef = {null}; + pushReceiver.consume(pushRef); + initialPushId = pushRef[0].pushId.pushId(); + assertEquals(connectionLabel, pushRef[0].pushId.connectionLabel()); + } + + // Send the 2nd request + log("requesting `%s`...", request.uri()); + HttpResponse response2 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 2nd request + assertEquals(200, response2.statusCode()); + assertEquals("response1", response2.body()); + assertEquals(connectionLabel, response2.connectionLabel().orElseThrow()); + { + ReceivedPush.AdditionalPromise[] pushRef = {null}; + pushReceiver.consume(pushRef); + assertEquals(initialPushId, pushRef[0].pushId.pushId()); + assertEquals(connectionLabel, pushRef[0].pushId.connectionLabel()); + } + + } + } + + @Test + @Order(7) + void testTwoPushResponsesWithSameIdInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + long[] pushId = {-1}; + CountDownLatch responseBodyWriteLatch = new CountDownLatch(1); + HttpTestHandler pushSender = new HttpTestHandler() { + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the 1st push promise and receive the push ID + log(">>> sending push promise (responseIndex=%d)", responseIndex); + pushId[0] = exchange.sendHttp3PushPromiseFrame(-1, uri, EMPTY_HEADERS); + + // Send two push responses + for (int trialIndex = 0; trialIndex < 2; trialIndex++) { + log( + ">>> sending push response (responseIndex=%d, pushId=%d, trialIndex=%d)", + responseIndex, pushId[0], trialIndex); + byte[] pushResponseBody = "pushResponse%d-%d" + .formatted(responseIndex, trialIndex) + .getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushId[0], + uri, + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + // Block to ensure the bad push stream is received before the response + awaitLatch(responseBodyWriteLatch); + exchange.getResponseBody().write(responseBody); + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + ExecutionException exception = assertThrows(ExecutionException.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + responseBodyWriteLatch.countDown(); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertInstanceOf(IOException.class, cause); + String actualMessage = cause.getMessage(); + String expectedMessage = "HTTP/3 pushId %d already used on this connection".formatted(pushId[0]); + assertEquals(expectedMessage, actualMessage); + } + } + + private static void awaitLatch(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // Restore the interrupt + throw new RuntimeException(ie); + } + } + + @Test + @Order(8) + void testTwoPushResponsesWithSameIdInTwoResponsesOverOneConnection(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + long[] pushId = {-1}; + HttpTestHandler pushSender = new HttpTestHandler() { + + private int responseIndex = 0; + + private final Semaphore responseBodyWriteSem = new Semaphore(1); + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the push promise and receive the push ID, iff this is the very first response + if (responseIndex == 0) { + log(">>> sending push promise (responseIndex=%d)", responseIndex); + pushId[0] = exchange.sendHttp3PushPromiseFrame(-1, uri, EMPTY_HEADERS); + } + + // Send the push response + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId[0]); + byte[] pushResponseBody = "pushResponse%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushId[0], + uri, + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + try { + // The second request will block here, ensuring the + // bad push stream is received before the response. + responseBodyWriteSem.acquire(); + } catch (InterruptedException x) { + Thread.currentThread().interrupt(); + } + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the 1st request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response1 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 1st response + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response + pushReceiver.consume(push1Ref, push2Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + String connectionLabel = response1.connectionLabel().orElseThrow(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + + // Send the 2nd request and verify the failure + log("requesting `%s`...", request.uri()); + ExecutionException exception = assertThrows(ExecutionException.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + pushReceiver) + .get()); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertInstanceOf(IOException.class, cause); + String actualMessage = cause.getMessage(); + String expectedMessage = "HTTP/3 pushId %d already used on this connection".formatted(pushId[0]); + assertEquals(expectedMessage, actualMessage); + + } + } + + @Test + @Order(9) + void testPushPromiseBeforeHeader(TestInfo testInfo) throws Exception { + testPositionalPushPromise(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange); + sendHeaders(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(10) + void testPushPromiseAfterHeaderAndBeforeBody(TestInfo testInfo) throws Exception { + testPositionalPushPromise(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendPushPromise(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(11) + void testPushPromiseAfterBody(TestInfo testInfo) throws Exception { + testPositionalPushPromise(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendBody(exchange); + sendPushPromise(exchange); + } + }); + } + + private static void testPositionalPushPromise(TestInfo testInfo, PositionalPushSender pushSender) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush.Promise[] pushRef = {null}; + pushReceiver.consume(pushRef); + assertEquals(pushSender.pushId, pushRef[0].pushId.pushId()); + + } + } + + @Test + @Order(12) + void testPushPromiseAndResponseBeforeHeader(TestInfo testInfo) throws Exception { + testPositionalPushPromiseAndResponse(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange); + sendPushResponse(exchange); + sendHeaders(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(13) + void testPushPromiseAndResponseAfterHeaderAndBeforeBody(TestInfo testInfo) throws Exception { + testPositionalPushPromiseAndResponse(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendPushPromise(exchange); + sendPushResponse(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(14) + void testPushPromiseAndResponseAfterBody(TestInfo testInfo) throws Exception { + testPositionalPushPromiseAndResponse(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendBody(exchange); + sendPushPromise(exchange); + sendPushResponse(exchange); + } + }); + } + + private static void testPositionalPushPromiseAndResponse(TestInfo testInfo, PositionalPushSender pushSender) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush.Promise[] push1Ref = {null}; + ReceivedPush.Response[] push2Ref = {null}; + pushReceiver.consume(push1Ref, push2Ref); + assertEquals(pushSender.pushId, push1Ref[0].pushId.pushId()); + String connectionLabel = response.connectionLabel().orElseThrow(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(pushSender.pushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + + } + } + + /** + * A server providing helper methods to send header, body, push promise & response. + * Subclasses can use these methods to inject custom server push behaviour at certain positions of the response assembly. + */ + private static abstract class PositionalPushSender implements HttpTestHandler { + + private long pushId = -1; + + private int responseIndex = 0; + + @Override + public final synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + handle0(exchange); + } finally { + responseIndex++; + } + } + + abstract void handle0(HttpTestExchange exchange) throws IOException; + + void sendHeaders(HttpTestExchange exchange) throws IOException { + log(">>> sending headers (responseIndex=%d)", responseIndex); + exchange.sendResponseHeaders( + 200, + // Use `-1` to avoid generating a single DataFrame. + // Otherwise, server closes the stream after writing + // the response body, and this makes it impossible to test + // server pushes delivered after the response body. + -1); + } + + void sendBody(HttpTestExchange exchange) throws IOException { + log(">>> sending body (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.getResponseBody().write(responseBody); + } + + void sendPushResponse(HttpTestExchange exchange) throws IOException { + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId); + byte[] pushResponseBody = "pushResponse%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushId, + exchange.getRequestURI(), + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + + void sendPushPromise(HttpTestExchange exchange) throws IOException { + log(">>> sending push promise (responseIndex=%d, pushId=%d)", responseIndex, pushId); + pushId = exchange.sendHttp3PushPromiseFrame(pushId, exchange.getRequestURI(), EMPTY_HEADERS); + } + + } + + /** + * The maximum number of distinct push promise IDs allowed in a single response. + */ + private static final int MAX_ALLOWED_PUSH_ID_COUNT_PER_RESPONSE = 100; + + /** + * A value slightly more than {@link #MAX_ALLOWED_PUSH_ID_COUNT_PER_RESPONSE} to intentionally violate limits. + */ + private static final int EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE = + Math.addExact(10, MAX_ALLOWED_PUSH_ID_COUNT_PER_RESPONSE); + + @Test + @Order(15) + void testExcessivePushPromisesWithSameIdInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + int pushCount = EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE; + HttpTestHandler pushSender = new ManyPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange, pushCount, () -> pushId); + } + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush[][] pushRefs = new ReceivedPush[pushCount][1]; + for (int i = 0; i < pushCount; i++) { + pushRefs[i] = i == 0 ? new ReceivedPush.Promise[1] : new ReceivedPush.AdditionalPromise[1]; + } + pushReceiver.consume(pushRefs); + long initialPushId = ((ReceivedPush.Promise[]) pushRefs[0])[0].pushId.pushId(); + for (int i = 1; i < pushCount; i++) { + assertEquals( + initialPushId, ((ReceivedPush.AdditionalPromise[]) pushRefs[i])[0].pushId.pushId(), + "push ID mismatch for received server push at index %d".formatted(i)); + } + + } + } + + @Test + @Order(16) + void testExcessivePushPromisesWithDistinctIdsInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + HttpTestHandler pushSender = new ManyPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange, EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE, () -> -1L); + } + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + Exception exception = assertThrows(Exception.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + String exceptionMessage = exception.getMessage(); + assertTrue( + exceptionMessage.contains("Max pushId exceeded"), + "Unexpected exception message: `%s`".formatted(exceptionMessage)); + + } + } + + @Test + @Order(17) + void testExcessivePushResponsesWithDistinctIdsInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + int pushCount = EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE; + HttpTestHandler pushSender = new ManyPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + long[] returnedPushIds = sendPushPromise(exchange, pushCount, () -> -1L); + Queue returnedPushIdQueue = Arrays + .stream(returnedPushIds) + .boxed() + .collect(Collectors.toCollection(LinkedList::new)); + sendPushResponse(exchange, pushCount, returnedPushIdQueue::poll); + } + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + Exception exception = assertThrows(Exception.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + String exceptionMessage = exception.getMessage(); + assertTrue( + exceptionMessage.contains("Max pushId exceeded"), + "Unexpected exception message: `%s`".formatted(exceptionMessage)); + + } + } + + /** + * A server providing helper methods subclasses can extend to send multiple push promises & responses. + */ + private static abstract class ManyPushSender implements HttpTestHandler { + + long pushId = -1; + + private int responseIndex = 0; + + @Override + public final synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + handle0(exchange); + sendHeaders(exchange); + sendBody(exchange); + } finally { + responseIndex++; + } + } + + abstract void handle0(HttpTestExchange exchange) throws IOException; + + private void sendHeaders(HttpTestExchange exchange) throws IOException { + log(">>> sending headers (responseIndex=%d)", responseIndex); + byte[] responseBody = responseBody(); + exchange.sendResponseHeaders(200, responseBody.length); + } + + private void sendBody(HttpTestExchange exchange) throws IOException { + log(">>> sending body (responseIndex=%d)", responseIndex); + byte[] responseBody = responseBody(); + exchange.getResponseBody().write(responseBody); + } + + private byte[] responseBody() { + return "response%d".formatted(responseIndex).getBytes(US_ASCII); + } + + long[] sendPushPromise(HttpTestExchange exchange, int count, Supplier pushIdProvider) throws IOException { + long[] returnedPushIds = new long[count]; + for (int i = 0; i < count; i++) { + long pushPromiseId = pushIdProvider.get(); + log( + ">>> sending push promise (responseIndex=%d, pushId=%d, i=%d/%d)", + responseIndex, pushPromiseId, i, count); + pushId = returnedPushIds[i] = + exchange.sendHttp3PushPromiseFrame(pushPromiseId, exchange.getRequestURI(), EMPTY_HEADERS); + } + return returnedPushIds; + } + + void sendPushResponse(HttpTestExchange exchange, int count, Supplier pushIdProvider) throws IOException { + for (int i = 0; i < count; i++) { + long pushResponseId = pushIdProvider.get(); + log( + ">>> sending push response (responseIndex=%d, pushId=%d, i=%d/%d)", + responseIndex, pushResponseId, i, count); + byte[] pushResponseBody = "pushResponse%d-%d".formatted(responseIndex, i).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushResponseId, + exchange.getRequestURI(), + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + } + + } + + private static URI createUri(HttpTestServer server, TestInfo testInfo) { + String uri = "https://%s/%s/%s".formatted( + server.serverAuthority(), + testInfo.getTestClass().map(Class::getSimpleName).orElse("UnknownClass"), + testInfo.getTestMethod().map(Method::getName).orElse("UnknownMethod")); + return URI.create(uri); + } + + private static HttpRequest createRequest(URI uri) { + return HttpRequest.newBuilder(uri).HEAD().setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + } + + private static final class PushReceiver implements HttpResponse.PushPromiseHandler { + + private final BlockingQueue buffer = new LinkedBlockingQueue<>(); + + @Override + public void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + fail("`applyPushPromise(...,PushId,...)` should have been called instead"); + } + + @Override + public void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushId, + Function, CompletableFuture>> acceptor) { + Http3PushId http3PushId = (Http3PushId) pushId; + buffer(new ReceivedPush.Promise(http3PushId)); + acceptor.apply(HttpResponse.BodyHandlers.ofString(US_ASCII)).thenAccept(response -> { + assertEquals(200, response.statusCode()); + String responseBody = response.body(); + buffer(new ReceivedPush.Response(http3PushId, responseBody)); + }); + } + + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushId) { + Http3PushId http3PushId = (Http3PushId) pushId; + buffer(new ReceivedPush.AdditionalPromise(http3PushId)); + } + + private void buffer(ReceivedPush push) { + log("<<< received push: `%s`", push); + try { + buffer.put(push); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // Restore the interrupt + throw new RuntimeException(ie); + } + } + + @SuppressWarnings("rawtypes") + private void consume(ReceivedPush[]... pushRefs) { + int n = pushRefs.length; + Class[] pushTypes = Arrays + .stream(pushRefs) + .map(pushRef -> pushRef.getClass().componentType()) + .toArray(Class[]::new); + boolean[] foundIndices = new boolean[n]; + for (int i = 0; i < n; i++) { + ReceivedPush push; + try { + push = buffer.take(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // Restore the interrupt + throw new RuntimeException(ie); + } + boolean found = false; + for (int j = 0; j < n; j++) { + if (!foundIndices[j] && pushTypes[j].isInstance(push)) { + pushRefs[j][0] = push; + foundIndices[j] = true; + found = true; + break; + } + } + if (!found) { + log("pushRefs: %s", List.of(pushRefs)); + log("foundIndices: %s", List.of(foundIndices)); + log("n: %d", n); + log("i: %d", i); + log("push: %s", push); + fail("received push does not match with the expected types"); + } + } + } + + } + + private sealed interface ReceivedPush { + + record Promise(Http3PushId pushId) implements ReceivedPush {} + + record Response(Http3PushId pushId, String responseBody) implements ReceivedPush {} + + record AdditionalPromise(Http3PushId pushId) implements ReceivedPush {} + + } + + private static HttpTestServer createServer() throws IOException { + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, SSL_CONTEXT); + server.start(); + return server; + } + + private static HttpClient createClient() { + return HttpServerAdapters + .createClientBuilderFor(HTTP_3) + .proxy(NO_PROXY) + .version(HTTP_3) + .sslContext(SSL_CONTEXT) + .build(); + } + + private static void log(String format, Object... args) { + String text = format.formatted(args); + System.err.printf( + "%s [%25s] %s%n", + LocalTime.now(), + Thread.currentThread().getName(), + text); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java b/test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java new file mode 100644 index 00000000000..af22651f0a0 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java @@ -0,0 +1,292 @@ +/* + * 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. + * + * 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 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses + * H3ServerPushWithDiffTypes + * @summary This is a clone of http2/ServerPushWithDiffTypes but for HTTP/3 + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.BodySubscribers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Flow; +import java.util.function.BiPredicate; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; + +public class H3ServerPushWithDiffTypes implements HttpServerAdapters { + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eighth push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + + private void sendHeadRequest(HttpClient client, URI headURI) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + @Test + public void test() throws Exception { + var sslContext = new SimpleSSLContext().get(); + try (HttpTestServer server = HttpTestServer.create(ANY, sslContext)) { + HttpTestHandler pushHandler = + new ServerPushHandler("the main response body", + PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + + server.start(); + System.err.println("Server listening on port " + server.serverAuthority()); + + // use multi-level path + URI uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + URI headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(sslContext).version(Version.HTTP_3).build()) { + + sendHeadRequest(client, headURI); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + + ConcurrentMap>>> + results = new ConcurrentHashMap<>(); + PushPromiseHandler> bh = PushPromiseHandler.of( + BodyAndTypeHandler::new, results); + + CompletableFuture>> cf = + client.sendAsync(request, new BodyAndTypeHandler(request), bh); + results.put(request, cf); + cf.join(); + + assertEquals(results.size(), PUSH_PROMISES.size() + 1); + + for (HttpRequest r : results.keySet()) { + URI u = r.uri(); + var resp = results.get(r).get(); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.version(), Version.HTTP_3); + BodyAndType body = resp.body(); + String result; + // convert all body types to String for easier comparison + if (body.type() == String.class) { + result = (String) body.body(); + } else if (body.type() == byte[].class) { + byte[] bytes = (byte[]) body.body(); + result = new String(bytes, UTF_8); + } else if (Path.class.isAssignableFrom(body.type())) { + Path path = (Path) body.body(); + result = Files.readString(path); + } else { + throw new AssertionError("Unknown:" + body.type()); + } + + System.err.printf("%s -> %s\n", u.toString(), result); + String expected = PUSH_PROMISES.get(r.uri().getPath()); + if (expected == null) + expected = "the main response body"; + assertEquals(result, expected); + } + } + } + } + + interface BodyAndType { + Class type(); + T body(); + } + + static final Path WORK_DIR = Paths.get("."); + + static class BodyAndTypeHandler implements BodyHandler> { + int count; + final HttpRequest request; + + BodyAndTypeHandler(HttpRequest request) { + this.request = request; + } + + @Override + @SuppressWarnings("rawtypes,unchecked") + public BodySubscriber> apply(HttpResponse.ResponseInfo info) { + int whichType = count++ % 3; // real world may base this on the request metadata + switch (whichType) { + case 0: // String + return new BodyAndTypeSubscriber(BodySubscribers.ofString(UTF_8)); + case 1: // byte[] + return new BodyAndTypeSubscriber(BodySubscribers.ofByteArray()); + case 2: // Path + URI u = request.uri(); + Path path = Paths.get(WORK_DIR.toString(), u.getPath()); + try { + Files.createDirectories(path.getParent()); + } catch (IOException ee) { + throw new UncheckedIOException(ee); + } + return new BodyAndTypeSubscriber(BodySubscribers.ofFile(path)); + default: + throw new AssertionError("Unexpected " + whichType); + } + } + } + + static class BodyAndTypeSubscriber + implements BodySubscriber> + { + private record BodyAndTypeImpl(Class type, T body) implements BodyAndType { } + + private final BodySubscriber bodySubscriber; + private final CompletableFuture> cf; + + @SuppressWarnings("unchecked") + BodyAndTypeSubscriber(BodySubscriber bodySubscriber) { + this.bodySubscriber = bodySubscriber; + cf = new CompletableFuture<>(); + bodySubscriber.getBody().whenComplete( + (r,t) -> cf.complete(new BodyAndTypeImpl<>((Class) r.getClass(), r))); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + bodySubscriber.onSubscribe(subscription); + } + + @Override + public void onNext(List item) { + bodySubscriber.onNext(item); + } + + @Override + public void onError(Throwable throwable) { + bodySubscriber.onError(throwable); + cf.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + bodySubscriber.onComplete(); + } + + @Override + public CompletionStage> getBody() { + return cf; + } + } + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + public void handle(HttpTestExchange exchange) throws IOException { + System.err.println("Server: handle " + exchange); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } + + static final BiPredicate ACCEPT_ALL = (x, y) -> true; + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + for (Map.Entry promise : promises.entrySet()) { + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + Map> map = Map.of("X-Promise", List.of(promise.getKey())); + HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); + // TODO: add some check on headers, maybe + exchange.serverPush(uri, headers, is); + } + System.err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3SimpleGet.java b/test/jdk/java/net/httpclient/http3/H3SimpleGet.java new file mode 100644 index 00000000000..bccd77e7e1d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3SimpleGet.java @@ -0,0 +1,315 @@ +/* + * 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 + * 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 id=with-continuations + * @bug 8087112 + * @requires os.family != "windows" | ( os.name != "Windows 10" & os.name != "Windows Server 2016" + * & os.name != "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.retryOnStreamlimit=20 + * -Djdk.httpclient.redirects.retrylimit=21 + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.retryOnStreamlimit=5 + * -Djdk.httpclient.redirects.retrylimit=6 + * -Djdk.httpclient.quic.defaultMTU=16336 + * H3SimpleGet + */ + +/* + * @test id=without-continuation + * @bug 8087112 + * @requires os.family == "windows" & ( os.name == "Windows 10" | os.name == "Windows Server 2016" + * | os.name == "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * -Djdk.httpclient.retryOnStreamlimit=20 + * -Djdk.httpclient.redirects.retrylimit=21 + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.retryOnStreamlimit=5 + * -Djdk.httpclient.redirects.retrylimit=6 + * -Djdk.httpclient.quic.defaultMTU=16336 + * H3SimpleGet + */ + +/* + * @test id=useNioSelector + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.internal.httpclient.quic.useNioSelector=true + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.internal.httpclient.quic.useNioSelector=true + * -Djdk.httpclient.retryOnStreamlimit=20 + * -Djdk.httpclient.redirects.retrylimit=21 + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.internal.httpclient.quic.useNioSelector=true + * -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.retryOnStreamlimit=5 + * -Djdk.httpclient.redirects.retrylimit=6 + * -Djdk.httpclient.quic.defaultMTU=16336 + * H3SimpleGet + */ + +// Interesting additional settings for debugging and manual testing: +// ----------------------------------------------------------------- +// -Djdk.httpclient.HttpClient.log=requests,errors,quic:retransmit:control,http3 +// -Djdk.httpclient.HttpClient.log=errors,requests,quic:all +// -Djdk.httpclient.quic.defaultMTU=64000 +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.httpclient.quic.defaultMTU=4096 +// -Djdk.httpclient.http3.maxStreamLimitTimeout=1375 +// -Xmx16g +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.internal.httpclient.debug=err +// -XX:+HeapDumpOnOutOfMemoryError +// -Xmx768m -XX:MaxRAMPercentage=12.5 +// -Djdk.httpclient.HttpClient.log=errors,requests,http3 +// -Djdk.httpclient.HttpClient.log=errors,http3,quic:retransmit:control + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +public class H3SimpleGet implements HttpServerAdapters { + static HttpTestServer httpsServer; + static HttpClient client = null; + static SSLContext sslContext; + static String httpsURIString; + static ExecutorService serverExec = + Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("server-vt-worker-", 1).factory()); + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + httpsServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, serverExec); + httpsServer.addHandler(new TestHandler(), "/"); + httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + + httpsServer.start(); + warmup(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + private static void warmup() throws Exception { + SimpleSSLContext sslct = new SimpleSSLContext(); + var sslContext = sslct.get(); + + // warmup server + try (var client2 = createClient(sslContext, Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("client-2-vt-worker", 1).factory()))) { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client2.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } + + // warmup client + var httpsServer2 = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, + Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("server-2-vt-worker", 1).factory())); + httpsServer2.addHandler(new TestHandler(), "/"); + var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/"; + httpsServer2.start(); + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } finally { + httpsServer2.stop(); + } + } + + public static void main(String[] args) throws Exception { + test(); + } + + static volatile boolean waitBeforeTest = false; + + @Test + public static void test() throws Exception { + try { + if (waitBeforeTest) { + Thread.sleep(20000); + } + long prestart = System.nanoTime(); + initialize(); + long done = System.nanoTime(); + System.out.println("Stat: Initialization and warmup took " + + TimeUnit.NANOSECONDS.toMillis(done-prestart)+" millis"); + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET().build(); + long start = System.nanoTime(); + var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("Stat: First request took: " + elapsed + + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = property("simpleget.requests", 50); + List>> list = new ArrayList<>(max); + Set connections = new ConcurrentSkipListSet<>(); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b-> {})) + .whenComplete((r, t) -> Optional.ofNullable(r) + .flatMap(HttpResponse::connectionLabel) + .ifPresent(connections::add)); + list.add(cf); + //cf.get(); // uncomment to test with serial instead of concurrent requests + } + try { + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + } finally { + long elapsed2 = System.nanoTime() - start2; + long completed = list.stream().filter(CompletableFuture::isDone) + .filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count(); + connections.forEach(System.out::println); + if (completed > 0) { + System.out.println("Stat: Next " + completed + " requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): " + + elapsed2 / completed + " nanos per request (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + " ms) on " + + connections.size() + " connections"); + } + } + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext, ExecutorService clientExec) { + var builder = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(Builder.NO_PROXY); + if (clientExec != null) { + builder = builder.executor(clientExec); + } + return builder.build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext, null); + } + return client; + } + + static int property(String name, int defaultValue) { + return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); + } + + // 32 * 32 * 1024 * 10 chars = 10Mb responses + // 50 requests => 500Mb + // 100 requests => 1Gb + // 1000 requests => 10Gb + private final static int REPEAT = property("simpleget.repeat", 32); + private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32)); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + try (var in = t.getRequestBody()) { + byte[] input = in.readAllBytes(); + t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT); + try (var out = t.getResponseBody()) { + if (t.getRequestMethod().equals("HEAD")) return; + for (int i=0; i {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("First GET request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = 50; + List>> list = new ArrayList<>(max); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + list.add(client.sendAsync(postRequest, BodyHandlers.ofByteArrayConsumer(b -> { + }))); + } + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + long elapsed2 = System.nanoTime() - start2; + System.out.println("Next " + max + " POST requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + max + " requests): " + + elapsed2 / max + " nanos per request (" + TimeUnit.NANOSECONDS.toMillis(elapsed2) / max + " ms)"); + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext, ExecutorService clientExec) { + var builder = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(Builder.NO_PROXY); + if (clientExec != null) { + builder = builder.executor(clientExec); + } + return builder.build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext, null); + } + return client; + } + + private final static int REPEAT = 32; + private final static String RESPONSE = "abcdefghij".repeat(1024*32); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + // consume all input bytes, + try (var in = t.getRequestBody()) { + in.skip(Integer.MAX_VALUE); + t.sendResponseHeaders(200, 0); + t.getResponseBody().close(); + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3SimpleTest.java b/test/jdk/java/net/httpclient/http3/H3SimpleTest.java new file mode 100644 index 00000000000..73c766f1eab --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3SimpleTest.java @@ -0,0 +1,129 @@ +/* + * 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. + * + * 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. + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Basic test to verify that simple GET/POST/HEAD + * requests work as expected with HTTP/3, using IPv4 + * or IPv6 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * H3SimpleTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djava.net.preferIPv6Addresses=true + * H3SimpleTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djava.net.preferIPv4Stack=true + * H3SimpleTest + */ +// -Djava.security.debug=all +public class H3SimpleTest implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURI; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + // create an H3 only server + h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURI = "https://" + h3Server.serverAuthority() + "/hello"; + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + /** + * Issues various HTTP3 requests and verifies the responses are received + */ + @Test + public void testBasicRequests() throws Exception { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .version(HTTP_3) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURI); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + + // GET + final HttpRequest req1 = reqBuilder.copy().GET().build(); + System.out.println("Issuing request: " + req1); + final HttpResponse resp1 = client.send(req1, BodyHandlers.discarding()); + Assert.assertEquals(resp1.statusCode(), 200, "unexpected response code for GET request"); + + // POST + final HttpRequest req2 = reqBuilder.copy().POST(BodyPublishers.ofString("foo")).build(); + System.out.println("Issuing request: " + req2); + final HttpResponse resp2 = client.send(req2, BodyHandlers.discarding()); + Assert.assertEquals(resp2.statusCode(), 200, "unexpected response code for POST request"); + + // HEAD + final HttpRequest req3 = reqBuilder.copy().HEAD().build(); + System.out.println("Issuing request: " + req3); + final HttpResponse resp3 = client.send(req3, BodyHandlers.discarding()); + Assert.assertEquals(resp3.statusCode(), 200, "unexpected response code for HEAD request"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3StopSendingTest.java b/test/jdk/java/net/httpclient/http3/H3StopSendingTest.java new file mode 100644 index 00000000000..1855cdba6ff --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3StopSendingTest.java @@ -0,0 +1,259 @@ +/* + * 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. + * + * 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 + * @summary Verifies that the client reacts correctly to the receipt of a STOP_SENDING frame. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm/timeout=40 -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=trace,errors,headers + * H3StopSendingTest + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.internal.net.http.http3.Http3Error; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.concurrent.ExecutionException; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +public class H3StopSendingTest { + + HttpTestServer h3TestServer; + HttpRequest postRequestNoError, postRequestError; + HttpRequest postRequestNoErrorWithData, postRequestErrorWithData; + URI h3TestServerUriNoError, h3TestServerUriError; + SSLContext sslContext; + + static final String TEST_ROOT_PATH = "/h3_stop_sending_test"; + static final String NO_ERROR_PATH = TEST_ROOT_PATH + "/no_error_path"; + static final String ERROR_PATH = TEST_ROOT_PATH + "/error_path"; + static final String WITH_RESPONSE_BODY_QUERY = "?withbody"; + + static final PrintStream err = System.err; + + + @Test + public void test() throws ExecutionException, InterruptedException { + HttpClient.Builder clientBuilder = HttpServerAdapters.createClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext); + try (HttpClient client = clientBuilder.build()) { + HttpResponse resp = client.sendAsync(postRequestNoError, HttpResponse.BodyHandlers.ofString()).get(); + err.println(resp.headers()); + err.println(resp.body()); + err.println(resp.statusCode()); + assertEquals(resp.statusCode(), 200); + resp = client.sendAsync(postRequestNoErrorWithData, HttpResponse.BodyHandlers.ofString()).get(); + err.println(resp.headers()); + err.println(resp.body()); + err.println(resp.statusCode()); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.body(), RESPONSE_MESSAGE.repeat(MESSAGE_REPEAT)); + } + } + + @Test + public void testError() { + Throwable caught = null; + HttpClient.Builder clientBuilder = HttpServerAdapters.createClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext); + try (HttpClient client = clientBuilder.build()) { + try { + client.sendAsync(postRequestError, HttpResponse.BodyHandlers.ofString()).get(); + } catch (Throwable throwable) { + caught = throwable; + } + assertRequestCancelled(caught); + try { + client.sendAsync(postRequestErrorWithData, HttpResponse.BodyHandlers.ofString()).get(); + } catch (Throwable throwable) { + caught = throwable; + } + assertRequestCancelled(caught); + } + } + + private static void assertRequestCancelled(Throwable caught) { + assertNotNull(caught); + if (!caught.getMessage().contains(Long.toString(Http3Error.H3_REQUEST_CANCELLED.code())) + && !caught.getMessage().contains(Http3Error.H3_REQUEST_CANCELLED.name())) { + throw new AssertionError(caught.getMessage() + " does not contain " + + Http3Error.H3_REQUEST_CANCELLED.code() + " or " + + Http3Error.H3_REQUEST_CANCELLED.name(), caught); + } else { + System.out.println("Got expected exception: " + caught); + } + } + + @BeforeTest + public void setup() throws IOException { + + sslContext = new SimpleSSLContext().get(); + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestStopSendingHandler(), TEST_ROOT_PATH); + h3TestServerUriError = URI.create("https://" + h3TestServer.serverAuthority() + ERROR_PATH); + h3TestServerUriNoError = URI.create("https://" + h3TestServer.serverAuthority() + NO_ERROR_PATH); + h3TestServer.start(); + + Iterable iterable = EndlessDataChunks::new; + HttpRequest.BodyPublisher testPub = HttpRequest.BodyPublishers.ofByteArrays(iterable); + postRequestNoError = HttpRequest.newBuilder() + .POST(testPub) + .uri(h3TestServerUriNoError) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + postRequestError = HttpRequest.newBuilder() + .POST(testPub) + .uri(h3TestServerUriError) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + postRequestNoErrorWithData = HttpRequest.newBuilder() + .POST(testPub) + .uri(URI.create(h3TestServerUriNoError.toString() + WITH_RESPONSE_BODY_QUERY)) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + postRequestErrorWithData = HttpRequest.newBuilder() + .POST(testPub) + .uri(URI.create(h3TestServerUriError.toString() + WITH_RESPONSE_BODY_QUERY)) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + } + + @AfterTest + public void afterTest() { + h3TestServer.stop(); + } + + static final String RESPONSE_MESSAGE = "May the road rise up to meet you "; + static final String ERROR_MESSAGE = "Forbidden: the data won't be sent "; + static final String NO_BODY = ""; + + static final int MESSAGE_REPEAT = 5; + + static class ServerRequestStopSendingHandler implements HttpTestHandler { + + @Override + public void handle(HttpServerAdapters.HttpTestExchange t) throws IOException { + String query = t.getRequestURI().getQuery(); + System.out.println("Query is: " + query); + boolean withData = WITH_RESPONSE_BODY_QUERY.substring(1).equals(query); + switch (t.getRequestURI().getPath()) { + case NO_ERROR_PATH -> { + final String RESP = withData ? RESPONSE_MESSAGE : NO_BODY; + byte[] resp = RESP.getBytes(StandardCharsets.UTF_8); + System.err.println("Replying with 200 to " + t.getRequestURI().getPath() + + " with data " + resp.length * MESSAGE_REPEAT); + t.sendResponseHeaders(200, resp.length * MESSAGE_REPEAT); + t.requestStopSending(Http3Error.H3_NO_ERROR.code()); + if (resp.length == 0) return; + try (var os = t.getResponseBody()) { + for (int i = 0; i { + final String RESP = withData ? ERROR_MESSAGE : NO_BODY; + byte[] resp = RESP.getBytes(StandardCharsets.UTF_8); + System.err.println("Replying with 403 to " + t.getRequestURI().getPath() + + " with data " + resp.length * MESSAGE_REPEAT); + if (resp.length == 0) { + t.requestStopSending(Http3Error.H3_EXCESSIVE_LOAD.code()); + sleep(100); + t.resetStream(Http3Error.H3_REQUEST_CANCELLED.code()); + } else { + t.sendResponseHeaders(403, resp.length * MESSAGE_REPEAT); + t.requestStopSending(Http3Error.H3_EXCESSIVE_LOAD.code()); + try (var os = t.getResponseBody()) { + for (int i = 0; i < MESSAGE_REPEAT; i++) { + os.write(resp); + os.flush(); + sleep(10); + if (i == MESSAGE_REPEAT - 1) { + t.resetStream(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } + } + } + } + } + } + } + + private static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + + static public class EndlessDataChunks implements Iterator { + + byte[] data = new byte[32]; + boolean hasNext = true; + @Override + public boolean hasNext() { + return hasNext; + } + @Override + public byte[] next() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return data; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java b/test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java new file mode 100644 index 00000000000..916a6a5221d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java @@ -0,0 +1,971 @@ +/* + * 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 id=with-default-wait + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors,http3,quic:control + * -Djdk.internal.httpclient.debug=false + * -Djdk.httpclient.quic.maxBidiStreams=1 + * H3StreamLimitReachedTest + */ + +/* + * @test id=with-no-wait + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors,http3,quic:control + * -Djdk.internal.httpclient.debug=false + * -Djdk.httpclient.quic.maxBidiStreams=1 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.retryOnStreamlimit=9 + * H3StreamLimitReachedTest + */ + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpOption; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +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; +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertNotEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static org.testng.Assert.assertFalse; + +public class H3StreamLimitReachedTest implements HttpServerAdapters { + + private static final String CLASS_NAME = H3ConnectionPoolTest.class.getSimpleName(); + + static int altsvcPort, https2Port, http3Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static volatile HttpClient client = null; + static SSLContext sslContext; + static volatile String http3OnlyURIString, https2URIString, http3AltSvcURIString, http3DirectURIString; + + static void initialize(boolean samePort) throws Exception { + BlockingHandler.GATE.drainPermits(); + BlockingHandler.IN_HANDLER.drainPermits(); + BlockingHandler.PERMITS.set(0); + initialize(samePort, BlockingHandler::new); + } + + static void initialize(boolean samePort, Supplier handlers) throws Exception { + System.out.println("\nConfiguring for advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = null; + client = getClient(); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, sslContext); + if (samePort) { + System.out.println("Attempting to enable advertised HTTP/3 service on same port"); + https2AltSvcServer.enableH3AltServiceOnSamePort(); + System.out.println("Advertised AltSvc on same port " + + (https2AltSvcServer.supportsH3DirectConnection() ? "enabled" : " not enabled")); + } else { + System.out.println("Attempting to enable advertised HTTP/3 service on different port"); + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/https2/"); + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/h2h3/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + altsvcPort = https2AltSvcServer.getH3AltService() + .map(Http3TestServer::getAddress).stream() + .mapToInt(InetSocketAddress::getPort).findFirst() + .getAsInt(); + // server that only supports HTTP/3 - we attempt to use the same port + // as the HTTP/2 server so that we can pretend that the H2 server as two H3 endpoints: + // one advertised (the alt service endpoint og the HTTP/2 server) + // one non advertised (the direct endpoint, at the same authority as HTTP/2, but which + // is in fact our http3OnlyServer) + try { + http3OnlyServer = new Http3TestServer(sslContext, samePort ? 0 : https2Port); + System.out.println("Unadvertised service enabled on " + + (samePort ? "ephemeral port" : "same port")); + } catch (IOException ex) { + System.out.println("Can't create HTTP/3 server on same port: " + ex); + http3OnlyServer = new Http3TestServer(sslContext, 0); + } + http3OnlyServer.addHandler("/" + CLASS_NAME + "/http3/", handlers.get()); + http3OnlyServer.addHandler("/" + CLASS_NAME + "/h2h3/", handlers.get()); + http3OnlyServer.start(); + http3Port = http3OnlyServer.getQuicServer().getAddress().getPort(); + + if (http3Port == https2Port) { + System.out.println("HTTP/3 server enabled on same port than HTTP/2 server"); + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for HTTP/3 server"); + } + } else { + System.out.println("HTTP/3 server enabled on a different port than HTTP/2 server"); + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for HTTP/3 server"); + } + } + if (altsvcPort == https2Port) { + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for advertised AltSvc"); + } + } else { + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for advertised AltSvc"); + } + } + + http3OnlyURIString = "https://" + http3OnlyServer.serverAuthority() + "/" + CLASS_NAME + "/http3/foo/"; + https2URIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/https2/bar/"; + http3DirectURIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/h2h3/direct/"; + http3AltSvcURIString = https2URIString + .replace(":" + https2Port + "/", ":" + altsvcPort + "/") + .replace("/https2/bar/", "/h2h3/altsvc/"); + System.out.println("HTTP/2 server started at: " + https2AltSvcServer.serverAuthority()); + System.out.println(" with advertised HTTP/3 endpoint at: " + + URI.create(http3AltSvcURIString).getRawAuthority()); + System.out.println("HTTP/3 server started at:" + http3OnlyServer.serverAuthority()); + + https2AltSvcServer.start(); + } catch (Throwable e) { + System.out.println("Configuration failed: " + e); + System.err.println("Throwing now: " + e); + e.printStackTrace(); + throw e; + } + } + + static class BlockingHandler implements Http2Handler { + static final AtomicLong REQCOUNT = new AtomicLong(); + static final Semaphore IN_HANDLER = new Semaphore(0); + static final Semaphore GATE = new Semaphore(0); + static final AtomicInteger PERMITS = new AtomicInteger(); + + static void acquireGate() throws InterruptedException { + GATE.acquire(); + System.out.println("GATE acquired: remaining permits: " + + PERMITS.decrementAndGet() + + " (actual: " + GATE.availablePermits() +")"); + } + static void releaseGate() throws InterruptedException { + int permits = PERMITS.incrementAndGet(); + GATE.release(); + System.out.println("GATE released: remaining permits: " + + permits + " (actual: " + GATE.availablePermits() +")"); + } + + static void releaseGate(int permits) throws InterruptedException { + int npermits = PERMITS.addAndGet(permits); + GATE.release(npermits); + System.out.println("GATE released (" + permits + "): remaining permits: " + + npermits + " (actual: " + GATE.availablePermits() +")"); + } + + @Override + public void handle(Http2TestExchange t) throws IOException { + long count = REQCOUNT.incrementAndGet(); + byte[] resp; + int status; + try { + try { + IN_HANDLER.release(); + System.out.printf("*** Server [%s] waiting for GATE: %s%n", + count, t.getRequestURI()); + System.err.printf("*** Server [%s] waiting for GATE: %s%n", + count, t.getRequestURI()); + acquireGate(); + System.err.printf("*** Server [%s] GATE acquired: %s%n", + count, t.getRequestURI()); + status = 200; + resp = "Request %s OK".formatted(count) + .getBytes(StandardCharsets.UTF_8); + } catch (InterruptedException x) { + status = 500; + resp = "Request %s interrupted: %s" + .formatted(count, x) + .getBytes(StandardCharsets.UTF_8); + } + System.out.printf("*** Server [%s] headers for: %s%n\t%s%n", + count, t.getRequestURI(), t.getRequestHeaders()); + System.out.printf("*** Server [%s] reading body for: %s%n", + count, t.getRequestURI()); + t.getRequestBody().readAllBytes(); + System.out.printf("*** Server [%s] sending headers for: %s%n", + count, t.getRequestURI()); + t.sendResponseHeaders(status, resp.length); + System.out.printf("*** Server [%s] sending body %s (%s bytes) for: %s%n", + count, status, resp.length, t.getRequestURI()); + try (var body = t.getResponseBody()) { + body.write(resp); + } + System.out.printf("*** Server [%s] response %s sent for: %s%n", + count, status, t.getRequestURI()); + } catch (Throwable throwable) { + var msg = String.format("Server [%s] response failed for: %s", + count, t.getRequestURI()); + System.out.printf("*** %s%n\t%s%n", msg, throwable); + var error = new IOException(msg,throwable); + error.printStackTrace(System.out); + System.err.printf("*** %s%n\t%s%n", msg, throwable); + //GATE.release(); + throw error; + } + } + } + + private static void printResponse(String name, + HttpOption option, + HttpResponse response) { + printResponse("%s %s".formatted( + name, response.request().getOption(option)), + response); + } + + private static void printResponse(String name, HttpResponse response) { + System.out.printf("%s: (%s): %s%n", + name, response.connectionLabel(), response); + response.headers().map().entrySet().forEach((e) -> { + System.out.printf(" %s: %s%n", e.getKey(), e.getValue()); + }); + System.out.printf(" :body: \"%s\"%n%n", response.body()); + } + + @Test + public static void testH3Only() throws Exception { + System.out.println("\nTesting HTTP/3 only"); + initialize(true); + try (HttpClient client = getClient()) { + var reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(http3OnlyURIString)) + .version(HTTP_3) + .GET(); + HttpRequest request1 = reqBuilder.copy() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + var responseCF1 = client.sendAsync(request1, BodyHandlers.ofString()); + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // ANY should reuse the same connection, get stream limit reached, + // and open a new connection + HttpRequest request2 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // release both + BlockingHandler.GATE.release(2); + + var response1 = responseCF1.get(); + printResponse("First response", response1); + var response2 = responseCF2.get(); + printResponse("Second response", response2); + + // set a timeout to make sure we wait long enough for + // the MAX_STREAMS update to reach us before attempting + // to create the HTTP/3 exchange. + HttpRequest request3 = reqBuilder.copy() + .timeout(Duration.ofSeconds(30)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + // request3 should now reuse the connection opened by request2 + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // ANY should reuse the same connection as request3, + // get stream limit exception, and open a new connection + HttpRequest request4 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + var responseCF4 = client.sendAsync(request4, BodyHandlers.ofString()); + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // release both response 3 and response 4 + BlockingHandler.GATE.release(2); + + var response3 = responseCF3.get(); + printResponse("Third response", response3); + var response4 = responseCF4.get(); + printResponse("Fourth response", response4); + + assertNotEquals(response1.connectionLabel().get(), response2.connectionLabel().get()); + assertEquals(response2.connectionLabel().get(), response3.connectionLabel().get()); + assertNotEquals(response1.connectionLabel().get(), response4.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), response4.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response4.connectionLabel().get()); + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testH2H3WithTwoAltSVC() throws Exception { + testH2H3(false); + } + + @Test + public static void testH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3(true); + } + + private static void testH2H3(boolean samePort) throws Exception { + System.out.println("\nTesting with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + var responseCF1 = client.sendAsync(request1, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + var h2resp2CF = client.sendAsync(request2, BodyHandlers.ofString()); + // Blocking until the handler is invoked should be enough + // to ensure we get the AltSvc frame before the next request. + BlockingHandler.IN_HANDLER.acquire(); + BlockingHandler.GATE.release(3); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + // There's still a potential race here - to avoid it + // we need h2resp2 = h2resp2CF.get(); before sending request2 + var h2resp2 = h2resp2CF.get(); + System.out.printf("Got expected h2 response: %s [%s]%n", + h2resp2, h2resp2.version()); + + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + var response1 = responseCF1.get(); + printResponse("response 1", H3_DISCOVERY, response1); + var response2 = responseCF2.get(); + printResponse("response 2", H3_DISCOVERY, response2); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // second request with HTTP3_URI_ONLY should reuse a created connection + // It should reuse the advertised connection (from response2) if same + // origin + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request3 = req1Builder.copy() + .timeout(Duration.ofSeconds(30)).build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + // It should not reuse the connection from response1, it should + // not invoke reconnection on the connection used by response 3 + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request4 = req2Builder.copy() + .timeout(Duration.ofSeconds(30)).build(); + var responseCF4 = client.sendAsync(request4, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // release both + BlockingHandler.GATE.release(2); + + var response3 = responseCF3.get(); + printResponse("response 3", H3_DISCOVERY, response3); + var response4 = responseCF4.get(); + printResponse("response 4", H3_DISCOVERY, response4); + + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response1.connectionLabel().get(), response3.connectionLabel().get()); + + assertEquals(HTTP_3, response4.version()); + checkStatus(200, response4.statusCode()); + assertEquals(response2.connectionLabel().get(), response4.connectionLabel().get()); + + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + BlockingHandler.GATE.release(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + System.out.printf("Got expected h2 response: %s [%s]%n", + h2resp2, h2resp2.version()); + + // second - make a direct connection + HttpRequest request1 = req1Builder.copy() + .uri(URI.create(http3DirectURIString+"?request1")).build(); + var responseCF1 = client.sendAsync(request1, BodyHandlers.ofString()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + var req2 = req2Builder.copy() + .uri(URI.create(http3DirectURIString + "?request2")) + .build(); + var responseCF2 = client.sendAsync(req2, BodyHandlers.ofString()); + + BlockingHandler.IN_HANDLER.acquire(2); + BlockingHandler.GATE.release(2); + + var response1 = responseCF1.get(); + printResponse("response 1", H3_DISCOVERY, response1); + var response2 = responseCF2.get(); + printResponse("response 2", H3_DISCOVERY, response2); + + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request3 = req2Builder.copy() + .uri(URI.create(http3DirectURIString + "?request3")) + .timeout(Duration.ofSeconds(30)).build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + + // fourth request with HTTP_3_URI_ONLY should reuse the first connection, + // and not reuse the second. + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request4 = req1Builder.copy() + .uri(URI.create(http3DirectURIString + "?request4")) + .timeout(Duration.ofSeconds(30)).build(); + var responseCF4 = client.sendAsync(request4, BodyHandlers.ofString()); + + BlockingHandler.IN_HANDLER.acquire(2); + BlockingHandler.GATE.release(2); + + var response3 = responseCF3.get(); + printResponse("response 3", H3_DISCOVERY, response3); + var response4 = responseCF4.get(); + printResponse("response 4", H3_DISCOVERY, response4); + + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response2.connectionLabel().get(), response3.connectionLabel().get()); + assertNotEquals(response1.connectionLabel().get(), response3.connectionLabel().get()); + assertEquals(HTTP_3, response4.version()); + assertEquals(response1.connectionLabel().get(), response4.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response4.connectionLabel().get()); + checkStatus(200, response1.statusCode()); + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + BlockingHandler.GATE.release(1); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin, get + // StreamLimitReached exception, and create a new connection + HttpRequest request3 = req2Builder.copy().build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + BlockingHandler.GATE.release(2); + + var response2 = responseCF2.get(); + printResponse("response 2", H3_DISCOVERY, response2); + + var response3 = responseCF3.get(); + printResponse("response 3", H3_DISCOVERY, response3); + + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testParallelH2H3WithTwoAltSVC() throws Exception { + testH2H3Concurrent(false); + } + + @Test + public static void testParallelH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3Concurrent(true); + } + + + private static void testH2H3Concurrent(boolean samePort) throws Exception { + System.out.println("\nTesting concurrent reconnections with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing reconnections with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + HttpRequest request2 = req2Builder.copy().build(); + List>> directResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + HttpRequest req1 = req1Builder.copy() + .uri(URI.create(http3DirectURIString+"?dir="+i)).build(); + directResponses.add(client.sendAsync(req1, BodyHandlers.ofString())); + BlockingHandler.IN_HANDLER.acquire(); + } + // can't send requests in parallel here because if any establishes + // a connection before the H3 direct are established, then the H3 + // direct might reuse the H3 alt since the service is with same origin + BlockingHandler.releaseGate(directResponses.size() + 1); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + CompletableFuture.allOf(directResponses.stream() + .toArray(CompletableFuture[]::new)).exceptionally((t) -> null) + .join(); + + Set c1Label = new HashSet<>(); + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s]: %s%n", i, + response1.connectionLabel(), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + var cLabel = response1.connectionLabel().get(); + assertFalse(c1Label.contains(cLabel), + "%s contained in %s".formatted(cLabel, c1Label)); + c1Label.add(cLabel); + } + + // first request with ALT_SVC is to get alt service, should be H2 + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + assertFalse(c1Label.contains(h2resp2.connectionLabel().get()), + "%s contained in %s!".formatted(h2resp2.connectionLabel().get(), c1Label)); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + List>> altResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + HttpRequest req2 = req2Builder.copy() + .uri(URI.create(http3DirectURIString+"?alt="+i)).build(); + altResponses.add(client.sendAsync(req2, BodyHandlers.ofString())); + } + + BlockingHandler.releaseGate(altResponses.size()); + BlockingHandler.IN_HANDLER.acquire(altResponses.size()); + + CompletableFuture.allOf(altResponses.stream().toArray(CompletableFuture[]::new)) + .exceptionally((t) -> null) + .join(); + + Set c2Label = new HashSet<>(); + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s]: %s%n", i, + response2.connectionLabel(), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + var cLabel = response2.connectionLabel().get(); + if (c2Label.contains(cLabel)) { + System.out.printf("Connection %s reused%n", cLabel); + } + c2Label.add(cLabel); + assertFalse(c1Label.contains(cLabel), + "%s contained in %s".formatted(cLabel, c1Label)); + // cLabel could already be in c2Label, if the previous + // request finished before the next one. + } + + System.out.println("Sending mixed requests"); + + // second set of requests should reuse a created connection + HttpRequest request3 = req1Builder.copy().build(); + List>> mixResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + var builder1 = req1Builder.copy() + .uri(URI.create(http3DirectURIString+"?mix1="+i)); + if (i == 0) { + // make sure to give time for MAX_STREAMS to arrive + builder1 = builder1.timeout(Duration.ofSeconds(30)); + } + HttpRequest req1 = builder1.build(); + mixResponses.add(client.sendAsync(req1, BodyHandlers.ofString())); + if (i == 0) BlockingHandler.IN_HANDLER.acquire(); + var builder2 = req2Builder.copy() + .uri(URI.create(http3DirectURIString+"?mix2="+i)); + if (i == 0) { + // make sure to give time for MAX_STREAMS to arrive + builder2 = builder2.timeout(Duration.ofSeconds(30)); + } + HttpRequest req2 = builder2.build(); + mixResponses.add(client.sendAsync(req2, BodyHandlers.ofString())); + if (i == 0) BlockingHandler.IN_HANDLER.acquire(); + } + + System.out.println("IN_HANDLER.acquire(" + + (mixResponses.size() - 2) + ") - available: " + + BlockingHandler.IN_HANDLER.availablePermits()); + BlockingHandler.IN_HANDLER.acquire(mixResponses.size() - 2); + BlockingHandler.releaseGate(mixResponses.size()); + + System.out.println("Getting mixed responses"); + + CompletableFuture.allOf(mixResponses.stream().toArray(CompletableFuture[]::new)) + .exceptionally((t) -> null) + .join(); + + System.out.println("All mixed responses received"); + + Set mixC1Label = new HashSet<>(); + Set mixC2Label = new HashSet<>(); + for (int i = 0; i < mixResponses.size(); i++) { + HttpResponse response3 = mixResponses.get(i).get(); + System.out.printf("mixed response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + var cLabel = response3.connectionLabel().get(); + if (response3.request().getOption(H3_DISCOVERY).orElse(null) == ALT_SVC) { + if (i == 0 || i == 1) { + assertTrue(c2Label.contains(cLabel), + "%s not in %s".formatted(cLabel, c2Label)); + System.out.printf("first ALTSVC connection reused %s from %s%n", cLabel, c2Label); + } else { + assertFalse(c2Label.contains(cLabel), + "%s in %s".formatted(cLabel, c2Label)); + } + assertFalse(c1Label.contains(cLabel), + "%s in %s".formatted(cLabel, c1Label)); + assertFalse(mixC1Label.contains(cLabel), + "%s in %s".formatted(cLabel, mixC1Label)); + if (mixC2Label.contains(cLabel)) { + System.out.printf("ALTSVC connection reused %s from %s%n", cLabel, mixC2Label); + } + mixC2Label.add(cLabel); + } else { + if (i == 0 || i == 1) { + assertTrue(c1Label.contains(cLabel), + "%s not in %s".formatted(cLabel, c1Label)); + System.out.printf("first ALTSVC connection reused %s from %s%n", cLabel, c1Label); + } else { + assertFalse(c1Label.contains(cLabel), + "%s in %s".formatted(cLabel, c1Label)); + } + assertFalse(c2Label.contains(cLabel), + "%s in %s".formatted(cLabel, c2Label)); + assertFalse(mixC2Label.contains(cLabel), + "%s in %s".formatted(cLabel, mixC2Label)); + if (mixC1Label.contains(cLabel)) { + System.out.printf("ALTSVC connection reused %s from %s%n", cLabel, mixC1Label); + } + mixC1Label.add(cLabel); + } + } + System.out.println("All done"); + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first - make a direct connection + HttpRequest request1 = req1Builder.copy().build(); + + // second, use the alt service + HttpRequest request2 = req2Builder.copy().build(); + BlockingHandler.GATE.release(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + + // third, use ANY + HttpRequest request3 = req2Builder.copy().setOption(H3_DISCOVERY, ANY).build(); + + List>> directResponses = new ArrayList<>(); + List>> altResponses = new ArrayList<>(); + List>> anyResponses = new ArrayList<>(); + checkStatus(200, h2resp2.statusCode()); + + // We're going to send nine requests here. We could get + // "No more stream available on connection" when we run with + // a stream limit timeout of 0, unless we raise the retry on + // stream limit to at least 6 + for (int i = 0; i < 3; i++) { + anyResponses.add(client.sendAsync(request3, BodyHandlers.ofString())); + directResponses.add(client.sendAsync(request1, BodyHandlers.ofString())); + altResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + + var all = new ArrayList<>(directResponses); + all.addAll(altResponses); + all.addAll(anyResponses); + int requestCount = all.size(); + + BlockingHandler.GATE.release(requestCount); + CompletableFuture.allOf(all.stream().toArray(CompletableFuture[]::new)) + .exceptionally((t) -> null) + .join(); + + Set c1Label = new HashSet<>(); + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s] %s: %s%n", i, + response1.connectionLabel(), + response1.request().getOption(H3_DISCOVERY), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + + var cLabel = response1.connectionLabel().get(); + if (c1Label.contains(cLabel)) { + System.out.printf(" connection %s reused from %s%n", cLabel, c1Label); + } + c1Label.add(cLabel); + } + Set c2Label = new HashSet<>(); + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s] %s: %s%n", i, + response2.connectionLabel(), + response2.request().getOption(H3_DISCOVERY), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + + var cLabel = response2.connectionLabel().get(); + if (c2Label.contains(cLabel)) { + System.out.printf(" connection %s reused from %s%n", cLabel, c2Label); + } + assertNotEquals(cLabel, h2resp2.connectionLabel().get()); + assertFalse(c1Label.contains(cLabel), + "%s found in %s".formatted(cLabel, c1Label)); + c2Label.add(cLabel); + } + + var diff = new HashSet<>(c2Label); + diff.retainAll(c1Label); + assertTrue(diff.isEmpty()); + + var anyLabels = new HashSet(Set.of(c1Label, c2Label)); + for (int i = 0; i < anyResponses.size(); i++) { + HttpResponse response3 = anyResponses.get(i).get(); + System.out.printf("any response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), h2resp2.connectionLabel().get()); + var label = response3.connectionLabel().orElse(""); + if (anyLabels.contains(label)) { + System.out.printf(" connection %s reused from %s%n", label, anyLabels); + } + } + BlockingHandler.IN_HANDLER.acquire(requestCount); + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + BlockingHandler.GATE.release(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + + // second request should have ALT_SVC and create new connection with H3 + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin, get + // stream limit, and create a new connection + HttpRequest request3 = req2Builder.copy().build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + BlockingHandler.GATE.release(2); + + CompletableFuture.allOf(responseCF2, responseCF3) + .exceptionally((t) -> null) + .join(); + + var response2 = responseCF2.get(); + printResponse("first HTTP/3 request", H3_DISCOVERY, response2); + var response3 = responseCF3.get(); + printResponse("second HTTP/3 request", H3_DISCOVERY, response2); + + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } catch (Throwable t) { + t.printStackTrace(System.out); + throw t; + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + static HttpClient getClient() { + if (client == null) { + client = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + + static T logExceptionally(String desc, Throwable t) { + System.out.println(desc + " failed: " + t); + System.err.println(desc + " failed: " + t); + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + throw new CompletionException(t); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3Timeout.java b/test/jdk/java/net/httpclient/http3/H3Timeout.java new file mode 100644 index 00000000000..81a0f87467b --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3Timeout.java @@ -0,0 +1,187 @@ +/* + * 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. + * + * 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. + */ + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpTimeoutException; +import java.time.Duration; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.OperationTrackers.Tracker; +import jdk.test.lib.net.SimpleSSLContext; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @bug 8156710 + * @summary Check if HttpTimeoutException is thrown if a server doesn't reply + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run main/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors H3Timeout + */ +public class H3Timeout implements HttpServerAdapters { + + private static final int TIMEOUT = 2 * 1000; // in millis + private static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + + public static void main(String[] args) throws Exception { + SSLContext context = new SimpleSSLContext().get(); + testConnect(context, false); + testConnect(context, true); + testTimeout(context, false); + testTimeout(context, true); + } + + public static void testConnect(SSLContext context, boolean async) throws Exception { + + InetSocketAddress loopback = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + try (DatagramSocket socket = new DatagramSocket(loopback)) { + String address = socket.getLocalAddress().getHostAddress(); + if (address.indexOf(':') >= 0) { + if (!address.startsWith("[") || !address.endsWith("]")) { + address = "[" + address + "]"; + } + } + String serverAuth = address + ":" + socket.getLocalPort(); + String uri = "https://" + serverAuth + "/"; + HttpTimeoutException expected; + if (async) { + System.out.println(uri + ": Trying to connect asynchronously"); + expected = connectAsync(context, uri); + } else { + System.out.println(uri + ": Trying to connect synchronously"); + expected = connect(context, uri); + } + if (!(expected instanceof HttpConnectTimeoutException)) { + throw new AssertionError("expected HttpConnectTimeoutException, got: " + + expected, expected); + } + } + } + + public static void testTimeout(SSLContext context, boolean async) throws Exception { + + CountDownLatch latch = new CountDownLatch(1); + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, context); + server.addHandler((exch) -> { + try { + System.err.println("server reading request"); + byte[] req = exch.getRequestBody().readAllBytes(); + System.err.printf("server got request: %s bytes", req.length); + latch.await(); + exch.sendResponseHeaders(500, 0); + } catch (Exception e) { + System.err.println("server exception: " + e); + } + }, "/"); + server.start(); + try { + String serverAuth = server.serverAuthority(); + String uri = "https://" + serverAuth + "/"; + HttpTimeoutException expected; + if (async) { + System.out.println(uri + ": Trying to connect asynchronously"); + expected = connectAsync(context, uri); + } else { + System.out.println(uri + ": Trying to connect synchronously"); + expected = connect(context, uri); + } + assert expected instanceof HttpTimeoutException; + } finally { + latch.countDown(); + server.stop(); + } + } + + private static HttpTimeoutException connect(SSLContext context, String server) throws Exception { + HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .version(HTTP_3) + .sslContext(context) + .build(); + try { + HttpRequest request = HttpRequest.newBuilder(new URI(server)) + .timeout(Duration.ofMillis(TIMEOUT)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .POST(BodyPublishers.ofString("body")) + .build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + System.out.println("Received unexpected reply: " + response.statusCode()); + throw new RuntimeException("unexpected successful connection"); + } catch (HttpTimeoutException e) { + System.out.println("expected exception: " + e); + return e; + } finally { + client.shutdown(); + if (!client.awaitTermination(Duration.ofSeconds(5))) { + Tracker tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 5000); + if (error != null) throw error; + } + } + } + + private static HttpTimeoutException connectAsync(SSLContext context, String server) throws Exception { + try (HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .version(HTTP_3) + .sslContext(context) + .build()) { + try { + HttpRequest request = HttpRequest.newBuilder(new URI(server)) + .timeout(Duration.ofMillis(TIMEOUT)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .POST(BodyPublishers.ofString("body")) + .build(); + HttpResponse response = client.sendAsync(request, BodyHandlers.ofString()).join(); + System.out.println("Received unexpected reply: " + response.statusCode()); + throw new RuntimeException("unexpected successful connection"); + } catch (CompletionException e) { + var cause = e.getCause(); + if (cause instanceof HttpTimeoutException timeout) { + System.out.println("expected exception: " + e.getCause()); + return timeout; + } else { + throw new RuntimeException("Unexpected exception received: " + e.getCause(), e); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java b/test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java new file mode 100644 index 00000000000..090dec2c189 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java @@ -0,0 +1,78 @@ +/* + * 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. + * + * 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. + */ +import java.io.UncheckedIOException; +import java.net.http.HttpClient; +import java.net.http.UnsupportedProtocolVersionException; + +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary Tests that a HttpClient configured with SSLParameters that doesn't include TLSv1.3 + * cannot be used for HTTP3 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @run junit H3UnsupportedSSLParametersTest + */ +public class H3UnsupportedSSLParametersTest { + + /** + * Configures a HttpClient builder to use a SSLParameter which doesn't list TLSv1.3 + * as one of the supported protocols. The method then uses this builder + * to create a HttpClient for HTTP3 and expects that build() to fail with + * UnsupportedProtocolVersionException + */ + @Test + public void testNoTLSv13() throws Exception { + final SSLParameters params = new SSLParameters(); + params.setProtocols(new String[]{"TLSv1.2"}); + final UncheckedIOException uioe = assertThrows(UncheckedIOException.class, + () -> HttpServerAdapters.createClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HttpClient.Version.HTTP_3) + .sslParameters(params) + .build()); + assertTrue(uioe.getCause() instanceof UnsupportedProtocolVersionException, + "Unexpected cause " + uioe.getCause() + " in HttpClient build failure"); + } + + /** + * Builds a HttpClient with SSLParameters which explicitly lists TLSv1.3 as one of the supported + * protocol versions and expects the build() to succeed and return a HttpClient instance + */ + @Test + public void testExplicitTLSv13() throws Exception { + final SSLParameters params = new SSLParameters(); + params.setProtocols(new String[]{"TLSv1.2", "TLSv1.3"}); + final HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslParameters(params) + .version(HttpClient.Version.HTTP_3).build(); + assertNotNull(client, "HttpClient is null"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3UserInfoTest.java b/test/jdk/java/net/httpclient/http3/H3UserInfoTest.java new file mode 100644 index 00000000000..15e25b865cd --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3UserInfoTest.java @@ -0,0 +1,193 @@ +/* + * 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. + * + * 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. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +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 org.junit.jupiter.params.provider.Arguments; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.stream.Stream; + + +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.junit.jupiter.api.Assertions.assertEquals; +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; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @bug 8292876 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run junit/othervm -Djdk.httpclient.HttpClient.log=quic,errors + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=4000 + * -Djdk.internal.httpclient.debug=true H3UserInfoTest + */ + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class H3UserInfoTest implements HttpServerAdapters { + + static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + static HttpTestServer server; + static HttpTestServer server3; + static String serverURI; + static String server3URI; + static SSLContext sslContext; + + @BeforeAll + static void before() throws Exception { + sslContext = new SimpleSSLContext().get(); + HttpTestHandler handler = new HttpHandler(); + + server = HttpTestServer.create(ANY, sslContext); + server.addHandler(handler, "/"); + serverURI = "https://" + server.serverAuthority() +"/http3-any/"; + server.start(); + + server3 = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + server3.addHandler(handler, "/"); + server3URI = "https://" + server3.serverAuthority() +"/http3-only/"; + server3.start(); + } + + @AfterAll + static void after() throws Exception { + server.stop(); + server3.stop(); + } + + static class HttpHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange e) throws IOException { + String authorityHeader = e.getRequestHeaders() + .firstValue(":authority") + .orElse(null); + if (authorityHeader == null || authorityHeader.contains("user@")) { + e.sendResponseHeaders(500, 0); + } else { + e.sendResponseHeaders(200, 0); + } + } + } + + public static Stream servers() { + return Stream.of( + Arguments.arguments(serverURI, server), + Arguments.arguments(server3URI, server3) + ); + } + + @ParameterizedTest + @MethodSource("servers") + public void testAuthorityHeader(String serverURI, HttpTestServer server) throws Exception { + try (HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(sslContext) + .build()) { + TRACKER.track(client); + + URI origURI = URI.create(serverURI); + URI uri = URIBuilder.newBuilder() + .scheme("https") + .userInfo("user") + .host(origURI.getHost()) + .port(origURI.getPort()) + .path(origURI.getRawPath()) + .build(); + var config = server.h3DiscoveryConfig(); + + int numRetries = 0; + while (true) { + if (config == ALT_SVC) { + // send head request + System.out.printf("Sending head request (%s) to %s%n", config, origURI); + System.err.printf("Sending head request (%s) to %s%n", config, origURI); + HttpRequest head = HttpRequest.newBuilder(origURI) + .HEAD() + .version(HTTP_2) + .build(); + var headResponse = client.send(head, BodyHandlers.ofString()); + assertEquals(200, headResponse.statusCode()); + assertEquals(HTTP_2, headResponse.version()); + assertEquals("", headResponse.body()); + } + + HttpRequest request = HttpRequest + .newBuilder(uri) + .setOption(H3_DISCOVERY, config) + .version(HTTP_3) + .GET() + .build(); + + System.out.printf("Sending GET request (%s) to %s%n", config, origURI); + System.err.printf("Sending GET request (%s) to %s%n", config, origURI); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), + "Test Failed : " + response.uri().getAuthority()); + assertEquals("", response.body()); + if (config != ANY) { + assertEquals(HTTP_3, response.version()); + } else if (response.version() != HTTP_3) { + // the request went through HTTP/2 - the next + // should go through HTTP/3 + if (numRetries++ < 3) { + System.out.printf("Received GET response (%s) to %s with version %s: " + + "repeating request once more%n", config, origURI, response.version()); + System.err.printf("Received GET response (%s) to %s with version %s: " + + "repeating request once more%n", config, origURI, response.version()); + assertEquals(HTTP_2, response.version()); + continue; + } else { + fail("Did not receive the expected HTTP3 response"); + } + } + break; + } + } + // the client should already be closed, but its facade ref might + // not have been cleared by GC yet. + System.gc(); + var error = TRACKER.checkClosed(1500); + if (error != null) throw error; + } +} diff --git a/test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java b/test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java new file mode 100644 index 00000000000..f04588e9362 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java @@ -0,0 +1,324 @@ +/* + * 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 + * 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 + * @key randomness + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http3.Http3TestServer + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors + * -Djdk.internal.httpclient.debug=true + * HTTP3NoBodyTest + * @summary this is a copy of http2/NoBodyTest over HTTP/3 + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.*; +import javax.net.ssl.*; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Random; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +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; +import static java.net.http.HttpOption.H3_DISCOVERY; + +@Test +public class HTTP3NoBodyTest { + private static final Random RANDOM = RandomFactory.getRandom(); + + static int http3Port, https2Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static HttpClient client = null; + static ExecutorService clientExec; + static ExecutorService serverExec; + static SSLContext sslContext; + static final String TEST_STRING = "The quick brown fox jumps over the lazy dog "; + + static volatile String http3URIString, https2URIString; + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + // server that only supports HTTP/3 + http3OnlyServer = new Http3TestServer(sslContext, serverExec); + http3OnlyServer.addHandler("/", new Handler()); + http3Port = http3OnlyServer.getAddress().getPort(); + System.out.println("HTTP/3 server started at localhost:" + http3Port); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, 0, serverExec, sslContext); + if (RANDOM.nextBoolean()) { + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } else { + https2AltSvcServer.enableH3AltServiceOnSamePort(); + } + https2AltSvcServer.addHandler(new Handler(), "/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + if (https2AltSvcServer.supportsH3DirectConnection()) { + System.out.println("HTTP/2 server (same HTTP/3 origin) started at localhost:" + https2Port); + } else { + System.out.println("HTTP/2 server (different HTTP/3 origin) started at localhost:" + https2Port); + } + + http3URIString = "https://localhost:" + http3Port + "/foo/"; + https2URIString = "https://localhost:" + https2Port + "/bar/"; + + http3OnlyServer.start(); + https2AltSvcServer.start(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(System.err); + throw e; + } + } + + @Test + public static void runtest() throws Exception { + try { + initialize(); + warmup(false); + warmup(true); + test(false); + test(true); + if (client != null) { + var tracker = ReferenceTracker.INSTANCE; + tracker.track(client); + client = null; + System.gc(); + var error = tracker.check(1500); + if (error != null) throw error; + } + } catch (Throwable tt) { + System.err.println("Unexpected Throwable caught"); + tt.printStackTrace(System.err); + throw tt; + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + serverExec.close(); + clientExec.close(); + } + } + + static HttpClient getClient() { + if (client == null) { + serverExec = Executors.newCachedThreadPool(); + clientExec = Executors.newCachedThreadPool(); + client = HttpServerAdapters.createClientBuilderForH3() + .executor(clientExec) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI(boolean altSvc) { + return getURI(altSvc, -1); + } + + static URI getURI(boolean altSvc, int step) { + return URI.create(getURIString(altSvc, step)); + } + + static String getURIString(boolean altSvc, int step) { + var uriStr = altSvc ? https2URIString : http3URIString; + return step >= 0 ? (uriStr + step) : uriStr; + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static final AtomicInteger count = new AtomicInteger(); + static Http3DiscoveryMode config(boolean http3only) { + if (http3only) return HTTP_3_URI_ONLY; + // if the server supports H3 direct connection, we can + // additionally use HTTP_3_URI_ONLY; Otherwise we can + // only use ALT_SVC - or ANY (given that we should have + // preloaded an ALT_SVC in warmup) + int bound = https2AltSvcServer.supportsH3DirectConnection() ? 4 : 3; + int rand = RANDOM.nextInt(bound); + count.getAndIncrement(); + return switch (rand) { + case 1 -> ANY; + case 2 -> ALT_SVC; + case 3 -> HTTP_3_URI_ONLY; + default -> null; + }; + } + + static final int LOOPS = 13; + + static void warmup(boolean altSvc) throws Exception { + URI uri = getURI(altSvc); + String type = altSvc ? "http2" : "http3"; + System.out.println("warmup: " + type); + System.err.println("Request to " + uri); + var http3Only = altSvc == false; + var config = config(http3Only); + + // in the warmup phase, we want to make sure + // to preload the ALT_SVC, otherwise the first + // request that uses ALT_SVC might go through HTTP/2 + if (altSvc) config = ALT_SVC; + + // Do a simple warmup request + + HttpClient client = getClient(); + var builder = HttpRequest.newBuilder(uri); + HttpRequest req = builder + .POST(BodyPublishers.ofString("Random text")) + .setOption(H3_DISCOVERY, config) + .build(); + HttpResponse response = client.send(req, BodyHandlers.ofString()); + checkStatus(200, response.statusCode()); + String responseBody = response.body(); + HttpHeaders h = response.headers(); + checkStrings(TEST_STRING + type, responseBody); + System.out.println("warmup: " + type + " done"); + System.err.println("warmup: " + type + " done"); + } + + static void test(boolean http2) throws Exception { + URI uri = getURI(http2); + String type = http2 ? "http2" : "http3"; + System.err.println("Request to " + uri); + var http3Only = http2 == false; + for (int i = 0; i < LOOPS; i++) { + var config = config(http3Only); + URI uri2 = getURI(http2, i); + HttpRequest request = HttpRequest.newBuilder(uri2) + .POST(BodyPublishers.ofString(TEST_STRING)) + .setOption(H3_DISCOVERY, config) + .build(); + System.out.println(type + ": Loop " + i + ", config: " + config + ", uri: " + uri2); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int expectedResponse = (i % 2) == 0 ? 200 : 204; + if (response.statusCode() != expectedResponse) + throw new RuntimeException("wrong response code " + response.statusCode()); + if (expectedResponse == 200 && !response.body().equals(TEST_STRING + type)) { + System.err.printf(type + " response received/expected %s/%s\n", response.body(), TEST_STRING + type); + throw new RuntimeException("wrong response body"); + } + if (response.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + response.version()); + } + System.out.println(type + ": Loop " + i + " done"); + } + System.err.println("test: " + type + " DONE"); + } + + static URI base(URI uri) { + var uriStr = uri.toString(); + if (uriStr.startsWith(http3URIString)) { + if (uriStr.equals(http3URIString)) return uri; + return URI.create(http3URIString); + } else if (uri.toString().startsWith(https2URIString)) { + if (uriStr.equals(https2URIString)) return uri; + return URI.create(https2URIString); + } else return uri; + } + + static class Handler implements Http2Handler { + + public Handler() {} + + volatile int invocation = 0; + + @Override + public void handle(Http2TestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request to %s from %s\n", + uri, t.getRemoteAddress()); + String type = uri.toString().startsWith(http3URIString) + ? "http3" : "http2"; + InputStream is = t.getRequestBody(); + while (is.read() != -1); + is.close(); + + // every second response is 204. + var base = base(uri); + int step = base == uri ? 0 : Integer.parseInt(base.relativize(uri).toString()); + invocation++; + + if ((step++ % 2) == 1) { + System.err.println("Server sending 204"); + t.sendResponseHeaders(204, -1); + } else { + System.err.println("Server sending 200"); + String body = TEST_STRING + type; + t.sendResponseHeaders(200, body.length()); + OutputStream os = t.getResponseBody(); + os.write(body.getBytes()); + os.close(); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java b/test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java new file mode 100644 index 00000000000..2052671b697 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java @@ -0,0 +1,250 @@ +/* + * 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. + * + * 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 + * @summary Tests Http3 expect continue + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @compile ../ReferenceTracker.java + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,headers + * Http3ExpectContinueTest + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.TestException; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static org.testng.Assert.*; + +public class Http3ExpectContinueTest implements HttpServerAdapters { + + ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + + Http3TestServer http3TestServer; + + URI h3postUri, h3forcePostUri, h3hangUri; + + static PrintStream err = new PrintStream(System.err); + static PrintStream out = new PrintStream(System.out); + static final String EXPECTATION_FAILED_417 = "417 Expectation Failed"; + static final String CONTINUE_100 = "100 Continue"; + static final String RESPONSE_BODY= "Verify response received"; + static final String BODY = "Post body"; + private SSLContext sslContext; + + @DataProvider(name = "uris") + public Object[][] urisData() { + return new Object[][]{ + // URI, Expected Status Code, Will finish with Exception + { h3postUri, 200, false }, + { h3forcePostUri, 200, false }, + { h3hangUri, 417, false }, + }; + } + + @Test(dataProvider = "uris") + public void test(URI uri, int expectedStatusCode, boolean exceptionally) + throws CancellationException, InterruptedException, ExecutionException, IOException { + + err.printf("\nTesting URI: %s, exceptionally: %b\n", uri, exceptionally); + out.printf("\nTesting URI: %s, exceptionally: %b\n", uri, exceptionally); + HttpClient client = newClientBuilderForH3(). + proxy(Builder.NO_PROXY) + .version(HTTP_3).sslContext(sslContext) + .build(); + AssertionError failed = null; + TRACKER.track(client); + try { + HttpResponse resp = null; + Throwable testThrowable = null; + + HttpRequest postRequest = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(HttpOption.H3_DISCOVERY, + Http3DiscoveryMode.HTTP_3_URI_ONLY) + .POST(HttpRequest.BodyPublishers.ofString(BODY)) + .expectContinue(true) + .build(); + + err.printf("Sending request: %s%n", postRequest); + CompletableFuture> cf = client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString()); + try { + resp = cf.get(); + } catch (Exception e) { + testThrowable = e.getCause(); + } + verifyRequest(uri.getPath(), expectedStatusCode, resp, exceptionally, testThrowable); + } catch (Throwable x) { + failed = new AssertionError("Unexpected exception:" + x, x); + } finally { + client.shutdown(); + if (!client.awaitTermination(Duration.ofMillis(1000))) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 2000); + if (error != null || failed != null) { + var ex = failed == null ? error : failed; + err.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, ex); + out.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, ex); + } + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + throw failed; + } + throw error; + } + } + } + if (failed != null) { + err.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, failed); + out.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, failed); + throw failed; + } + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + final QuicServer quicServer = Http3TestServer.quicServerBuilder() + .sslContext(sslContext) + .build(); + http3TestServer = new Http3TestServer(quicServer); + http3TestServer.addHandler("/http3/post", new PostHandler().toHttp2Handler()); + http3TestServer.addHandler("/http3/forcePost", new ForcePostHandler().toHttp2Handler()); + http3TestServer.addHandler("/http3/hang", new PostHandlerCantContinue().toHttp2Handler()); + + h3postUri = new URI("https://" + http3TestServer.serverAuthority() + "/http3/post"); + h3forcePostUri = URI.create("https://" + http3TestServer.serverAuthority() + "/http3/forcePost"); + h3hangUri = URI.create("https://" + http3TestServer.serverAuthority() + "/http3/hang"); + out.printf("HTTP/3 server listening at: %s", http3TestServer.getAddress()); + + http3TestServer.start(); + } + + @AfterTest + public void teardown() throws IOException { + var error = TRACKER.check(500); + if (error != null) throw error; + http3TestServer.stop(); + } + + static class PostHandler implements HttpTestHandler { + + @java.lang.Override + public void handle(HttpTestExchange exchange) throws IOException { + System.out.printf("Server version %s and exchange version %s", exchange.getServerVersion(), exchange.getExchangeVersion()); + + if(exchange.getExchangeVersion().equals(HTTP_3)){ + // send 100 header + byte[] ContinueResponseBytes = CONTINUE_100.getBytes(); + err.println("Server send 100 (length="+ContinueResponseBytes.length+")"); + exchange.sendResponseHeaders(100, ContinueResponseBytes.length); + } + + // Read body from client and acknowledge with 200 + try (InputStream is = exchange.getRequestBody()) { + err.println("Server reading body"); + var bytes = is.readAllBytes(); + String responseBody = new String(bytes); + assert responseBody.equals(BODY); + byte[] responseBodyBytes = RESPONSE_BODY.getBytes(); + err.println("Server send 200 (length="+responseBodyBytes.length+")"); + exchange.sendResponseHeaders(200, responseBodyBytes.length); + exchange.getResponseBody().write(responseBodyBytes); + } + } + } + + static class ForcePostHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange exchange) throws IOException { + try (InputStream is = exchange.getRequestBody()) { + err.println("Server reading body inside the force Post"); + is.readAllBytes(); + err.println("Server send 200 (length=0) in the force post"); + exchange.sendResponseHeaders(200, 0); + } + } + } + + static class PostHandlerCantContinue implements HttpTestHandler { + @java.lang.Override + public void handle(HttpTestExchange exchange) throws IOException { + //Send 417 Headers, tell client to not send body + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = EXPECTATION_FAILED_417.getBytes(); + err.println("Server send 417 (length="+bytes.length+")"); + exchange.sendResponseHeaders(417, bytes.length); + err.println("Server sending Response Body"); + os.write(bytes); + } + } + } + + private void verifyRequest(String path, int expectedStatusCode, HttpResponse resp, boolean exceptionally, Throwable testThrowable) { + if (!exceptionally) { + err.printf("Response code %s received for path %s %n", resp.statusCode(), path); + } + if (exceptionally && testThrowable != null) { + err.println("Finished exceptionally Test throwable: " + testThrowable); + assertEquals(IOException.class, testThrowable.getClass()); + } else if (exceptionally) { + throw new TestException("Expected case to finish with an IOException but testException is null"); + } else if (resp != null) { + assertEquals(resp.statusCode(), expectedStatusCode); + err.println("Request completed successfully for path " + path); + err.println("Response Headers: " + resp.headers()); + err.println("Response Status Code: " + resp.statusCode()); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java b/test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java new file mode 100644 index 00000000000..be1b8304bf1 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2015, 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. + */ + +/* + * @test + * @run testng/othervm + * -Djdk.internal.httpclient.debug=out + * PeerUniStreamDispatcherTest + * @summary Unit test for the PeerUniStreamDispatcher + */ + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; + +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.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreams; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +public class PeerUniStreamDispatcherTest { + + final Logger debug = Utils.getDebugLogger(() -> "PeerUniStreamDispatcherStub"); + + enum DISPATCHED_STREAM { + CONTROL, ENCODER, DECODER, PUSH, RESERVED, UNKNOWN + } + + sealed interface DispatchedStream { + record StandardStream(DISPATCHED_STREAM type, String description, QuicReceiverStream stream) + implements DispatchedStream { } + record PushStream(DISPATCHED_STREAM type, String description, QuicReceiverStream stream, long pushId) + implements DispatchedStream { } + record UnknownStream(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) + implements DispatchedStream { } + record ReservedStream(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) + implements DispatchedStream { } + + static DispatchedStream of(DISPATCHED_STREAM type, String description, QuicReceiverStream stream) { + return new StandardStream(type, description, stream); + } + static DispatchedStream of(DISPATCHED_STREAM type, String description, QuicReceiverStream stream, long pushId) { + return new PushStream(type, description, stream, pushId); + } + static DispatchedStream reserved(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) { + return new ReservedStream(type, code, stream); + } + static DispatchedStream unknown(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) { + return new UnknownStream(type, code, stream); + } + } + + class PeerUniStreamDispatcherStub extends PeerUniStreamDispatcher { + + final List dispatched = new CopyOnWriteArrayList<>(); + + PeerUniStreamDispatcherStub(QuicReceiverStream stream) { + super(stream); + } + + private void dispatched(DISPATCHED_STREAM type, String description, QuicReceiverStream stream) { + dispatched.add(DispatchedStream.of(type, description, stream)); + } + + private void dispatched(DISPATCHED_STREAM type, String description, QuicReceiverStream stream, long pushId) { + dispatched.add(DispatchedStream.of(type, description, stream, pushId)); + } + + private void dispatched(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) { + dispatched.add(switch (type) { + case UNKNOWN -> DispatchedStream.unknown(type, code, stream); + case RESERVED -> DispatchedStream.reserved(type, code, stream); + default -> throw new IllegalArgumentException(String.valueOf(type)); + }); + } + + @Override + protected Logger debug() { + return debug; + } + + @Override + protected void onControlStreamCreated(String description, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.CONTROL, description, stream); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.ENCODER, description, stream); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.DECODER, description, stream); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + dispatched(DISPATCHED_STREAM.PUSH, description, stream, pushId); + } + + @Override + protected void onReservedStreamType(long code, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.RESERVED, code, stream); + super.onReservedStreamType(code, stream); + } + + @Override + protected void onUnknownStreamType(long code, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.UNKNOWN, code, stream); + super.onUnknownStreamType(code, stream); + } + + @Override + public void start() { + super.start(); + } + } + + static class QuicReceiverStreamStub implements QuicReceiverStream { + + class QuicStreamReaderStub extends QuicStreamReader { + + volatile boolean connected, started; + QuicStreamReaderStub(SequentialScheduler scheduler) { + super(scheduler); + } + + @Override + public ReceivingStreamState receivingState() { + return QuicReceiverStreamStub.this.receivingState(); + } + + @Override + public ByteBuffer poll() throws IOException { + return buffers.poll(); + } + + @Override + public ByteBuffer peek() throws IOException { + return buffers.peek(); + } + + @Override + public QuicReceiverStream stream() { + return QuicReceiverStreamStub.this; + } + + @Override + public boolean connected() { + return connected; + } + + @Override + public boolean started() { + return started; + } + + @Override + public void start() { + started = true; + if (!buffers.isEmpty()) scheduler.runOrSchedule(); + } + } + + volatile QuicStreamReaderStub reader; + volatile SequentialScheduler scheduler; + volatile long errorCode; + final long streamId; + ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); + + QuicReceiverStreamStub(long streamId) { + this.streamId = streamId; + } + + + @Override + public ReceivingStreamState receivingState() { + return ReceivingStreamState.RECV; + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + this.scheduler = scheduler; + var reader = this.reader + = new QuicStreamReaderStub(scheduler); + reader.connected = true; + return reader; + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + this.scheduler = null; + this.reader = null; + ((QuicStreamReaderStub) reader).connected = false; + } + + @Override + public void requestStopSending(long errorCode) { + this.errorCode = errorCode; + } + + @Override + public long dataReceived() { + return 0; + } + + @Override + public long maxStreamData() { + return 0; + } + + @Override + public long rcvErrorCode() { + return errorCode; + } + + @Override + public long streamId() { + return streamId; + } + + @Override + public StreamMode mode() { + return StreamMode.READ_ONLY; + } + + @Override + public boolean isClientInitiated() { + return QuicStreams.isClientInitiated(streamId); + } + + @Override + public boolean isServerInitiated() { + return QuicStreams.isServerInitiated(streamId); + } + + @Override + public boolean isBidirectional() { + return QuicStreams.isBidirectional(streamId); + } + + @Override + public boolean isLocalInitiated() { + return isClientInitiated(); + } + + @Override + public boolean isRemoteInitiated() { + return !isClientInitiated(); + } + + @Override + public int type() { + return QuicStreams.streamType(streamId); + } + + @Override + public StreamState state() { + return ReceivingStreamState.RECV; + } + + } + + private void simpleStreamType(DISPATCHED_STREAM type, long code) { + System.out.println("Testing " + type + " with " + code); + QuicReceiverStreamStub stream = new QuicReceiverStreamStub(QuicStreams.UNI_MASK + QuicStreams.SRV_MASK); + PeerUniStreamDispatcherStub dispatcher = new PeerUniStreamDispatcherStub(stream); + QuicStreamReader reader = stream.reader; + SequentialScheduler scheduler = stream.scheduler; + assertTrue(reader.connected()); + int size = VariableLengthEncoder.getEncodedSize(code); + ByteBuffer buffer = ByteBuffer.allocate(size); + assertEquals(buffer.remaining(), size); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + stream.buffers.add(buffer); + scheduler.runOrSchedule(); + dispatcher.start(); + if (type == DISPATCHED_STREAM.PUSH) { + // we want to encode the pushId in multiple buffers, but call + // the scheduler only once to check that the dispatcher + // will loop correctly. + size = VariableLengthEncoder.getEncodedSize(1L << 62 - 5); + ByteBuffer buffer2 = ByteBuffer.allocate(size); + assertEquals(buffer2.remaining(), size); + VariableLengthEncoder.encode(buffer2, 1L << 62 - 5); + buffer2.flip(); + stream.buffers.add(ByteBuffer.wrap(new byte[] {buffer2.get()})); + scheduler.runOrSchedule(); // call runOrSchedule after supplying the first byte. + assertTrue(reader.connected()); + assert buffer2.remaining() > 1; // should always be true + while (buffer2.hasRemaining()) { + stream.buffers.add(ByteBuffer.wrap(new byte[] {buffer2.get()})); + } + } + scheduler.runOrSchedule(); + assertFalse(reader.connected()); + assertFalse(dispatcher.dispatched.isEmpty()); + assertTrue(stream.buffers.isEmpty()); + assertEquals(dispatcher.dispatched.size(), 1); + var dispatched = dispatcher.dispatched.get(0); + checkDispatched(type, code, stream, dispatched); + } + + private void checkDispatched(DISPATCHED_STREAM type, + long code, + QuicReceiverStream stream, + DispatchedStream dispatched) { + var streamClass = switch (type) { + case CONTROL, ENCODER, DECODER -> DispatchedStream.StandardStream.class; + case PUSH -> DispatchedStream.PushStream.class; + case RESERVED -> DispatchedStream.ReservedStream.class; + case UNKNOWN -> DispatchedStream.UnknownStream.class; + }; + assertEquals(dispatched.getClass(), streamClass, + "unexpected dispatched class " + dispatched + " for " + type); + if (dispatched instanceof DispatchedStream.StandardStream st) { + System.out.println("Got expected stream: " + st); + assertEquals(st.type(), type); + assertEquals(st.stream, stream); + } else if (dispatched instanceof DispatchedStream.ReservedStream res) { + System.out.println("Got expected stream: " + res); + assertEquals(res.type(), type); + assertEquals(res.stream, stream); + assertEquals(res.code(), code); + assertTrue(Http3Streams.isReserved(res.code())); + } else if (dispatched instanceof DispatchedStream.UnknownStream unk) { + System.out.println("Got expected stream: " + unk); + assertEquals(unk.type(), type); + assertEquals(unk.stream, stream); + assertEquals(unk.code(), code); + assertFalse(Http3Streams.isReserved(unk.code())); + } else if (dispatched instanceof DispatchedStream.PushStream push) { + System.out.println("Got expected stream: " + push); + assertEquals(push.type(), type); + assertEquals(push.stream, stream); + assertEquals(push.pushId, 1L << 62 - 5); + assertEquals(push.type(), DISPATCHED_STREAM.PUSH); + } + + } + @Test + public void simpleControl() { + simpleStreamType(DISPATCHED_STREAM.CONTROL, Http3Streams.CONTROL_STREAM_CODE); + } + @Test + public void simpleDecoder() { + simpleStreamType(DISPATCHED_STREAM.DECODER, Http3Streams.QPACK_DECODER_STREAM_CODE); + } + @Test + public void simpleEncoder() { + simpleStreamType(DISPATCHED_STREAM.ENCODER, Http3Streams.QPACK_ENCODER_STREAM_CODE); + } + @Test + public void simplePush() { + simpleStreamType(DISPATCHED_STREAM.PUSH, Http3Streams.PUSH_STREAM_CODE); + } + @Test + public void simpleUknown() { + simpleStreamType(DISPATCHED_STREAM.UNKNOWN, VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + @Test + public void simpleReserved() { + simpleStreamType(DISPATCHED_STREAM.RESERVED, 31 * 256 + 2); + } + + @Test + public void multyBytes() { + DISPATCHED_STREAM type = DISPATCHED_STREAM.UNKNOWN; + long code = VariableLengthEncoder.MAX_ENCODED_INTEGER; + System.out.println("Testing multi byte " + type + " with " + code); + QuicReceiverStreamStub stream = new QuicReceiverStreamStub(QuicStreams.UNI_MASK + QuicStreams.SRV_MASK); + PeerUniStreamDispatcherStub dispatcher = new PeerUniStreamDispatcherStub(stream); + QuicStreamReader reader = stream.reader; + SequentialScheduler scheduler = stream.scheduler; + assertTrue(reader.connected()); + int size = VariableLengthEncoder.getEncodedSize(code); + assertEquals(size, 8); + ByteBuffer buffer = ByteBuffer.allocate(size); + assertEquals(buffer.remaining(), size); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + dispatcher.start(); + for (int i=0; i FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + final Set sharedClientHasH3 = ConcurrentHashMap.newKeySet(); + private volatile HttpClient sharedClient; + private boolean directQuicConnectionSupported; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + h3URI, + }; + } + + @DataProvider(name = "variants") + public Object[][] variants(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + String[] uris = uris(); + Object[][] result = new Object[uris.length * 2 * 2 * 2][]; + int i = 0; + for (var version : List.of(Optional.empty(), Optional.of(HTTP_3))) { + for (Version firstRequestVersion : List.of(HTTP_2, HTTP_3)) { + for (boolean sameClient : List.of(false, true)) { + for (String uri : uris()) { + result[i++] = new Object[]{uri, firstRequestVersion, sameClient, version}; + } + } + } + } + assert i == result.length; + return result; + } + + @DataProvider(name = "uris") + public Object[][] uris(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + BodyPublisher oflines(boolean streaming, String ...lines) { + if (streaming) { + return BodyPublishers.fromPublisher(BodyPublishers.concat( + Stream.of(lines) + .map(s -> s + '\n') + .map(BodyPublishers::ofString) + .toArray(BodyPublisher[]::new))); + } else { + return BodyPublishers.fromPublisher(BodyPublishers.concat( + Stream.of(lines) + .map(s -> s + '\n') + .map(BodyPublishers::ofString) + .toArray(BodyPublisher[]::new)), + Stream.of(lines).mapToLong(String::length) + .map((l) -> l+1) + .sum()); + } + } + + + @Test(dataProvider = "variants") + public void testAsync(String uri, Version firstRequestVersion, boolean sameClient, Optional version) throws Exception { + System.out.println("Request to " + uri +"/Async/*" + + ", firstRequestVersion=" + firstRequestVersion + + ", sameclient=" + sameClient + ",version=" + version); + + HttpClient client = newHttpClient(sameClient); + final URI headURI = URI.create(uri + "/Async/First/HEAD"); + final Builder headBuilder = HttpRequest.newBuilder(headURI) + .version(firstRequestVersion) + .HEAD(); + Http3DiscoveryMode config = null; + if (firstRequestVersion == HTTP_3 && !directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + config = ALT_SVC; + } + if (config != null) { + out.println("first request will use " + config); + headBuilder.setOption(H3_DISCOVERY, config); + config = null; + } + + HttpResponse response1 = client.send(headBuilder.build(), BodyHandlers.ofString()); + assertEquals(response1.statusCode(), 200, "Unexpected first response code"); + assertEquals(response1.body(), "", "Unexpected first response body"); + boolean expectH3 = sameClient && sharedClientHasH3.contains(headURI.getRawAuthority()); + if (firstRequestVersion == HTTP_3) { + if (expectH3) { + out.println("Expecting HEAD response over HTTP_3"); + assertEquals(response1.version(), HTTP_3, "Unexpected first response version"); + } + } else { + out.println("Expecting HEAD response over HTTP_2"); + assertEquals(response1.version(), HTTP_2, "Unexpected first response version"); + } + out.println("HEAD response version: " + response1.version()); + if (response1.version() == HTTP_2) { + if (sameClient) { + sharedClientHasH3.add(headURI.getRawAuthority()); + } + expectH3 = version.isEmpty() && client.version() == HTTP_3; + if (version.orElse(null) == HTTP_3 && !directQuicConnectionSupported) { + config = ALT_SVC; + expectH3 = true; + } + // we can expect H3 only if the (default) config is not ANY + if (expectH3) { + out.println("first response came over HTTP/2, so we should expect all responses over HTTP/3"); + } + } else if (response1.version() == HTTP_3) { + expectH3 = directQuicConnectionSupported && version.orElse(null) == HTTP_3; + if (expectH3) { + out.println("first response came over HTTP/3, direct connection supported: expect HTTP/3"); + } else if (firstRequestVersion == HTTP_3 && version.isEmpty() + && config == null && directQuicConnectionSupported) { + config = ANY; + expectH3 = true; + } + } + out.printf("request version: %s, directConnectionSupported: %s, first response: %s," + + " config: %s, expectH3: %s%n", + version, directQuicConnectionSupported, response1.version(), config, expectH3); + if (expectH3) { + out.println("All responses should now come through HTTP/3"); + } + + Builder builder = HttpRequest.newBuilder(); + version.ifPresent(builder::version); + if (config != null) { + builder.setOption(H3_DISCOVERY, config); + } + Map>> responses = new HashMap<>(); + boolean streaming = false; + int h3Count = 0; + for (int i = 0; i < ITERATION_COUNT; i++) { + streaming = !streaming; + HttpRequest request = builder.uri(URI.create(uri+"/Async/"+i)) + .POST(oflines(streaming, BODY.split("\n"))) + .build(); + System.out.println("Iteration: " + request.uri()); + responses.put(request.uri(), client.sendAsync(request, BodyHandlers.ofString())); + } + while (!responses.isEmpty()) { + CompletableFuture.anyOf(responses.values().toArray(CompletableFuture[]::new)).join(); + var done = responses.entrySet().stream() + .filter((e) -> e.getValue().isDone()).toList(); + for (var e : done) { + URI u = e.getKey(); + responses.remove(u); + out.println("Checking response: " + u); + var response = e.getValue().get(); + out.println("Response is: " + response + ", [version: " + response.version() + "]"); + assertEquals(response.statusCode(), 200,"status for " + u); + assertEquals(response.body(), BODY,"body for " + u); + if (expectH3) { + assertEquals(response.version(), HTTP_3, "version for " + u); + } + if (response.version() == HTTP_3) { + h3Count++; + } + } + } + if (client.version() == HTTP_3 || version.orElse(null) == HTTP_3) { + if (h3Count == 0) { + throw new AssertionError("No request used HTTP/3"); + } + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + System.out.println("test: DONE"); + } + + @Test(dataProvider = "uris") + public void testSync(String h3URI) throws Exception { + HttpClient client = makeNewClient(); + Builder builder = HttpRequest.newBuilder(URI.create(h3URI + "/Sync/1")) + .version(HTTP_3); + if (!directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + builder.setOption(H3_DISCOVERY, ALT_SVC); + } + + HttpRequest request = builder + .POST(oflines(true, BODY.split("\n"))) + .build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + out.println("Body #1:\n" + response.body().indent(4)); + assertEquals(response.statusCode(), 200, "first response status"); + if (directQuicConnectionSupported) { + // TODO unreliable assertion + //assertEquals(response.version(), HTTP_3, "Unexpected first response version"); + } else { + assertEquals(response.version(), HTTP_2, "Unexpected first response version"); + } + assertEquals(response.body(), BODY, "first response body"); + + request = builder.uri(URI.create(h3URI + "/Sync/2")) + .POST(oflines(true, BODY.split("\n"))) + .build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + out.println("Body #2:\n" + response.body().indent(4)); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_3, "second response version"); + assertEquals(response.body(), BODY, "second response body"); + + request = builder.uri(URI.create(h3URI + "/Sync/3")) + .POST(oflines(true, BODY.split("\n"))) + .build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #3: " + response); + out.println("Version #3: " + response.version()); + out.println("Body #3:\n" + response.body().indent(4)); + assertEquals(response.statusCode(), 200, "third response status"); + assertEquals(response.version(), HTTP_3, "third response version"); + assertEquals(response.body(), BODY, "third response body"); + + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + final Http2TestServer h2WithAltService = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnSamePort(); + h3TestServer = HttpTestServer.of(h2WithAltService); + h3TestServer.addHandler(new Handler(), "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/POST"; + + serverCount.addAndGet(1); + h3TestServer.start(); + directQuicConnectionSupported = h2WithAltService.supportsH3DirectConnection(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + h3TestServer.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + + boolean head = "HEAD".equals(t.getRequestMethod()); + if ((invocation++ % 2) == 1 && !head) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Server sending %d - %s length\n", 200, BODY.length()); + t.sendResponseHeaders(200, BODY.length()); + } + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + reader.lines().forEach((line) -> { + try { + if (head) return; + os.write(line.getBytes(StandardCharsets.UTF_8)); + os.write('\n'); + os.flush(); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + }); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/StopSendingTest.java b/test/jdk/java/net/httpclient/http3/StopSendingTest.java new file mode 100644 index 00000000000..2a029e176a2 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/StopSendingTest.java @@ -0,0 +1,215 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.ResponseSubscribers; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Exercises the HTTP3 client to send a STOP_SENDING frame + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors StopSendingTest + */ +public class StopSendingTest implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURIBase; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3Server.addHandler(new Handler(), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(h3Server.getAddress().getPort()).build().toString(); + + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + private static final class Handler implements HttpTestHandler { + private static final byte[] DUMMY_BODY = "foo bar hello world".getBytes(StandardCharsets.UTF_8); + private static volatile boolean stop; + private static final CountDownLatch stopped = new CountDownLatch(1); + + /** + * Keeps writing out response data (bytes) until asked to stop + */ + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + System.out.println("Handling request: " + exchange.getRequestURI()); + exchange.sendResponseHeaders(200, -1); + try (final OutputStream os = exchange.getResponseBody()) { + while (!stop) { + os.write(DUMMY_BODY); + os.flush(); + System.out.println("Wrote response data of size " + DUMMY_BODY.length); + try { + Thread.sleep(5); + } catch (InterruptedException e) { + // ignore + } + } + System.out.println("Stopped writing response"); + } catch (IOException io) { + System.out.println("Got expected exception: " + io); + } finally { + stopped.countDown(); + } + } + } + + /** + * Issues a HTTP3 request to a server handler which keeps sending data. When some amount of + * data is received on the client side, the request is cancelled by the test method. This + * internally is expected to trigger a STOP_SENDING frame from the HTTP client to the server. + */ + @Test + public void testStopSending() throws Exception { + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest req = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + // used to wait and trigger a request cancellation + final CountDownLatch cancellationTrigger = new CountDownLatch(1); + System.out.println("Issuing request to " + reqURI); + final CompletableFuture> futureResp = client.sendAsync(req, + BodyHandlers.fromSubscriber(new CustomBodySubscriber(cancellationTrigger))); + // wait for the subscriber to receive some amount of response data before we trigger + // the request cancellation + System.out.println("Awaiting some response data to arrive"); + cancellationTrigger.await(); + System.out.println("Cancelling request"); + // cancel the request which will internal trigger a STOP_SENDING frame from the HTTP + // client to the server + final boolean cancelled = futureResp.cancel(true); + System.out.println("Cancelled request: " + cancelled); + try { + // we expect a CancellationException for a cancelled request, + // but due to a bug (race condition) in the HttpClient's implementation + // of the Future instance, sometimes the Future.cancel(true) results + // in an ExecutionException which wraps the CancellationException. + // TODO: fix the actual race condition and then expect only CancellationException here + final Exception actualException = Assert.expectThrows(Exception.class, futureResp::get); + if (actualException instanceof CancellationException) { + // expected + System.out.println("Received the expected CancellationException"); + } else if (actualException instanceof ExecutionException + && actualException.getCause() instanceof CancellationException) { + System.out.println("Received CancellationException wrapped as ExecutionException"); + } else { + // unexpected + throw actualException; + } + } catch (Exception | Error e) { + Handler.stop = true; + System.err.println("Unexpected exception: " + e); + e.printStackTrace(); + throw e; + } finally { + // wait until the handler stops sending + Handler.stopped.await(10, TimeUnit.SECONDS); + } + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker,1000); + if (error != null) throw error; + } + + /** + * A {@link java.net.http.HttpResponse.BodySubscriber} which informs any interested parties + * whenever it receives any data in {@link #onNext(List)} + */ + private static final class CustomBodySubscriber extends ResponseSubscribers.ByteArraySubscriber { + // the latch used to inform any interested parties about data being received + private final CountDownLatch latch; + + private CustomBodySubscriber(final CountDownLatch latch) { + // a finisher which just returns the bytes back + super((bytes) -> bytes); + this.latch = latch; + } + + @Override + public void onNext(final List items) { + super.onNext(items); + long totalSize = 0; + for (final ByteBuffer bb : items) { + totalSize += bb.remaining(); + } + System.out.println("Subscriber got response data of size " + totalSize); + // inform interested party that we received some data + latch.countDown(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/StreamLimitTest.java b/test/jdk/java/net/httpclient/http3/StreamLimitTest.java new file mode 100644 index 00000000000..d8a920c8544 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/StreamLimitTest.java @@ -0,0 +1,266 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.SocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary verifies that when the Quic stream limit is reached + * then HTTP3 requests are retried on newer connection + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http3.Http3TestServer + * @run testng/othervm -Djdk.internal.httpclient.debug=true StreamLimitTest + */ +public class StreamLimitTest { + + private SSLContext sslContext; + private HttpTestServer server; + private QuicServer quicServer; + private URI requestURI; + private volatile QuicServerConnection latestServerConn; + + private final class Handler implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange exchange) throws IOException { + final String handledBy = latestServerConn.logTag(); + System.out.println(handledBy + " handling request " + exchange.getRequestURI()); + final byte[] respBody; + if (handledBy == null) { + respBody = new byte[0]; + } else { + respBody = handledBy.getBytes(StandardCharsets.UTF_8); + } + exchange.sendResponseHeaders(200, respBody.length == 0 ? -1 : respBody.length); + // write out the server's connection id as a response + if (respBody.length > 0) { + try (OutputStream os = exchange.getResponseBody()) { + os.write(respBody); + } + } + exchange.close(); + } + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + quicServer = Http3TestServer.quicServerBuilder().sslContext(sslContext).build(); + final Http3TestServer h3Server = new Http3TestServer(quicServer) { + @Override + public boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConn) { + final boolean accepted = super.acceptIncoming(source, quicConn); + if (accepted) { + // keep track of the latest server connection + latestServerConn = quicConn; + } + return accepted; + } + }; + server = HttpTestServer.of(h3Server); + server.addHandler(new Handler(), "/foo"); + server.start(); + System.out.println("Server started at " + server.serverAuthority()); + requestURI = new URI("https://" + server.serverAuthority() + "/foo"); + } + + @AfterClass + public void afterClass() throws Exception { + latestServerConn = null; + if (server != null) { + server.stop(); + } + } + + /** + * Configures different limits for max bidi stream creation by HTTP client and then verifies + * the expected behaviour by sending HTTP3 requests + */ + @Test + public void testBidiMaxStreamLimit() throws Exception { + final QuicTransportParameters transportParameters = new QuicTransportParameters(); + final int intialMaxStreamLimit = 3; + final AtomicInteger maxStreamLimit = new AtomicInteger(intialMaxStreamLimit); + transportParameters.setIntParameter(ParameterId.initial_max_streams_bidi, + maxStreamLimit.get()); + // set the limit so that any new server connections created with advertise this limit + // to the peer (client) + quicServer.setTransportParameters(transportParameters); + // also set a MAX_STREAMS limit computer for this server so that the created + // connections use this computer for deciding MAX_STREAMS limit + quicServer.setMaxStreamLimitComputer((ignore) -> maxStreamLimit.longValue()); + final HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + final HttpRequest req = HttpRequest.newBuilder().version(HTTP_3) + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, server.h3DiscoveryConfig()) + .build(); + String requestHandledBy = null; + System.out.println("Server has been configured with a limit for max" + + " bidi streams: " + intialMaxStreamLimit); + // issue N number of requests where N == the max bidi stream creation limit that is + // advertised to the peer. All these N requests are expected to be handled by the same + // server connection + for (int i = 1; i <= intialMaxStreamLimit; i++) { + System.out.println("Sending request " + i + " to " + requestURI); + final HttpResponse resp = client.send(req, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(resp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(resp.statusCode(), 200, "Unexpected response code"); + final String respBody = resp.body(); + System.out.println("Request " + i + " was handled by server connection: " + respBody); + if (i == 1) { + // first request; keep track the server connection id which responded + // to this request + requestHandledBy = respBody; + } else { + Assert.assertEquals(respBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } + } + // at this point the limit for bidi stream creation has reached on the client. + // now issue a request so that: + // - the HTTP client implementation notices that it has hit the limit + // - HTTP client sends a STREAMS_BLOCKED frame and waits for a while (timeout derived + // internally based on request timeout) for server to increase the limit. But this server + // connection will not send any MAX_STREAMS frame upon receipt of STREAMS_BLOCKED frame + // - client notices that server connection hasn't increased the stream limit, so internally + // retries the request, which should trigger a new server connection and thus this + // request should be handled by a different server connection than the last N requests + final HttpRequest reqWithTimeout = HttpRequest.newBuilder().version(HTTP_3) + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, server.h3DiscoveryConfig()) + .timeout(Duration.of(10, ChronoUnit.SECONDS)) + .build(); + for (int i = 1; i <= intialMaxStreamLimit; i++) { + System.out.println("Sending request " + i + " (configured with timeout) to " + + requestURI); + final HttpResponse resp = client.send(reqWithTimeout, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(resp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(resp.statusCode(), 200, "Unexpected response code"); + final String respBody = resp.body(); + System.out.println("Request " + i + " was handled by server connection: " + respBody); + if (i == 1) { + // first request after the limit was hit. + // verify that it was handled by a new connection and not the one that handled + // the previous N requests + Assert.assertNotEquals(respBody, requestHandledBy, "Request was expected to be" + + " handled by a new server connection, but wasn't"); + // keep track this new server connection id which responded to this request + requestHandledBy = respBody; + } else { + Assert.assertEquals(respBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } + } + // at this point the limit for bidi stream creation has reached on the client, for + // this new server connection. we now configure this current server connection to + // increment the limit to new higher limit + maxStreamLimit.set(intialMaxStreamLimit + 2); + System.out.println("Server connection " + latestServerConn + " has now been configured to" + + " increment the bidi stream creation limit to " + maxStreamLimit.get()); + // we now issue new requests, with timeout specified + // - the HTTP client implementation notices that it has hit the limit + // - HTTP client sends a STREAMS_BLOCKED frame and waits for a while (timeout derived + // internally based on request timeout) for server to increase the limit. This server + // connection, since it is configured to increment the stream limit, will send + // MAX_STREAMS frame upon receipt of STREAMS_BLOCKED frame + // - client receives the MAX_STREAMS frame (hopefully within the timeout) and + // notices that server connection has increased the stream limit, so opens a new bidi + // stream and lets the request move forward (on the same server connection) + final int numNewRequests = maxStreamLimit.get() - intialMaxStreamLimit; + for (int i = 1; i <= numNewRequests; i++) { + System.out.println("Sending request " + i + " (after stream limit has been increased)" + + " to " + requestURI); + final HttpResponse resp = client.send(reqWithTimeout, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(resp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(resp.statusCode(), 200, "Unexpected response code"); + final String respBody = resp.body(); + System.out.println("Request " + i + " was handled by server connection: " + respBody); + // all these requests should be handled by the same server connection which handled + // the previous requests + Assert.assertEquals(respBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } + // at this point the newer limit for bidi stream creation has reached on the client. + // we now issue a new request without any timeout configured on the request, so that the + // client internally just immediately retries and uses a different connection on noticing + // that the stream creation limit for the current server connection has been reached. + System.out.println("Server connection " + latestServerConn + " has now been configured to" + + " not increase max stream limit for bidi streams created by the client"); + final HttpRequest finalReq = HttpRequest.newBuilder() + .version(HTTP_3) + .setOption(H3_DISCOVERY, server.h3DiscoveryConfig()) + .GET().uri(requestURI) + .build(); + System.out.println("Sending request, without timeout, to " + requestURI); + final HttpResponse finalResp = client.send(finalReq, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(finalResp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(finalResp.statusCode(), 200, "Unexpected response code"); + final String finalRespBody = finalResp.body(); + System.out.println("Request was handled by server connection: " + finalRespBody); + // this request should have been handled by a new server connection + Assert.assertNotEquals(finalRespBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java new file mode 100644 index 00000000000..5b96d4c0b76 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java @@ -0,0 +1,266 @@ +/* + * 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. + * + * 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.httpclient.test.lib.common; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Objects; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import sun.security.x509.CertificateExtensions; +import sun.security.x509.CertificateSerialNumber; +import sun.security.x509.CertificateValidity; +import sun.security.x509.CertificateVersion; +import sun.security.x509.CertificateX509Key; +import sun.security.x509.DNSName; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.SubjectAlternativeNameExtension; +import sun.security.x509.X500Name; +import sun.security.x509.X509CertImpl; +import sun.security.x509.X509CertInfo; + +/** + * A utility for generating dynamic {@link java.security.KeyStore}s in tests. The keystores + * generated by this utility are dynamic in the sense that they are generated as necessary and + * don't require keys/certificates to be present on the filesystem. The generated keystores too + * aren't saved to the filesystem, by this utility. + */ +public class DynamicKeyStoreUtil { + + private static final String PKCS12_KEYSTORE_TYPE = "PKCS12"; + + /** + * The default alias that will be used in the KeyStore generated by + * {@link #generateKeyStore(String, String...)} + */ + public static final String DEFAULT_ALIAS = "foobar-key-alias"; + + private static final String DEFAULT_SUBJECT_OU = "foobar-org-unit"; + private static final String DEFAULT_SUBJECT_ON = "foobar-org-name"; + private static final String DEFAULT_SUBJECT_COUNTRY = "US"; + + + /** + * Generates a PKCS12 type {@link KeyStore} which has one + * {@link KeyStore#getKey(String, char[]) key entry} which corresponds to a newly generated + * {@link java.security.PrivateKey} and accompanied by a newly generated self-signed certificate, + * certifying the corresponding public key. + *

    + * The newly generated {@link X509Certificate} certificate will use the {@code certSubject} + * as the certificate's subject. If the {@code certSubjectAltNames} is non-null then + * the certificate will be created with a + * {@link sun.security.x509.SubjectAlternativeNameExtension subject alternative name extension} + * which will include the {@code certSubject} and each of the {@code certSubjectAltNames} as + * subject alternative names (represented as DNS names) + *

    + * The generated KeyStore won't be password protected + * + * @param certSubject The subject to be used for the newly generated certificate + * @param certSubjectAltNames Optional subject alternative names to be used in the generated + * certificate + * @return The newly generated KeyStore + * @throws NullPointerException If {@code certSubject} is null + */ + public static KeyStore generateKeyStore(final String certSubject, final String... certSubjectAltNames) + throws Exception { + Objects.requireNonNull(certSubject); + final SecureRandom secureRandom = new SecureRandom(); + final KeyPair keyPair = generateRSAKeyPair(secureRandom); + final X509Certificate cert = generateCert(keyPair, secureRandom, certSubject, certSubjectAltNames); + final KeyStore.Builder keystoreBuilder = KeyStore.Builder.newInstance(PKCS12_KEYSTORE_TYPE, + null, new KeyStore.PasswordProtection(null)); + final KeyStore keyStore = keystoreBuilder.getKeyStore(); + // write a private key (with cert chain for the public key) + final char[] keyPassword = null; + keyStore.setKeyEntry(DEFAULT_ALIAS, keyPair.getPrivate(), keyPassword, new Certificate[]{cert}); + return keyStore; + } + + /** + * Generates a PKCS12 type {@link KeyStore} which has one + * {@link KeyStore#getKey(String, char[]) key entry} which corresponds to the {@code privateKey} + * and accompanied by the {@code certChain} certifying the corresponding public key. + *

    + * The generated KeyStore won't be password protected + * + * @param privateKey The private key to include in the keystore + * @param certChain The certificate chain + * @return The newly generated KeyStore + * @throws NullPointerException If {@code privateKey} or {@code certChain} is null + */ + public static KeyStore generateKeyStore(final PrivateKey privateKey, final Certificate[] certChain) + throws Exception { + Objects.requireNonNull(privateKey); + Objects.requireNonNull(certChain); + final KeyStore.Builder keystoreBuilder = KeyStore.Builder.newInstance(PKCS12_KEYSTORE_TYPE, + null, new KeyStore.PasswordProtection(null)); + final KeyStore keyStore = keystoreBuilder.getKeyStore(); + // write a private key (with cert chain for the public key) + final char[] keyPassword = null; + keyStore.setKeyEntry(DEFAULT_ALIAS, privateKey, keyPassword, certChain); + return keyStore; + } + + /** + * Creates and returns a new PKCS12 type keystore without any entries in the keystore + * + * @return The newly created keystore + * @throws Exception if any exception occurs during keystore generation + */ + public static KeyStore generateBlankKeyStore() throws Exception { + final KeyStore.Builder keystoreBuilder = KeyStore.Builder.newInstance(PKCS12_KEYSTORE_TYPE, + null, new KeyStore.PasswordProtection(null)); + return keystoreBuilder.getKeyStore(); + } + + /** + * Generates a {@link KeyPair} using the {@code secureRandom} + * + * @param secureRandom The SecureRandom + * @return The newly generated KeyPair + * @throws NoSuchAlgorithmException + */ + public static KeyPair generateRSAKeyPair(final SecureRandom secureRandom) + throws NoSuchAlgorithmException { + final String keyType = "RSA"; + final int defaultRSAKeySize = 3072; + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyType); + keyPairGenerator.initialize(defaultRSAKeySize, secureRandom); + //final String sigAlgName = "SHA384withRSA"; + final KeyPair pair = keyPairGenerator.generateKeyPair(); + final PublicKey publicKey = pair.getPublic(); + // publicKey's format must be X.509 otherwise + if (!"X.509".equalsIgnoreCase(publicKey.getFormat())) { + throw new IllegalArgumentException("Public key format " + publicKey.getFormat() + + ", isn't X.509"); + } + return pair; + } + + /** + * Generates a X509 certificate using the passed {@code keyPair} + * + * @param keyPair The KeyPair to use + * @param secureRandom The SecureRandom + * @param subjectName The subject to use in the certificate + * @param subjectAltNames Any subject alternate names to use in the certificate + * @return The newly generated certificate + * @throws Exception + */ + public static X509Certificate generateCert(final KeyPair keyPair, final SecureRandom secureRandom, + final String subjectName, final String... subjectAltNames) + throws Exception { + final X500Name subject = new X500Name(subjectName, DEFAULT_SUBJECT_OU, DEFAULT_SUBJECT_ON, + DEFAULT_SUBJECT_COUNTRY); + final X500Name issuer = subject; // self-signed cert + final GeneralNames generalNames; + if (subjectAltNames == null) { + generalNames = null; + } else { + generalNames = new GeneralNames(); + generalNames.add(new GeneralName(new DNSName(subjectName, true))); + for (final String san : subjectAltNames) { + if (san == null) { + continue; + } + final DNSName dnsName = new DNSName(san, true); + generalNames.add(new GeneralName(dnsName)); + } + } + return generateCert(keyPair, secureRandom, subject, issuer, generalNames); + } + + private static X509Certificate generateCert(final KeyPair keyPair, final SecureRandom secureRandom, + final X500Name subjectName, final X500Name issuerName, + final GeneralNames subjectAltNames) + throws Exception { + final X509CertInfo certInfo = new X509CertInfo(); + certInfo.setVersion(new CertificateVersion(CertificateVersion.V3)); + certInfo.setSerialNumber(CertificateSerialNumber.newRandom64bit(secureRandom)); + certInfo.setSubject(subjectName); + final PublicKey publicKey = keyPair.getPublic(); + certInfo.setKey(new CertificateX509Key(publicKey)); + certInfo.setValidity(certDuration()); + certInfo.setIssuer(issuerName); + if (subjectAltNames != null && !subjectAltNames.isEmpty()) { + final SubjectAlternativeNameExtension sanExtn = new SubjectAlternativeNameExtension( + true, subjectAltNames); + final CertificateExtensions certExtensions = new CertificateExtensions(); + certExtensions.setExtension(sanExtn.getId(), sanExtn); + certInfo.setExtensions(certExtensions); + } + final PrivateKey privateKey = keyPair.getPrivate(); + final String sigAlgName = "SHA384withRSA"; + return X509CertImpl.newSigned(certInfo, privateKey, sigAlgName); + } + + private static CertificateValidity certDuration() { + // create a cert with 1 day validity, starting from 1 minute back + final long currentTime = System.currentTimeMillis(); + final long oneMinuteInThePast = currentTime - (60 * 1000); + final long oneDayInTheFuture = currentTime + (24 * 60 * 60 * 1000); + final Date startDate = new Date(oneMinuteInThePast); + final Date expiryDate = new Date(oneDayInTheFuture); + return new CertificateValidity(startDate, expiryDate); + } + + /** + * Creates a {@link SSLContext} which is + * {@link SSLContext#init(KeyManager[], TrustManager[], SecureRandom) initialized} using the + * {@link javax.net.ssl.KeyManager}s and {@link javax.net.ssl.TrustManager}s available in the + * {@code keyStore}. The {@code keyStore} is expected to be password-less. + * + * @param keyStore The password-less keystore + * @return the SSLContext + * @throws Exception + */ + public static SSLContext createSSLContext(final KeyStore keyStore) throws Exception { + Objects.requireNonNull(keyStore); + final char[] password = null; + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX"); + kmf.init(keyStore, password); + + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(keyStore); + + final String protocol = "TLS"; + final SSLContext ctx = SSLContext.getInstance(protocol); + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return ctx; + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java index 93a93ad25d2..10633340a66 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java @@ -34,31 +34,46 @@ import com.sun.net.httpserver.HttpsServer; import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.qpack.Encoder; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.net.InetAddress; import java.io.ByteArrayInputStream; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; import java.net.http.HttpClient.Version; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; -import java.io.UncheckedIOException; -import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpHeaders; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Base64; +import java.util.HashSet; +import java.util.HexFormat; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,9 +81,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; 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.test.lib.Asserts.assertFileContentsEqual; /** * Defines an adaptation layers so that a test server handlers and filters @@ -93,24 +111,14 @@ import static java.net.http.HttpClient.Version.HTTP_2; */ public interface HttpServerAdapters { - static final boolean PRINTSTACK = - Boolean.getBoolean("jdk.internal.httpclient.debug"); - - static void uncheckedWrite(ByteArrayOutputStream baos, byte[] ba) { - try { - baos.write(ba); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + static final boolean PRINTSTACK = getPrintStack(); + private static boolean getPrintStack() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); } + static final HexFormat HEX_FORMAT = HexFormat.ofDelimiter(":").withUpperCase(); static void printBytes(PrintStream out, String prefix, byte[] bytes) { - int padding = 4 + 4 - (bytes.length % 4); - padding = padding > 4 ? padding - 4 : 4; - byte[] bigbytes = new byte[bytes.length + padding]; - System.arraycopy(bytes, 0, bigbytes, padding, bytes.length); - out.println(prefix + bytes.length + " " - + new BigInteger(bigbytes).toString(16)); + out.println(prefix + bytes.length + " " + HEX_FORMAT.formatHex(bytes)); } /** @@ -122,6 +130,7 @@ public interface HttpServerAdapters { public abstract Set>> entrySet(); public abstract List get(String name); public abstract boolean containsKey(String name); + public abstract OptionalLong firstValueAsLong(String name); @Override public boolean equals(Object o) { if (this == o) return true; @@ -166,6 +175,11 @@ public interface HttpServerAdapters { return headers.containsKey(name); } @Override + public OptionalLong firstValueAsLong(String name) { + return Optional.ofNullable(headers.getFirst(name)) + .stream().mapToLong(Long::parseLong).findFirst(); + } + @Override public String toString() { return String.valueOf(headers); } @@ -192,6 +206,10 @@ public interface HttpServerAdapters { return headers.firstValue(name).isPresent(); } @Override + public OptionalLong firstValueAsLong(String name) { + return headers.firstValueAsLong(name); + } + @Override public String toString() { return String.valueOf(headers); } @@ -244,18 +262,169 @@ public interface HttpServerAdapters { public abstract String getRequestMethod(); public abstract void close(); public abstract InetSocketAddress getRemoteAddress(); - public abstract String getConnectionKey(); public abstract InetSocketAddress getLocalAddress(); - public void serverPush(URI uri, HttpHeaders headers, byte[] body) { + public abstract String getConnectionKey(); + public abstract SSLSession getSSLSession(); + public void serverPush(URI uri, HttpHeaders reqHeaders, byte[] body) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream(body); - serverPush(uri, headers, bais); + serverPush(uri, reqHeaders, bais); } - public void serverPush(URI uri, HttpHeaders headers, InputStream body) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, byte[] body) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(body); + serverPush(uri, reqHeaders, rspHeaders, bais); + } + public void serverPush(URI uri, HttpHeaders reqHeaders, InputStream body) + throws IOException { + serverPush(uri, reqHeaders, HttpHeaders.of(Map.of(), (n,v) -> true), body); + } + + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) + throws IOException { throw new UnsupportedOperationException("serverPush with " + getExchangeVersion()); } + + public void requestStopSending(long errorCode) { + throw new UnsupportedOperationException("sendHttp3ConnectionClose with " + getExchangeVersion()); + } + + /** + * Sends an HTTP/3 PUSH_PROMISE frame, for the given {@code uri}, + * with the given request {@code reqHeaders}, and opens a push promise + * stream to send the given response {@code rspHeaders} and {@code body}. + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param uri the push promise URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise request headers + * @param body the push response body + * + * @return the pushId used to push the promise + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public long http3ServerPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) + throws IOException { + throw new UnsupportedOperationException("serverPushWithId with " + getExchangeVersion()); + } + /** + * Sends an HTTP/3 PUSH_PROMISE frame, for the given {@code uri}, + * with the given request {@code headers}, and with the given + * {@code pushId}. This method only sends the PUSH_PROMISE frame + * and doesn't open any push stream. + * + * @apiNote + * This method can be used to send a PUSH_PROMISE whose body has + * already been promised by calling {@link + * #http3ServerPush(URI, HttpHeaders, HttpHeaders, InputStream)}. In that case + * the {@code pushId} returned by {@link + * #http3ServerPush(URI, HttpHeaders, HttpHeaders, InputStream)} should be passed + * as parameter. Otherwise, if {@code pushId=-1} is passed as parameter, + * a new pushId will be allocated. The push response headers and body + * can be later sent using {@link + * #sendHttp3PushResponse(long, URI, HttpHeaders, HttpHeaders, InputStream)}. + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the pushId to use, or {@code -1} if a new + * pushId should be allocated. + * @param uri the push promise URI + * @param headers the push promise request headers + * @return the given pushId, if positive, otherwise the new allocated pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public long sendHttp3PushPromiseFrame(long pushId, URI uri, HttpHeaders headers) + throws IOException { + throw new UnsupportedOperationException("serverPushId with " + getExchangeVersion()); + } + /** + * Opens an HTTP/3 PUSH_STREAM to send a push promise response headers + * and body. + * + * @apiNote + * No check is performed on the provided pushId + * + * @param pushId a positive pushId obtained from {@link + * #sendHttp3PushPromiseFrame(long, URI, HttpHeaders)} + * @param uri the push request URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise response headers + * @param body the push response body + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + */ + public void sendHttp3PushResponse(long pushId, URI uri, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream body) + throws IOException { + throw new UnsupportedOperationException("serverPushWithId with " + getExchangeVersion()); + } + /** + * Sends an HTTP/3 CANCEL_PUSH frame to cancel a push that has been + * promised by either {@link #http3ServerPush(URI, HttpHeaders, HttpHeaders, InputStream)} + * or {@link #sendHttp3PushPromiseFrame(long, URI, HttpHeaders)}. + * + * This method doesn't cancel the push stream but just sends + * a CANCEL_PUSH frame. + * Note that if the push stream has already been opened this + * method may not have any effect. + * + * @apiNote + * No check is performed on the provided pushId + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the cancelled pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public void sendHttp3CancelPushFrame(long pushId) + throws IOException { + throw new UnsupportedOperationException("cancelPushId with " + getExchangeVersion()); + } + /** + * Waits until the given {@code pushId} is allowed by the HTTP/3 peer + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId a pushId + * + * @return the maximum pushId allowed (exclusive) + * + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public long waitForHttp3MaxPushId(long pushId) + throws InterruptedException { + throw new UnsupportedOperationException("waitForMaxPushId with " + getExchangeVersion()); + } public boolean serverPushAllowed() { return false; } + public Encoder qpackEncoder() { + throw new UnsupportedOperationException("qpackEncoder with " + getExchangeVersion()); + } + public CompletableFuture clientHttp3Settings() { + throw new UnsupportedOperationException("HTTP/3 client connection settings with " + + getExchangeVersion()); + } public static HttpTestExchange of(HttpExchange exchange) { return new Http1TestExchange(exchange); } @@ -265,6 +434,10 @@ public interface HttpServerAdapters { abstract void doFilter(Filter.Chain chain) throws IOException; + public void resetStream(long code) throws IOException { + throw new UnsupportedOperationException(String.valueOf(this.getServerVersion())); + } + // implementations... private static final class Http1TestExchange extends HttpTestExchange { private final HttpExchange exchange; @@ -303,7 +476,8 @@ public interface HttpServerAdapters { } @Override public void close() { exchange.close(); } - + @Override + public SSLSession getSSLSession() { return null; } @Override public InetSocketAddress getRemoteAddress() { return exchange.getRemoteAddress(); @@ -334,9 +508,9 @@ public interface HttpServerAdapters { this.exchange = exch; } @Override - public Version getServerVersion() { return HTTP_2; } + public Version getServerVersion() { return exchange.getServerVersion(); } @Override - public Version getExchangeVersion() { return HTTP_2; } + public Version getExchangeVersion() { return exchange.getServerVersion(); } @Override public InputStream getRequestBody() { return exchange.getRequestBody(); @@ -365,15 +539,61 @@ public interface HttpServerAdapters { return exchange.serverPushAllowed(); } @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream body) { - exchange.serverPush(uri, headers, body); + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) + throws IOException { + exchange.serverPush(uri, reqHeaders, rspHeaders, body); } + @Override + public void requestStopSending(long errorCode) { + exchange.requestStopSending(errorCode); + } + @Override + public void resetStream(long code) throws IOException { + exchange.resetStream(code); + } + + @Override + public long http3ServerPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) throws IOException { + return exchange.serverPushWithId(uri, reqHeaders, rspHeaders, body); + } + @Override + public long sendHttp3PushPromiseFrame(long pushId, URI uri, HttpHeaders reqHeaders) throws IOException { + return exchange.sendPushId(pushId, uri, reqHeaders); + } + @Override + public void sendHttp3CancelPushFrame(long pushId) throws IOException { + exchange.cancelPushId(pushId); + } + @Override + public void sendHttp3PushResponse(long pushId, + URI uri, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream body) throws IOException { + exchange.sendPushResponse(pushId, uri, reqHeaders, rspHeaders, body); + } + @Override + public long waitForHttp3MaxPushId(long pushId) throws InterruptedException { + return exchange.waitForMaxPushId(pushId); + } + @Override + public Encoder qpackEncoder() { + return exchange.qpackEncoder(); + } + + @Override + public CompletableFuture clientHttp3Settings() { + return exchange.clientHttp3Settings(); + } + + @Override void doFilter(Filter.Chain filter) throws IOException { throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server"); } @Override public void close() { exchange.close();} - + @Override + public SSLSession getSSLSession() { return exchange.getSSLSession();} @Override public InetSocketAddress getRemoteAddress() { return exchange.getRemoteAddress(); @@ -400,31 +620,6 @@ public interface HttpServerAdapters { } - - /** - * A version agnostic adapter class for HTTP Server Handlers. - */ - public interface HttpTestHandler { - void handle(HttpTestExchange t) throws IOException; - - default HttpHandler toHttpHandler() { - return (t) -> doHandle(HttpTestExchange.of(t)); - } - default Http2Handler toHttp2Handler() { - return (t) -> doHandle(HttpTestExchange.of(t)); - } - private void doHandle(HttpTestExchange t) throws IOException { - try { - handle(t); - } catch (Throwable x) { - System.out.println("WARNING: exception caught in HttpTestHandler::handle " + x); - System.err.println("WARNING: exception caught in HttpTestHandler::handle " + x); - if (PRINTSTACK && !expectException(t)) x.printStackTrace(System.out); - throw x; - } - } - } - /** * An {@link HttpTestHandler} that handles only HEAD and GET * requests. If another method is used 405 is returned with @@ -473,24 +668,192 @@ public interface HttpServerAdapters { } + /** + * A version agnostic adapter class for HTTP Server Handlers. + */ + public interface HttpTestHandler { + void handle(HttpTestExchange t) throws IOException; + + default HttpHandler toHttpHandler() { + return (t) -> doHandle(HttpTestExchange.of(t)); + } + default Http2Handler toHttp2Handler() { + return (t) -> doHandle(HttpTestExchange.of(t)); + } + + default void handleFailure(final HttpTestExchange exchange, Throwable failure) { + System.out.println("WARNING: exception caught in HttpTestHandler::handle " + failure); + System.err.println("WARNING: exception caught in HttpTestHandler::handle " + failure); + if (PRINTSTACK && !expectException(exchange)) { + failure.printStackTrace(System.out); + } + } + + private void doHandle(HttpTestExchange exchange) throws IOException { + try { + handle(exchange); + } catch (Throwable failure) { + handleFailure(exchange, failure); + throw failure; + } + } + } + + /** + * An echo handler that can be used to transfer large amount of data, and + * uses file on the file system to download the input. + */ + // TODO: it would be good if we could merge this with the Http2EchoHandler, + // from which this code was copied and adapted. + public static class HttpTestFileEchoHandler implements HttpTestHandler { + static final Path CWD = Paths.get("."); + + @Override + public void handle(HttpTestExchange t) throws IOException { + try { + System.err.printf("EchoHandler received request to %s from %s (version %s)%n", + t.getRequestURI(), t.getRemoteAddress(), t.getExchangeVersion()); + InputStream is = t.getRequestBody(); + var requestHeaders = t.getRequestHeaders(); + var responseHeaders = t.getResponseHeaders(); + responseHeaders.addHeader("X-Hello", "world"); + responseHeaders.addHeader("X-Bye", "universe"); + String fixedrequest = requestHeaders.firstValue("XFixed").orElse(null); + File outfile = Files.createTempFile(CWD, "foo", "bar").toFile(); + //System.err.println ("QQQ = " + outfile.toString()); + FileOutputStream fos = new FileOutputStream(outfile); + long count = is.transferTo(fos); + System.err.printf("EchoHandler read %s bytes\n", count); + is.close(); + fos.close(); + InputStream is1 = new FileInputStream(outfile); + OutputStream os = null; + + Path check = requestHeaders.firstValue("X-Compare") + .map((String s) -> Path.of(s)).orElse(null); + if (check != null) { + System.err.println("EchoHandler checking file match: " + check); + try { + assertFileContentsEqual(check, outfile.toPath()); + } catch (Throwable x) { + System.err.println("Files do not match: " + x); + t.sendResponseHeaders(500, -1); + outfile.delete(); + os.close(); + return; + } + } + + // return the number of bytes received (no echo) + String summary = requestHeaders.firstValue("XSummary").orElse(null); + if (fixedrequest != null && summary == null) { + t.sendResponseHeaders(200, count); + os = t.getResponseBody(); + if (!t.getRequestMethod().equals("HEAD")) { + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes%n", count1); + } else { + System.err.printf("EchoHandler HEAD received, no bytes sent%n"); + } + } else { + t.sendResponseHeaders(200, -1); + os = t.getResponseBody(); + if (!t.getRequestMethod().equals("HEAD")) { + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes\n", count1); + + if (summary != null) { + String s = Long.toString(count); + os.write(s.getBytes()); + } + } else { + System.err.printf("EchoHandler HEAD received, no bytes sent%n"); + } + } + outfile.delete(); + os.close(); + is1.close(); + } catch (Throwable e) { + e.printStackTrace(); + throw new IOException(e); + } + } + } + public static class HttpTestEchoHandler implements HttpTestHandler { + + private final boolean printBytes; + public HttpTestEchoHandler() { + this(true); + } + + public HttpTestEchoHandler(boolean printBytes) { + this.printBytes = printBytes; + } + @Override public void handle(HttpTestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); - printBytes(System.out,"Echo server got " - + t.getExchangeVersion() + " bytes: ", bytes); + if (printBytes) { + printBytes(System.out, "Echo server got " + + t.getExchangeVersion() + " bytes: ", bytes); + } if (t.getRequestHeaders().firstValue("Content-type").isPresent()) { t.getResponseHeaders().addHeader("Content-type", t.getRequestHeaders().firstValue("Content-type").get()); } t.sendResponseHeaders(200, bytes.length); - os.write(bytes); + if (!t.getRequestMethod().equals("HEAD")) { + os.write(bytes); + } } } } + public static class HttpTestRedirectHandler implements HttpTestHandler { + + final Supplier supplier; + + public HttpTestRedirectHandler(Supplier redirectSupplier) { + supplier = redirectSupplier; + } + + @Override + public void handle(HttpTestExchange t) throws IOException { + examineExchange(t); + try (InputStream is = t.getRequestBody()) { + is.readAllBytes(); + String location = supplier.get(); + System.err.printf("RedirectHandler request to %s from %s\n", + t.getRequestURI().toString(), t.getRemoteAddress().toString()); + System.err.println("Redirecting to: " + location); + var headersBuilder = t.getResponseHeaders(); + headersBuilder.addHeader("Location", location); + byte[] bb = getResponseBytes(); + t.sendResponseHeaders(redirectCode(), bb.length); + OutputStream os = t.getResponseBody(); + os.write(bb); + os.close(); + t.close(); + } + } + + protected byte[] getResponseBytes() { + return new byte[1024]; + } + + protected int redirectCode() { + return 301; + } + + // override in sub-class to examine the exchange, but don't + // alter transaction state by reading the request body etc. + protected void examineExchange(HttpTestExchange t) { + } + } + public static boolean expectException(HttpTestExchange e) { HttpTestRequestHeaders h = e.getRequestHeaders(); Optional expectException = h.firstValue("X-expect-exception"); @@ -780,6 +1143,41 @@ public interface HttpServerAdapters { public abstract HttpTestContext addHandler(HttpTestHandler handler, String root); public abstract InetSocketAddress getAddress(); public abstract Version getVersion(); + + /** + * {@return the HTTP3 test server which is acting as an alt-service for this server, + * if any} + */ + public Optional getH3AltService() { + return Optional.empty(); + } + + /** + * {@return true if any HTTP3 test server is acting as an alt-service for this server and the + * HTTP3 test server listens on the same host and port as this server. + * Returns false otherwise} + */ + public boolean supportsH3DirectConnection() { + return false; + } + + public Http3DiscoveryMode h3DiscoveryConfig() { + return null; + } + + @Override + public String toString() { + var conf = Optional.ofNullable(h3DiscoveryConfig()).orElse(getVersion()); + return "HttpTestServer(%s: %s)".formatted(conf, serverAuthority()); + } + + /** + * @param version the HTTP version + * @param more additional HTTP versions + * {@return true if the handlers registered with this server can be accessed (through + * request URIs) using all of the passed HTTP versions. Returns false otherwise} + */ + public abstract boolean canHandle(Version version, Version... more); public abstract void setRequestApprover(final Predicate approver); @Override @@ -799,18 +1197,26 @@ public interface HttpServerAdapters { return hostString + ":" + address.getPort(); } - public static HttpTestServer of(HttpServer server) { + public static HttpTestServer of(final HttpServer server) { + Objects.requireNonNull(server); return new Http1TestServer(server); } - public static HttpTestServer of(HttpServer server, ExecutorService executor) { + public static HttpTestServer of(final HttpServer server, ExecutorService executor) { + Objects.requireNonNull(server); return new Http1TestServer(server, executor); } - public static HttpTestServer of(Http2TestServer server) { + public static HttpTestServer of(final Http2TestServer server) { + Objects.requireNonNull(server); return new Http2TestServerImpl(server); } + public static HttpTestServer of(final Http3TestServer server) { + Objects.requireNonNull(server); + return new H3ServerAdapter(server); + } + /** * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. The server * will only be available on {@code http} protocol. {@code https} will not be supported @@ -841,7 +1247,7 @@ public interface HttpServerAdapters { public static HttpTestServer create(Version serverVersion, SSLContext sslContext) throws IOException { Objects.requireNonNull(serverVersion); - return create(serverVersion, sslContext, null); + return create(serverVersion, sslContext, null, null); } /** @@ -860,7 +1266,130 @@ public interface HttpServerAdapters { public static HttpTestServer create(Version serverVersion, SSLContext sslContext, ExecutorService executor) throws IOException { Objects.requireNonNull(serverVersion); + return create(serverVersion, sslContext, null, executor); + } + + /** + * Creates a {@link HttpTestServer} which supports HTTP_3 version. + * + * @param h3DiscoveryCfg Discovery config for HTTP_3 connection creation. Can be null + * @param sslContext SSLContext. Cannot be null + * @return The newly created server + * @throws IOException if any exception occurs during the server creation + */ + public static HttpTestServer create(Http3DiscoveryMode h3DiscoveryCfg, + SSLContext sslContext) + throws IOException { + Objects.requireNonNull(sslContext, "SSLContext"); + return create(h3DiscoveryCfg, sslContext, null); + } + + /** + * Creates a {@link HttpTestServer} which supports HTTP_3 version. + * + * @param h3DiscoveryCfg Discovery config for HTTP_3 connection creation. Can be null + * @param sslContext SSLContext. Cannot be null + * @param executor The executor to be used by the server. Can be null + * @return The newly created server + * @throws IOException if any exception occurs during the server creation + */ + public static HttpTestServer create(Http3DiscoveryMode h3DiscoveryCfg, + SSLContext sslContext, ExecutorService executor) + throws IOException { + Objects.requireNonNull(sslContext, "SSLContext"); + return create(HTTP_3, sslContext, h3DiscoveryCfg, executor); + } + + + /** + * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. If the + * {@code sslContext} is null, then only {@code http} protocol will be supported by the + * server. Else, the server will be configured with the {@code sslContext} and will support + * {@code https} protocol. + * + * If {@code serverVersion} is {@link Version#HTTP_3 HTTP_3}, then a {@code h3DiscoveryCfg} + * can be passed to decide how the HTTP_3 server will be created. The following table + * summarizes how {@code h3DiscoveryCfg} is used: + *
      + *
    • HTTP3_ONLY - A server which only supports HTTP_3 is created
    • + *
    • HTTP3_ALTSVC - A HTTP_2 server is created and a HTTP_3 server is created. + * The HTTP_2 server advertises the HTTP_3 server as an alternate service. When + * creating the HTTP_3 server, an ephemeral port is used and thus the alternate + * service will be advertised on a different port than the HTTP_2 server's port
    • + *
    • ANY - A HTTP_2 server is created and a HTTP_3 server is created. + * The HTTP_2 server advertises the HTTP_3 server as an alternate service. When + * creating the HTTP_3 server, the same port as that of the HTTP_2 server is used + * to bind the HTTP_3 server. If that bind attempt fails, then an ephemeral port + * is used to bind the HTTP_3 server
    • + *
    + * + * @param serverVersion The HTTP version of the server + * @param sslContext The SSLContext to use. Can be null + * @param h3DiscoveryCfg The Http3DiscoveryMode for HTTP_3 server. Can be null, + * in which case it defaults to {@code ALT_SVC} for HTTP_3 + * server + * @param executor The executor to be used by the server. Can be null + * @return The newly created server + * @throws IllegalArgumentException if {@code serverVersion} is not supported by this method + * @throws IllegalArgumentException if {@code h3DiscoveryCfg} is not null when + * {@code serverVersion} is not {@code HTTP_3} + * @throws IOException if any exception occurs during the server creation + */ + private static HttpTestServer create(final Version serverVersion, final SSLContext sslContext, + final Http3DiscoveryMode h3DiscoveryCfg, + final ExecutorService executor) throws IOException { + Objects.requireNonNull(serverVersion); + if (h3DiscoveryCfg != null && serverVersion != HTTP_3) { + // Http3DiscoveryMode is only supported when version of HTTP_3 + throw new IllegalArgumentException("Http3DiscoveryMode" + + " isn't allowed for " + serverVersion + " version"); + } switch (serverVersion) { + case HTTP_3 -> { + if (sslContext == null) { + throw new IllegalArgumentException("SSLContext cannot be null when" + + " constructing a HTTP_3 server"); + } + final Http3DiscoveryMode effectiveDiscoveryCfg = h3DiscoveryCfg == null + ? Http3DiscoveryMode.ALT_SVC + : h3DiscoveryCfg; + switch (effectiveDiscoveryCfg) { + case HTTP_3_URI_ONLY -> { + // create only a HTTP3 server + return HttpTestServer.of(new Http3TestServer(sslContext, executor)); + } + case ALT_SVC -> { + // create a HTTP2 server which advertises an HTTP3 alternate service. + // that alternate service will be using an ephemeral port for the server + final Http2TestServer h2WithAltService; + try { + h2WithAltService = new Http2TestServer( + "localhost", true, 0, executor, sslContext) + .enableH3AltServiceOnEphemeralPort(); + } catch (Exception e) { + throw new IOException(e); + } + return HttpTestServer.of(h2WithAltService); + } + case ANY -> { + // create a HTTP2 server which advertises an HTTP3 alternate service. + // that alternate service will first attempt to use the same port as the + // HTTP2 server and if binding to that port fails, then will attempt + // to use a ephemeral port. + final Http2TestServer h2WithAltService; + try { + h2WithAltService = new Http2TestServer( + "localhost", true, 0, executor, sslContext) + .enableH3AltServiceOnSamePort(); + } catch (Exception e) { + throw new IOException(e); + } + return HttpTestServer.of(h2WithAltService); + } + default -> throw new IllegalArgumentException("Unsupported" + + " Http3DiscoveryMode: " + effectiveDiscoveryCfg); + } + } case HTTP_2 -> { Http2TestServer underlying; try { @@ -874,7 +1403,7 @@ public interface HttpServerAdapters { } return HttpTestServer.of(underlying); } - case HTTP_1_1 -> { + case HTTP_1_1 -> { InetAddress loopback = InetAddress.getLoopbackAddress(); InetSocketAddress sa = new InetSocketAddress(loopback, 0); HttpServer underlying; @@ -933,8 +1462,22 @@ public interface HttpServerAdapters { return new InetSocketAddress(InetAddress.getLoopbackAddress(), impl.getAddress().getPort()); } + @Override public Version getVersion() { return HTTP_1_1; } + @Override + public boolean canHandle(final Version version, final Version... more) { + if (version != HTTP_1_1) { + return false; + } + for (var v : more) { + if (v != HTTP_1_1) { + return false; + } + } + return true; + } + @Override public void setRequestApprover(final Predicate approver) { throw new UnsupportedOperationException("not supported"); @@ -996,8 +1539,41 @@ public interface HttpServerAdapters { return new InetSocketAddress(InetAddress.getLoopbackAddress(), impl.getAddress().getPort()); } + + @Override + public Optional getH3AltService() { + return impl.getH3AltService(); + } + + @Override + public boolean supportsH3DirectConnection() { + return impl.supportsH3DirectConnection(); + } + + public Http3DiscoveryMode h3DiscoveryConfig() { + return supportsH3DirectConnection() + ? Http3DiscoveryMode.ANY + : Http3DiscoveryMode.ALT_SVC; + } + public Version getVersion() { return HTTP_2; } + @Override + public boolean canHandle(final Version version, final Version... more) { + final Set supported = new HashSet<>(); + supported.add(HTTP_2); + impl.getH3AltService().ifPresent((unused)-> supported.add(HTTP_3)); + if (!supported.contains(version)) { + return false; + } + for (var v : more) { + if (!supported.contains(v)) { + return false; + } + } + return true; + } + @Override public void setRequestApprover(final Predicate approver) { this.impl.setRequestApprover(approver); @@ -1036,6 +1612,119 @@ public interface HttpServerAdapters { } @Override public Version getVersion() { return HTTP_2; } } + + private static final class H3ServerAdapter extends HttpTestServer { + private final Http3TestServer underlyingH3Server; + + private H3ServerAdapter(final Http3TestServer server) { + this.underlyingH3Server = server; + } + + @Override + public void start() { + underlyingH3Server.start(); + } + + @Override + public void stop() { + underlyingH3Server.stop(); + } + + @Override + public HttpTestContext addHandler(final HttpTestHandler handler, final String path) { + Objects.requireNonNull(path); + Objects.requireNonNull(handler); + final H3RootCtx h3Ctx = new H3RootCtx(path, handler); + this.underlyingH3Server.addHandler(path, h3Ctx::doHandle); + return h3Ctx; + } + + @Override + public InetSocketAddress getAddress() { + return underlyingH3Server.getAddress(); + } + + @Override + public Version getVersion() { + return HTTP_3; + } + + @Override + public Http3DiscoveryMode h3DiscoveryConfig() { + return Http3DiscoveryMode.HTTP_3_URI_ONLY; + } + + @Override + public boolean canHandle(Version version, Version... more) { + if (version != HTTP_3) { + return false; + } + for (var v : more) { + if (v != HTTP_3) { + return false; + } + } + return true; + } + + @Override + public void setRequestApprover(final Predicate approver) { + underlyingH3Server.setRequestApprover(approver); + } + + } + + private static final class H3RootCtx extends HttpTestContext implements HttpTestHandler { + private final String path; + private final HttpTestHandler handler; + private final List filters = new CopyOnWriteArrayList<>(); + + private H3RootCtx(final String path, final HttpTestHandler handler) { + this.path = path; + this.handler = handler; + } + + @Override + public String getPath() { + return this.path; + } + + @Override + public void addFilter(final HttpTestFilter filter) { + Objects.requireNonNull(filter); + this.filters.add(filter); + } + + @Override + public Version getVersion() { + return HTTP_3; + } + + @Override + public void setAuthenticator(final Authenticator authenticator) { + if (authenticator instanceof BasicAuthenticator basicAuth) { + addFilter(new HttpBasicAuthFilter(basicAuth)); + } else { + throw new UnsupportedOperationException( + "Only BasicAuthenticator is supported on an H3 context"); + } + } + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + HttpChain.of(this.filters, this.handler).doFilter(exchange); + } + + private void doHandle(final Http2TestExchange exchange) throws IOException { + final HttpTestExchange adapted = HttpTestExchange.of(exchange); + try { + H3RootCtx.this.handle(adapted); + } catch (Throwable failure) { + handleFailure(adapted, failure); + throw failure; + } + } + } } public static void enableServerLogging() { @@ -1044,4 +1733,73 @@ public interface HttpServerAdapters { HttpTestServer.ServerLogging.enableLogging(); } + public default HttpClient.Builder newClientBuilderForH3() { + return createClientBuilderForH3(); + } + + /** + * {@return a client builder suitable for interacting with the specified + * version} + * The builder's {@linkplain HttpClient.Builder#version(Version) version}, + * {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy selector} + * and {@linkplain HttpClient.Builder#sslContext(SSLContext) SSL context} + * are not set. + * @apiNote This method sets the {@linkplain HttpClient.Builder#localAddress(InetAddress) + * bind address} to the {@linkplain InetAddress#getLoopbackAddress() loopback address} + * if version is HTTP/3, the OS is Mac, and the OS version is 10.X, in order to + * avoid conflicting with system allocated ephemeral UDP ports. + * @param version the highest version the client is assumed to interact with. + */ + public static HttpClient.Builder createClientBuilderFor(Version version) { + var builder = HttpClient.newBuilder(); + return switch (version) { + case HTTP_3 -> configureForH3(builder); + default -> builder; + }; + } + + /** + * {@return a client builder suitable for interacting with HTTP/3} + * The builder's {@linkplain HttpClient.Builder#version(Version) version}, + * {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy selector} + * and {@linkplain HttpClient.Builder#sslContext(SSLContext) SSL context} + * are not set. + * @apiNote This method sets the {@linkplain HttpClient.Builder#localAddress(InetAddress) + * bind address} to the {@linkplain InetAddress#getLoopbackAddress() loopback address} + * if version is HTTP/3, the OS is Mac, and the OS version is 10.X, in order to + * avoid conflicting with system allocated ephemeral UDP ports. + * @implSpec This is identical to calling {@link #createClientBuilderFor(Version) + * newClientBuilderFor(Version.HTTP_3)} or {@link #configureForH3(Builder) + * configureForH3(HttpClient.newBuilder())} + */ + public static HttpClient.Builder createClientBuilderForH3() { + return configureForH3(HttpClient.newBuilder()); + } + + /** + * Configure a builder to be suitable for a client that may send requests + * through HTTP/3. + * The builder's {@linkplain HttpClient.Builder#version(Version) version}, + * {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy selector} + * and {@linkplain HttpClient.Builder#sslContext(SSLContext) SSL context} + * are not set. + * @apiNote This method sets the {@linkplain HttpClient.Builder#localAddress(InetAddress) + * bind address} to the {@linkplain InetAddress#getLoopbackAddress() loopback address} + * if the OS is Mac, and the OS version is 10.X, in order to + * avoid conflicting with system allocated ephemeral UDP ports. + * @return a client builder suitable for interacting with HTTP/3 + */ + public static HttpClient.Builder configureForH3(HttpClient.Builder builder) { + if (TestUtil.sysPortsMayConflict()) { + return builder.localAddress(InetAddress.getLoopbackAddress()); + } + return builder; + } + + public static InetAddress clientLocalBindAddress() { + if (TestUtil.sysPortsMayConflict()) { + return InetAddress.getLoopbackAddress(); + } + return new InetSocketAddress(0).getAddress(); + } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java new file mode 100644 index 00000000000..e732f45efe4 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java @@ -0,0 +1,68 @@ +/* + * 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. + * + * 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.httpclient.test.lib.common; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utility which parses a request path and finds a best match registered handler + */ +public class RequestPathMatcherUtil { + + public record Resolved(String bestMatchedPath, T handler) { + } + + /** + * Matches the {@code path} against the registered {@code pathHandlers} and returns the best + * matched handler. + * + * @param path The request path + * @param pathHandlers The handlers for each of the registered paths + * @param + * @return The resolved result or an {@linkplain Optional#empty() empty Optional} if no + * handler could be found for the {@code path} + * @throws NullPointerException if {@code pathHandlers} is null + */ + public static Optional> findHandler(final String path, + final Map pathHandlers) { + Objects.requireNonNull(pathHandlers, "pathHandlers is null"); + final String fpath = (path == null || path.isEmpty()) ? "/" : path; + final AtomicReference bestMatch = new AtomicReference<>(""); + final AtomicReference result = new AtomicReference<>(); + pathHandlers.forEach((key, value) -> { + if (fpath.startsWith(key) && key.length() > bestMatch.get().length()) { + bestMatch.set(key); + result.set(value); + } + }); + final T handler = result.get(); + if (handler == null) { + System.err.println("No handler found for path: " + path); + return Optional.empty(); + } + return Optional.of(new Resolved(bestMatch.get(), handler)); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java index a471f3ce07f..0d525a4f2c8 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java @@ -57,11 +57,22 @@ public final class TestServerConfigurator extends HttpsConfigurator { @Override public void configure(final HttpsParameters params) { final SSLParameters sslParams = getSSLContext().getDefaultSSLParameters(); - final String hostname = serverAddr.getHostName(); - - final List sniMatchers = List.of(new ServerNameMatcher(hostname)); - sslParams.setSNIMatchers(sniMatchers); + addSNIMatcher(serverAddr, sslParams); // configure the server with these custom SSLParameters params.setSSLParameters(sslParams); } + + public static void addSNIMatcher(final InetAddress serverAddr, final SSLParameters sslParams) { + final String hostname; + if (serverAddr.isLoopbackAddress()) { + // when it's loopback address, don't rely on InetAddress.getHostName() to get us the + // hostname, since it has been observed on Windows setups that InetAddress.getHostName() + // can return an IP address (127.0.0.1) instead of the hostname for loopback address + hostname = "localhost"; + } else { + hostname = serverAddr.getHostName(); + } + final List sniMatchers = List.of(new ServerNameMatcher(hostname)); + sslParams.setSNIMatchers(sniMatchers); + } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java new file mode 100644 index 00000000000..37ce49cd901 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java @@ -0,0 +1,58 @@ +/* + * 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 + * 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.httpclient.test.lib.common; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Optional; + +import jdk.internal.util.OperatingSystem; + +public final class TestUtil { + + private TestUtil() {} + + public static boolean sysPortsMayConflict() { + if (OperatingSystem.isMacOS()) { + // syslogd udp_in module may be dynamically started and opens an udp4 port + // on the wildcard address. In addition, macOS will allow different processes + // to bind to the same port on the wildcard, if one uses udp4 and the other + // binds using udp46 (dual IPv4 IPv6 socket). + // Binding to the loopback (or a specific interface) instead of binding + // to the wildcard can prevent such conflicts. + return true; + } + return false; + } + + public static Optional chooseClientBindAddress() { + if (!TestUtil.sysPortsMayConflict()) { + return Optional.empty(); + } + final InetSocketAddress address = new InetSocketAddress( + InetAddress.getLoopbackAddress(), 0); + return Optional.of(address); + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java index c6eee5afabf..33cca60ff38 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java @@ -26,6 +26,7 @@ package jdk.httpclient.test.lib.http2; import java.io.*; import java.nio.ByteBuffer; import java.util.Objects; +import java.util.concurrent.Semaphore; import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.ResetFrame; @@ -39,7 +40,8 @@ public class BodyOutputStream extends OutputStream { final static byte[] EMPTY_BARRAY = new byte[0]; final int streamid; - int window; + // stream level send window, permits = bytes + final Semaphore window; volatile boolean closed; volatile BodyInputStream bis; volatile int resetErrorCode; @@ -48,7 +50,7 @@ public class BodyOutputStream extends OutputStream { final Queue outputQ; BodyOutputStream(int streamid, int initialWindow, Http2TestServerConnection conn) { - this.window = initialWindow; + this.window = new Semaphore(initialWindow); this.streamid = streamid; this.conn = conn; this.outputQ = conn.outputQ; @@ -57,9 +59,8 @@ public class BodyOutputStream extends OutputStream { // called from connection reader thread as all incoming window // updates are handled there. - synchronized void updateWindow(int update) { - window += update; - notifyAll(); + void updateWindow(int update) { + window.release(update); } void waitForWindow(int demand) throws InterruptedException { @@ -69,23 +70,8 @@ public class BodyOutputStream extends OutputStream { conn.obtainConnectionWindow(demand); } - public void waitForStreamWindow(int amount) throws InterruptedException { - int demand = amount; - try { - synchronized (this) { - while (amount > 0) { - int n = Math.min(amount, window); - amount -= n; - window -= n; - if (amount > 0) { - wait(); - } - } - } - } catch (Throwable t) { - window += (demand - amount); - throw t; - } + public void waitForStreamWindow(int demand) throws InterruptedException { + window.acquire(demand); } public void goodToGo() { @@ -176,7 +162,7 @@ public class BodyOutputStream extends OutputStream { sendEndStream(); if (bis!= null && bis.unconsumed()) { // Send a reset if there is still unconsumed data in the input stream - sendReset(EMPTY_BARRAY, 0, 0, ResetFrame.NO_ERROR); + sendReset(ResetFrame.NO_ERROR); } } catch (IOException ex) { ex.printStackTrace(); @@ -187,12 +173,18 @@ public class BodyOutputStream extends OutputStream { send(EMPTY_BARRAY, 0, 0, DataFrame.END_STREAM); } - public void sendReset(byte[] buf, int offset, int len, int flags) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(len); - buffer.put(buf, offset, len); - buffer.flip(); + public void sendReset(int resetErrorCode) throws IOException { assert streamid != 0; - ResetFrame rf = new ResetFrame(streamid, flags); + ResetFrame rf = new ResetFrame(streamid, resetErrorCode); outputQ.put(rf); } + + public void reset(int resetErrorCode) throws IOException { + if (closed) return; + synchronized (this) { + if (closed) return; + this.closed = true; + } + sendReset(resetErrorCode); + } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java index 24ac59d96dc..047b52b3699 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java @@ -24,11 +24,12 @@ package jdk.httpclient.test.lib.http2; import java.io.*; +import java.net.http.HttpHeaders; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersBuilder; public class EchoHandler implements Http2Handler { static final Path CWD = Paths.get("."); @@ -41,8 +42,8 @@ public class EchoHandler implements Http2Handler { try { System.err.println("EchoHandler received request to " + t.getRequestURI()); InputStream is = t.getRequestBody(); - HttpHeadersImpl map = t.getRequestHeaders(); - HttpHeadersImpl map1 = t.getResponseHeaders(); + HttpHeaders map = t.getRequestHeaders(); + HttpHeadersBuilder map1 = t.getResponseHeaders(); map1.addHeader("X-Hello", "world"); map1.addHeader("X-Bye", "universe"); String fixedrequest = map.firstValue("XFixed").orElse(null); diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java index 7b13724ac51..fd0b03ac691 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, 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 @@ -27,8 +27,11 @@ import java.net.http.HttpHeaders; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; + import jdk.internal.net.http.common.HttpHeadersBuilder; +import static jdk.test.lib.Asserts.assertFileContentsEqual; + public class Http2EchoHandler implements Http2Handler { static final Path CWD = Paths.get("."); @@ -49,27 +52,42 @@ public class Http2EchoHandler implements Http2Handler { File outfile = Files.createTempFile(CWD, "foo", "bar").toFile(); //System.err.println ("QQQ = " + outfile.toString()); FileOutputStream fos = new FileOutputStream(outfile); - int count = (int) is.transferTo(fos); - System.err.printf("EchoHandler read %d bytes\n", count); + long count = is.transferTo(fos); + System.err.printf("EchoHandler read %s bytes\n", count); is.close(); fos.close(); InputStream is1 = new FileInputStream(outfile); OutputStream os = null; + + Path check = map.firstValue("X-Compare").map((String s) -> Path.of(s)).orElse(null); + if (check != null) { + System.err.println("EchoHandler checking file match: " + check); + try { + assertFileContentsEqual(check, outfile.toPath()); + } catch (Throwable x) { + System.err.println("Files do not match: " + x); + t.sendResponseHeaders(500, -1); + outfile.delete(); + os.close(); + return; + } + } + // return the number of bytes received (no echo) String summary = map.firstValue("XSummary").orElse(null); if (fixedrequest != null && summary == null) { t.sendResponseHeaders(200, count); os = t.getResponseBody(); - int count1 = (int)is1.transferTo(os); - System.err.printf("EchoHandler wrote %d bytes\n", count1); + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes\n", count1); } else { t.sendResponseHeaders(200, 0); os = t.getResponseBody(); - int count1 = (int)is1.transferTo(os); - System.err.printf("EchoHandler wrote %d bytes\n", count1); + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes\n", count1); if (summary != null) { - String s = Integer.toString(count); + String s = Long.toString(count); os.write(s.getBytes()); } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java index 8871f0c2cd5..c1a5538b95d 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java @@ -37,6 +37,5 @@ public interface Http2Handler { * client and used to send the response * @throws NullPointerException if exchange is null */ - void handle (Http2TestExchange exchange) throws IOException; + void handle(Http2TestExchange exchange) throws IOException; } - diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java index 69e4344aff2..ab817d0c79d 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.function.Supplier; + import jdk.internal.net.http.common.HttpHeadersBuilder; public class Http2RedirectHandler implements Http2Handler { @@ -48,8 +49,8 @@ public class Http2RedirectHandler implements Http2Handler { System.err.println("Redirecting to: " + location); HttpHeadersBuilder headersBuilder = t.getResponseHeaders(); headersBuilder.addHeader("Location", location); - t.sendResponseHeaders(redirectCode(), 1024); - byte[] bb = new byte[1024]; + byte[] bb = getResponseBytes(); + t.sendResponseHeaders(redirectCode(), bb.length == 0 ? -1 : bb.length); OutputStream os = t.getResponseBody(); os.write(bb); os.close(); @@ -57,6 +58,10 @@ public class Http2RedirectHandler implements Http2Handler { } } + protected byte[] getResponseBytes() { + return new byte[1024]; + } + protected int redirectCode() { return 301; } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java index 828c939f53f..77c9e831fb0 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2024, 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 @@ -28,12 +28,18 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.InetSocketAddress; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiPredicate; import javax.net.ssl.SSLSession; + import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.quic.VariableLengthEncoder; import jdk.internal.net.http.frame.Http2Frame; public interface Http2TestExchange { @@ -72,19 +78,177 @@ public interface Http2TestExchange { boolean serverPushAllowed(); - void serverPush(URI uri, HttpHeaders headers, InputStream content); + default void serverPush(URI uri, HttpHeaders headers, InputStream content) + throws IOException { + serverPush(uri, headers, HttpHeaders.of(Map.of(), (n,v) -> true), content); + } + + void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException; + + // For HTTP/3 only: send push promise + push stream, returns pushId + // + + /** + * For HTTP/3 only: send push promise + push stream, returns pushId. + * The pushId can be promised again using {@link + * #sendPushId(long, URI, HttpHeaders)} + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param uri the push promise URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise response headers + * @param content the push response body + * + * @return the pushId used to push the promise + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default long serverPushWithId(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + throw new UnsupportedOperationException("serverPushWithId " + getExchangeVersion()); + } + + /** + * For HTTP/3 only: only sends a push promise frame. If a positive + * pushId is provided, uses the provided pushId and returns it. + * Otherwise, a new pushId will be allocated and returned. + * This allows to send an additional promise after {@linkplain + * #serverPushWithId(URI, HttpHeaders, HttpHeaders, InputStream) sending the first}, + * or to send one or several push promise frames before {@linkplain + * #sendPushResponse(long, URI, HttpHeaders, HttpHeaders, InputStream) sending + * the response}. + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the pushId to use, or {@code -1} if a new + * pushId should be allocated. + * @param uri the push promise URI + * @param headers the push promise request headers + * + * @return the given pushId, if positive, otherwise the new allocated pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default long sendPushId(long pushId, URI uri, HttpHeaders headers) throws IOException { + throw new UnsupportedOperationException("sendPushId with " + getExchangeVersion()); + } + + /** + * For HTTP/3 only: sends an HTTP/3 CANCEL_PUSH frame to cancel + * a push that has been promised by either {@link + * #serverPushWithId(URI, HttpHeaders, HttpHeaders, InputStream)} or {@link + * #sendPushId(long, URI, HttpHeaders)}. + * + * This method just sends a CANCEL_PUSH frame. + * Note that if the push stream has already been opened this + * sending a CANCEL_PUSH frame may have no effect. + * + * @apiNote + * No check is performed on the provided pushId + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the cancelled pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default void cancelPushId(long pushId) throws IOException { + throw new UnsupportedOperationException("cancelPush with " + getExchangeVersion()); + } + + /** + * For HTTP/3 only: opens an HTTP/3 PUSH_STREAM to send a + * push promise response headers and body. + * + * @apiNote + * No check is performed on the provided pushId + * + * @param pushId a positive pushId obtained from {@link + * #sendPushId(long, URI, HttpHeaders)} + * @param uri the push request URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise response headers + * @param content the push response body + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + */ + default void sendPushResponse(long pushId, URI uri, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream content) + throws IOException { + throw new UnsupportedOperationException("sendPushResponse with " + getExchangeVersion()); + } + + default void requestStopSending(long errorCode) { + throw new UnsupportedOperationException("sendStopSendingFrame with " + getExchangeVersion()); + } default void sendFrames(List frames) throws IOException { throw new UnsupportedOperationException("not implemented"); } /** - * Send a PING on this exchanges connection, and completes the returned CF + * For HTTP/3 only: waits until the given {@code pushId} is allowed by + * the HTTP/3 peer. + * + * @implSpec + * The default implementation of this method returns the larger + * possible variable length integer. + * + * @param pushId a pushId + * + * @return the upper bound pf the maximum pushId allowed (exclusive) + * + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default long waitForMaxPushId(long pushId) throws InterruptedException { + return VariableLengthEncoder.MAX_ENCODED_INTEGER; + } + + default Encoder qpackEncoder() { + throw new UnsupportedOperationException("QPack encoder not supported: " + getExchangeVersion()); + } + + default CompletableFuture clientHttp3Settings() { + throw new UnsupportedOperationException("HTTP/3 client connection settings not supported: " + getExchangeVersion()); + } + + /** + * Send a PING on this exchange connection, and completes the returned CF * with the number of milliseconds it took to get a valid response. * It may also complete exceptionally */ CompletableFuture sendPing(); + default void close(IOException closed) throws IOException { + close(); + } + + default Version getServerVersion() { return Version.HTTP_2; } + + default Version getExchangeVersion() { return Version.HTTP_2; } + + default void resetStream(long code) throws IOException { + throw new UnsupportedOperationException("resetStream with " + getExchangeVersion()); + } + /** * {@return the identification of the connection on which this exchange is being * processed} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java index 12324b3ba0b..8bc0fd2473c 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, 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 @@ -149,12 +149,12 @@ public class Http2TestExchangeImpl implements Http2TestExchange { long clen = responseLength > 0 ? responseLength : 0; rspheadersBuilder.setHeader("Content-length", Long.toString(clen)); } - - rspheadersBuilder.setHeader(":status", Integer.toString(rCode)); - HttpHeaders headers = rspheadersBuilder.build(); - + final HttpHeadersBuilder pseudoHeadersBuilder = new HttpHeadersBuilder(); + pseudoHeadersBuilder.setHeader(":status", Integer.toString(rCode)); + final HttpHeaders pseudoHeaders = pseudoHeadersBuilder.build(); + final HttpHeaders headers = rspheadersBuilder.build(); ResponseHeaders response - = new ResponseHeaders(headers, insertionPolicy); + = new ResponseHeaders(pseudoHeaders, headers, insertionPolicy); response.streamid(streamid); response.setFlag(HeaderFrame.END_HEADERS); @@ -184,6 +184,13 @@ public class Http2TestExchangeImpl implements Http2TestExchange { conn.sendFrames(frames); } + @Override + public void resetStream(long code) throws IOException { + // will close the os if not closed. + // reset will be sent only if the os is not closed. + os.sendReset((int) code); + } + @Override public InetSocketAddress getRemoteAddress() { return (InetSocketAddress) conn.socket.getRemoteSocketAddress(); @@ -210,18 +217,18 @@ public class Http2TestExchangeImpl implements Http2TestExchange { } @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream content) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) { HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder(); headersBuilder.setHeader(":method", "GET"); headersBuilder.setHeader(":scheme", uri.getScheme()); headersBuilder.setHeader(":authority", uri.getAuthority()); headersBuilder.setHeader(":path", uri.getPath()); - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) headersBuilder.addHeader(entry.getKey(), value); } HttpHeaders combinedHeaders = headersBuilder.build(); - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content); pp.setFlag(HeaderFrame.END_HEADERS); try { diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java index a4d696ee53c..543c4079c14 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java @@ -24,22 +24,39 @@ package jdk.httpclient.test.lib.http2; import java.io.IOException; -import java.net.*; -import java.util.*; +import java.net.BindException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import javax.net.ServerSocketFactory; +import javax.net.ssl.SNIMatcher; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocket; +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil; +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil.Resolved; +import jdk.httpclient.test.lib.common.ServerNameMatcher; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.internal.net.http.frame.ErrorFrame; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.quic.QuicVersion; +import jdk.httpclient.test.lib.quic.QuicServer; /** * Waits for incoming TCP connections from a client and establishes @@ -48,7 +65,10 @@ import jdk.internal.net.http.frame.ErrorFrame; * Http2Handler on additional threads. All threads * obtained from the supplied ExecutorService. */ -public class Http2TestServer implements AutoCloseable { +public final class Http2TestServer implements AutoCloseable { + + record AltSvcAddr(String host, int port, InetSocketAddress original) {} + static final AtomicLong IDS = new AtomicLong(); final long id = IDS.incrementAndGet(); final ServerSocket server; @@ -61,7 +81,12 @@ public class Http2TestServer implements AutoCloseable { final String serverName; final Set connections; final Properties properties; + volatile Http3TestServer h3Server; + volatile AltSvcAddr h3AltSvcAddr; final String name; + private final SNIMatcher sniMatcher; + volatile boolean altSvcAsRespHeader; + private final ReentrantLock serverLock = new ReentrantLock(); // request approver which takes the server connection key as the input private volatile Predicate newRequestApprover; @@ -179,7 +204,6 @@ public class Http2TestServer implements AutoCloseable { throws Exception { this.name = "TestServer(%d)".formatted(id); - this.serverName = serverName; this.supportsHTTP11 = supportsHTTP11; if (secure) { if (context != null) @@ -192,12 +216,166 @@ public class Http2TestServer implements AutoCloseable { server = initPlaintext(port, backlog); } this.secure = secure; + this.serverName = serverName; + this.sniMatcher = serverName == null + ? new ServerNameMatcher(localAddr.getHostName()) + : new ServerNameMatcher(this.serverName); this.exec = exec == null ? createExecutor(name) : exec; this.handlers = Collections.synchronizedMap(new HashMap<>()); this.properties = properties == null ? new Properties() : properties; this.connections = ConcurrentHashMap.newKeySet(); } + /** + * {@return the {@link SNIMatcher} configured for this server. Returns {@code null} + * if none is configured} + */ + public SNIMatcher getSniMatcher() { + return this.sniMatcher; + } + + /** + * Creates a H3 server which will attempt to use the same host/port as the one used + * by this current H2 server (except that the H3 server will use UDP). If that host/port + * isn't available for the H3 server then it uses an ephemeral port on loopback address. + * That H3 server then acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + */ + public Http2TestServer enableH3AltServiceOnSamePort() throws IOException { + this.enableH3AltService(false); + return this; + } + + /** + * Creates a H3 server that acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + *

    + * The H3 server will be created using an ephemeral port on loopback address. + *

    + */ + public Http2TestServer enableH3AltServiceOnEphemeralPort() throws IOException { + return enableH3AltService(true); + } + + /** + * Creates a H3 server that acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + *

    + * The H3 server will be created using an ephemeral port on loopback address. + *

    + * The server will switch to selected QUIC version using compatible or incompatible negotiation. + */ + public Http2TestServer enableH3AltServiceOnEphemeralPortWithVersion(QuicVersion version, boolean compatible) throws IOException { + return enableH3AltService(0, new QuicVersion[]{version}, compatible); + } + + public Http2TestServer enableH3AltServiceOnPort(int port) throws IOException { + return enableH3AltService(port, new QuicVersion[]{QuicVersion.QUIC_V1}); + } + + /** + * Creates a H3 server that acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + *

    + * If {@code useEphemeralAddr} is {@code true} then the H3 server will be created using an + * ephemeral port on loopback address. Otherwise, an attempt will be made by this current H2 + * server (except that the H3 server will use UDP). If that attempt fails then this method + * implementation will fallback to using an ephemeral port for creating the H3 server. + *

    + * @param useEphemeralAddr If true then the H3 server will be created using an ephemeral port + */ + Http2TestServer enableH3AltService(final boolean useEphemeralAddr) throws IOException { + return enableH3AltService(useEphemeralAddr ? 0 : getAddress().getPort(), new QuicVersion[]{QuicVersion.QUIC_V1}); + } + + Http2TestServer enableH3AltService(final int port, QuicVersion[] quicVersions) throws IOException { + return enableH3AltService(port, quicVersions, false); + } + + Http2TestServer enableH3AltService(final int port, QuicVersion[] quicVersions, boolean compatible) throws IOException { + if (this.h3Server != null) { + // already enabled + // TODO: throw exception instead? + return this; + } + if (!secure) { + throw new IllegalStateException("Cannot enable H3 alt service for a non-secure H2 server"); + } + serverLock.lock(); + try { + if (this.h3Server != null) { + return this; + } + QuicServer.Builder quicServerBuilder = Http3TestServer.quicServerBuilder(); + quicServerBuilder.sslContext(this.sslContext).serverId("h2-server-" + id) + .executor(this.exec) + .sniMatcher(this.sniMatcher) + .availableVersions(quicVersions) + .compatibleNegotiation(compatible) + .appErrorCodeToString(Http3Error::stringForCode) + .bindAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); + try { + this.h3Server = new Http3TestServer(quicServerBuilder.build(), this::getHandlerFor); + } catch (BindException be) { + if (port == 0) { + // this means that we already attempted to bind with an ephemeral port + // and it failed, so no need to attempt again. Just throw back the original + // exception + throw be; + } + // try with an ephemeral port + quicServerBuilder.bindAddress(new InetSocketAddress( + InetAddress.getLoopbackAddress(), 0)); + this.h3Server = new Http3TestServer(quicServerBuilder.build(), this::getHandlerFor); + } + // we keep track of the InetSocketAddress.getHostString() when the alt service address + // was created and keep using the same host string irrespective of whether the + // underlying/original InetSocketAddress' hostname resolution could potentially have + // changed the value returned by getHostString(). This allows us to use a consistent + // host in the alt-svc that we advertise. + this.h3AltSvcAddr = new AltSvcAddr(h3Server.getAddress().getHostString(), + h3Server.getAddress().getPort(), h3Server.getAddress()); + } finally { + serverLock.unlock(); + } + return this; + } + + public Optional getH3AltService() { + return Optional.ofNullable(h3Server); + } + + /** + * {@return true if this H2 server is configured with an H3 alternate service and that + * H3 alternate service listens on the same host and port as that of this H2 server (except + * that H3 uses UDP). Returns false otherwise} + */ + public boolean supportsH3DirectConnection() { + final AltSvcAddr h3Addr = this.h3AltSvcAddr; + if (h3Addr == null) { + return false; + } + final InetSocketAddress h2Addr = this.getAddress(); + return h2Addr.equals(h3Addr.original); + } + + /** + * Controls whether this H2 server sends a alt-svc response header or an AltSvc frame + * when H3 alternate service is enable on this server. The alt-svc header or the frame + * will be sent whenever this server responds next to an HTTP request. + * + * @param enable If {@code true} then the alt-svc response header is sent. Else AltSvc frame + * is sent. + * @return The current Http2TestServer + */ + public Http2TestServer advertiseAltSvcResponseHeader(final boolean enable) { + // TODO: this is only set as a flag currently. We need to implement the logic + // which sends the alt-svc response header. we currently send a alt-svc frame + // whenever a alt-svc is present. + this.altSvcAsRespHeader = enable; + return this; + } + /** * Adds the given handler for the given path */ @@ -218,24 +396,14 @@ public class Http2TestServer implements AutoCloseable { } Http2Handler getHandlerFor(String path) { - if (path == null || path.equals("")) - path = "/"; - - final String fpath = path; - AtomicReference bestMatch = new AtomicReference<>(""); - AtomicReference href = new AtomicReference<>(); - - handlers.forEach((key, value) -> { - if (fpath.startsWith(key) && key.length() > bestMatch.get().length()) { - bestMatch.set(key); - href.set(value); - } - }); - Http2Handler handler = href.get(); - if (handler == null) - throw new RuntimeException("No handler found for path " + path); - System.err.println(name + ": Using handler for: " + bestMatch.get()); - return handler; + final Optional> match = RequestPathMatcherUtil.findHandler(path, handlers); + if (match.isEmpty()) { + // no handler available for the path + return null; + } + final Resolved resolved = match.get(); + System.err.println(name + ": Using handler for: " + resolved.bestMatchedPath()); + return resolved.handler(); } final ServerSocket initPlaintext(int port, int backlog) throws Exception { @@ -245,7 +413,16 @@ public class Http2TestServer implements AutoCloseable { return ss; } - public synchronized void stop() { + public void stop() { + serverLock.lock(); + try { + implStop(); + } finally { + serverLock.unlock(); + } + } + + private void implStop() { // TODO: clean shutdown GoAway stopping = true; System.err.printf("%s: stopping %d connections\n", name, connections.size()); @@ -255,6 +432,12 @@ public class Http2TestServer implements AutoCloseable { try { server.close(); } catch (IOException e) {} + try { + var h3Server = this.h3Server; + if (h3Server != null) { + h3Server.close(); + } + } catch (IOException e) {} exec.shutdownNow(); } @@ -284,6 +467,16 @@ public class Http2TestServer implements AutoCloseable { return serverName; } + private void putConnection(InetSocketAddress addr, Http2TestServerConnection c) { + serverLock.lock(); + try { + if (!stopping) + connections.add(c); + } finally { + serverLock.unlock(); + } + } + public void setRequestApprover(final Predicate approver) { this.newRequestApprover = approver; } @@ -292,13 +485,13 @@ public class Http2TestServer implements AutoCloseable { return this.newRequestApprover; } - private synchronized void putConnection(InetSocketAddress addr, Http2TestServerConnection c) { - if (!stopping) - connections.add(c); - } - - private synchronized void removeConnection(InetSocketAddress addr, Http2TestServerConnection c) { - connections.remove(c); + private void removeConnection(InetSocketAddress addr, Http2TestServerConnection c) { + serverLock.lock(); + try { + connections.remove(c); + } finally { + serverLock.unlock(); + } } record AcceptedConnection(Http2TestServer server, @@ -350,6 +543,10 @@ public class Http2TestServer implements AutoCloseable { * Starts a thread which waits for incoming connections. */ public void start() { + var h3Server = this.h3Server; + if (h3Server != null) { + h3Server.start(); + } exec.submit(() -> { try { while (!stopping) { diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java index deec3ec2c24..20668d281c8 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java @@ -41,25 +41,19 @@ import jdk.internal.net.http.frame.SettingsFrame; import jdk.internal.net.http.frame.WindowUpdateFrame; import jdk.internal.net.http.hpack.Decoder; import jdk.internal.net.http.hpack.DecodingCallback; -import jdk.internal.net.http.hpack.Encoder; import sun.net.www.http.ChunkedInputStream; import sun.net.www.http.HttpClient; -import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIMatcher; -import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import javax.net.ssl.StandardConstants; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.net.InetAddress; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; @@ -74,20 +68,25 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Random; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiPredicate; import java.util.function.Consumer; + +import jdk.internal.net.http.frame.AltSvcFrame; + import java.util.function.Predicate; import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import static jdk.internal.net.http.frame.ErrorFrame.REFUSED_STREAM; import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_MAX_FRAME_SIZE; @@ -125,7 +124,6 @@ public class Http2TestServerConnection { private final AtomicInteger goAwayRequestStreamId = new AtomicInteger(-1); final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); - final static byte[] EMPTY_BARRAY = new byte[0]; final Random random; final static byte[] clientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(); @@ -175,7 +173,7 @@ public class Http2TestServerConnection { if (socket instanceof SSLSocket) { SSLSocket sslSocket = (SSLSocket)socket; - handshake(server.serverName(), sslSocket); + handshake(server.getSniMatcher(), sslSocket); if (!server.supportsHTTP11 && !"h2".equals(sslSocket.getApplicationProtocol())) { throw new IOException("Unexpected ALPN: [" + sslSocket.getApplicationProtocol() + "]"); } @@ -312,39 +310,12 @@ public class Http2TestServerConnection { outputQ.put(frame); } - private static boolean compareIPAddrs(InetAddress addr1, String host) { - try { - InetAddress addr2 = InetAddress.getByName(host); - return addr1.equals(addr2); - } catch (IOException e) { - throw new UncheckedIOException(e); + private static void handshake(final SNIMatcher sniMatcher, final SSLSocket sock) throws IOException { + if (sniMatcher != null) { + final SSLParameters params = sock.getSSLParameters(); + params.setSNIMatchers(List.of(sniMatcher)); + sock.setSSLParameters(params); } - } - - private static void handshake(String name, SSLSocket sock) throws IOException { - if (name == null) { - sock.startHandshake(); // blocks until handshake done - return; - } else if (name.equals("localhost")) { - name = "localhost"; - } - final String fname = name; - final InetAddress addr1 = InetAddress.getByName(name); - SSLParameters params = sock.getSSLParameters(); - SNIMatcher matcher = new SNIMatcher(StandardConstants.SNI_HOST_NAME) { - public boolean matches (SNIServerName n) { - String host = ((SNIHostName)n).getAsciiName(); - if (host.equals("localhost")) - host = "localhost"; - boolean cmp = host.equalsIgnoreCase(fname); - if (cmp) - return true; - return compareIPAddrs(addr1, host); - } - }; - List list = List.of(matcher); - params.setSNIMatchers(list); - sock.setSSLParameters(params); sock.startHandshake(); // blocks until handshake done } @@ -356,8 +327,9 @@ public class Http2TestServerConnection { if (stopping) return; stopping = true; - System.err.printf(server.name + ": Server connection to %s stopping. %d streams\n", - socket.getRemoteSocketAddress().toString(), streams.size()); + System.err.printf(server.name + ": Server connection to %s stopping (%s). %d streams\n", + socket.getRemoteSocketAddress().toString(), + (error == -1 ? "no error" : ("error="+error)), streams.size()); streams.forEach((i, q) -> { q.orderlyClose(); }); @@ -374,15 +346,20 @@ public class Http2TestServerConnection { private void readPreface() throws IOException { int len = clientPreface.length; byte[] bytes = new byte[len]; + System.err.println("reading preface"); int n = is.readNBytes(bytes, 0, len); - if (Arrays.compare(clientPreface, bytes) != 0) { - String msg = String.format("Invalid preface: read %s/%s bytes", n, len); - System.err.println(server.name + ": " + msg); - throw new IOException(msg +": \"" + - new String(bytes, 0, n, ISO_8859_1) - .replace("\r", "\\r") - .replace("\n", "\\n") - + "\""); + if (n >= 0) { + if (Arrays.compare(clientPreface, bytes) != 0) { + String msg = String.format("Invalid preface: read %s/%s bytes", n, len); + System.err.println(server.name + ": " + msg); + throw new IOException(msg +": \"" + + new String(bytes, 0, n, ISO_8859_1) + .replace("\r", "\\r") + .replace("\n", "\\n") + + "\""); + } + } else { + throw new IOException("EOF while reading preface"); } } @@ -686,7 +663,7 @@ public class Http2TestServerConnection { // all other streams created here @SuppressWarnings({"rawtypes","unchecked"}) - void createStream(HeaderFrame frame) throws IOException { + private boolean createStream(HeaderFrame frame, Http2TestServer.AltSvcAddr altSvcAddr) throws IOException { List frames = new LinkedList<>(); frames.add(frame); int streamid = frame.streamid(); @@ -725,7 +702,7 @@ public class Http2TestServerConnection { if (disallowedHeader.isPresent()) { throw new IOException("Unexpected HTTP2-Settings in headers:" + headers); } - + boolean altSvcSent = false; // skip processing the request if the server is configured to do so final String connKey = connectionKey(); final String path = headers.firstValue(":path").orElse(""); @@ -734,10 +711,21 @@ public class Http2TestServerConnection { + " and sending GOAWAY on server connection " + connKey + ", for request: " + path); sendGoAway(ErrorFrame.NO_ERROR); - return; + return altSvcSent; } + Queue q = new Queue(sentinel); streams.put(streamid, q); + + if (altSvcAddr != null) { + String originHost = headers.firstValue("host") + .or(() -> headers.firstValue(":authority")) + .orElse(null); + if (originHost != null) { + altSvcSent = sendAltSvc(originHost, altSvcAddr); + } + } + // keep track of the largest request id that we have processed int currentLargest = maxProcessedRequestStreamId.get(); while (streamid > currentLargest) { @@ -749,6 +737,7 @@ public class Http2TestServerConnection { exec.submit(() -> { handleRequest(headers, q, streamid, endStreamReceived); }); + return altSvcSent; } // runs in own thread. Handles request from start to finish. Incoming frames @@ -793,14 +782,19 @@ public class Http2TestServerConnection { headers, rspheadersBuilder, uri, bis, getSSLSession(), bos, this, pushAllowed); - // give to user - Http2Handler handler = server.getHandlerFor(uri.getPath()); - - // Need to pass the BodyInputStream reference to the BodyOutputStream, so it can determine if the stream - // must be reset due to the BodyInputStream not being consumed by the handler when invoked. - if (bis instanceof BodyInputStream bodyInputStream) bos.bis = bodyInputStream; - + final String reqPath = uri.getPath(); + // locate a handler for the request + final Http2Handler handler = server.getHandlerFor(reqPath); try { + // no handler available for the request path, respond with 404 + if (handler == null) { + respondForMissingHandler(exchange); + return; + } + // Need to pass the BodyInputStream reference to the BodyOutputStream, so it can determine if the stream + // must be reset due to the BodyInputStream not being consumed by the handler when invoked. + if (bis instanceof BodyInputStream bodyInputStream) bos.bis = bodyInputStream; + handler.handle(exchange); } catch (IOException closed) { if (bos.closed) { @@ -821,6 +815,16 @@ public class Http2TestServerConnection { close(-1); } } + private void respondForMissingHandler(final Http2TestExchange exchange) + throws IOException { + final byte[] responseBody = (this.getClass().getSimpleName() + + " - No handler available to handle request " + + exchange.getRequestURI()).getBytes(US_ASCII); + try (final OutputStream os = exchange.getResponseBody()) { + exchange.sendResponseHeaders(404, responseBody.length); + os.write(responseBody); + } + } public void sendFrames(List frames) throws IOException { synchronized (outputQ) { @@ -845,6 +849,7 @@ public class Http2TestServerConnection { @SuppressWarnings({"rawtypes","unchecked"}) void readLoop() { try { + boolean altSvcSent = false; while (!stopping) { Http2Frame frame = readFrameImpl(); if (frame == null) { @@ -885,7 +890,9 @@ public class Http2TestServerConnection { outputQ.put(rst); continue; } - createStream((HeadersFrame) frame); + final Http2TestServer.AltSvcAddr altSvcAddr = server.h3AltSvcAddr; + final boolean sendAltSvc = secure && !altSvcSent && altSvcAddr != null; + altSvcSent = createStream((HeadersFrame) frame, sendAltSvc ? altSvcAddr : null); } } else { if (q == null && !pushStreams.contains(stream)) { @@ -896,9 +903,12 @@ public class Http2TestServerConnection { } if (frame.type() == WindowUpdateFrame.TYPE) { WindowUpdateFrame wup = (WindowUpdateFrame) frame; - synchronized (updaters) { + updatersLock.lock(); + try { Consumer r = updaters.get(stream); r.accept(wup.getUpdate()); + } finally { + updatersLock.unlock(); } } else if (frame.type() == ResetFrame.TYPE) { // do orderly close on input q @@ -949,6 +959,28 @@ public class Http2TestServerConnection { } } + boolean sendAltSvc(final String originHost, final Http2TestServer.AltSvcAddr altSvcAddr) { + Objects.requireNonNull(originHost); + System.err.printf("TestServer: AltSvcFrame for: %s%n", originHost); + try { + URI url = new URI("https://" + originHost); + String origin = url.toASCIIString(); + String svc = "h3=\"" + altSvcAddr.host() + ":" + altSvcAddr.port() + "\""; + svc = "fooh2=\":443\"; ma=2592000; persist=1, " + svc; + svc = svc + ", bar3=\":446\"; ma=2592000; persist=1"; + svc = svc + ", h3-34=\"" + altSvcAddr.host() + ":" + altSvcAddr.port() + +"\"; ma=2592000; persist=1"; + AltSvcFrame frame = new AltSvcFrame(0, 0, Optional.of(origin), svc); + System.err.printf("TestServer: Sending AltSvcFrame for: %s [%s]%n", origin, svc); + outputQ.put(frame); + return true; + } catch (IOException | URISyntaxException x) { + System.err.println("TestServer: Failed to send AltSvcFrame: " + x); + x.printStackTrace(); + } + return false; + } + static boolean isClientStreamId(int streamid) { return (streamid & 0x01) == 0x01; } @@ -967,12 +999,13 @@ public class Http2TestServerConnection { public List encodeHeaders(HttpHeaders headers, BiPredicate insertionPolicy) { List buffers = new LinkedList<>(); - + var entrySet = headers.map().entrySet(); + if (entrySet.isEmpty()) return buffers; ByteBuffer buf = getBuffer(); boolean encoded; headersLock.lock(); try { - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : entrySet) { List values = entry.getValue(); String key = entry.getKey().toLowerCase(); for (String value : values) { @@ -998,6 +1031,7 @@ public class Http2TestServerConnection { /** Encodes an ordered list of headers. */ public List encodeHeadersOrdered(List> headers) { List buffers = new LinkedList<>(); + if (headers.isEmpty()) return buffers; ByteBuffer buf = getBuffer(); boolean encoded; @@ -1045,7 +1079,9 @@ public class Http2TestServerConnection { } else throw x; } if (frame instanceof ResponseHeaders rh) { - var buffers = encodeHeaders(rh.headers, rh.insertionPolicy); + // order of headers matters - pseudo headers first followed by rest of the headers + final List encodedHeaders = new ArrayList(encodeHeaders(rh.pseudoHeaders, rh.insertionPolicy)); + encodedHeaders.addAll(encodeHeaders(rh.headers, rh.insertionPolicy)); int maxFrameSize = Math.min(rh.getMaxFrameSize(), getMaxFrameSize() - 64); int next = 0; int cont = 0; @@ -1054,9 +1090,9 @@ public class Http2TestServerConnection { // size we need to split the headers into one // HeadersFrame + N x ContinuationFrames int remaining = maxFrameSize; - var list = new ArrayList(buffers.size()); - for (; next < buffers.size(); next++) { - var b = buffers.get(next); + var list = new ArrayList(encodedHeaders.size()); + for (; next < encodedHeaders.size(); next++) { + var b = encodedHeaders.get(next); var len = b.remaining(); if (!b.hasRemaining()) continue; if (len <= remaining) { @@ -1072,7 +1108,7 @@ public class Http2TestServerConnection { } } int flags = rh.getFlags(); - if (next != buffers.size()) { + if (next != encodedHeaders.size()) { flags = flags & ~HeadersFrame.END_HEADERS; } if (cont > 0) { @@ -1087,7 +1123,7 @@ public class Http2TestServerConnection { } writeFrame(hf); cont++; - } while (next < buffers.size()); + } while (next < encodedHeaders.size()); } else if (frame instanceof OutgoingPushPromise) { handlePush((OutgoingPushPromise)frame); } else @@ -1109,7 +1145,7 @@ public class Http2TestServerConnection { PushPromiseFrame pp = new PushPromiseFrame(op.parentStream, op.getFlags(), promisedStreamid, - encodeHeaders(op.headers), + encodeHeaders(op.reqHeaders), 0); pushStreams.add(promisedStreamid); nextPushStreamId += 2; @@ -1140,7 +1176,7 @@ public class Http2TestServerConnection { oo.goodToGo(); exec.submit(() -> { try { - ResponseHeaders oh = getPushResponse(promisedStreamid); + ResponseHeaders oh = getPushResponse(promisedStreamid, op.rspHeaders); outputQ.put(oh); ii.transferTo(oo); @@ -1162,10 +1198,10 @@ public class Http2TestServerConnection { // returns a minimal response with status 200 // that is the response to the push promise just sent - private ResponseHeaders getPushResponse(int streamid) { - HttpHeadersBuilder hb = createNewHeadersBuilder(); - hb.addHeader(":status", "200"); - ResponseHeaders oh = new ResponseHeaders(hb.build()); + private ResponseHeaders getPushResponse(int streamid, HttpHeaders rspHeaders) { + HttpHeadersBuilder pseudoHeaders = createNewHeadersBuilder(); + pseudoHeaders.addHeader(":status", "200"); + ResponseHeaders oh = new ResponseHeaders(pseudoHeaders.build(), rspHeaders); oh.streamid(streamid); oh.setFlag(HeaderFrame.END_HEADERS); return oh; @@ -1187,7 +1223,9 @@ public class Http2TestServerConnection { try { byte[] buf = new byte[9]; int ret; + // System.err.println("TestServer: reading frame headers"); ret=is.readNBytes(buf, 0, 9); + // System.err.println("TestServer: got frame headers"); if (ret == 0) { return null; } else if (ret != 9) { @@ -1200,6 +1238,7 @@ public class Http2TestServerConnection { len = (len << 8) + n; } byte[] rest = new byte[len]; + // System.err.println("TestServer: reading frame body"); int n = is.readNBytes(rest, 0, len); if (n != len) throw new IOException("Error reading frame"); @@ -1364,82 +1403,60 @@ public class Http2TestServerConnection { // window updates done in main reader thread because they may // be used to unblock BodyOutputStreams waiting for WUPs - HashMap> updaters = new HashMap<>(); + final HashMap> updaters = new HashMap<>(); + final ReentrantLock updatersLock = new ReentrantLock(); void registerStreamWindowUpdater(int streamid, Consumer r) { - synchronized(updaters) { + updatersLock.lock(); + try { updaters.put(streamid, r); + } finally { + updatersLock.unlock(); } } - int sendWindow = 64 * 1024 - 1; // connection level send window + // connection level send window, permits = bytes + final Semaphore sendWindow = new Semaphore(64 * 1024 - 1); /** * BodyOutputStreams call this to get the connection window first. * * @param amount */ - public synchronized void obtainConnectionWindow(int amount) throws InterruptedException { - int demand = amount; - try { - int waited = 0; - while (amount > 0) { - int n = Math.min(amount, sendWindow); - amount -= n; - sendWindow -= n; - if (amount > 0) { - // Do not include this print line on a version that does not have - // JDK-8337395 - System.err.printf("%s: blocked waiting for %s connection window, obtained %s%n", - server.name, amount, demand - amount); - waited++; - wait(); - } - } - if (waited > 0) { - // Do not backport this print line on a version that does not have - // JDK-8337395 - System.err.printf("%s: obtained %s connection window, remaining %s%n", - server.name, demand, sendWindow); - } - assert amount == 0; - } catch (Throwable t) { - sendWindow += (demand - amount); - throw t; - } + public void obtainConnectionWindow(int amount) throws InterruptedException { + sendWindow.acquire(amount); } public void updateConnectionWindow(int amount) { - synchronized (this) { - // Do not backport this print line on a version that does not have - // JDK-8337395 - System.err.printf(server.name + ": update sendWindow (window=%s, amount=%s) is now: %s%n", - sendWindow, amount, sendWindow + amount); - sendWindow += amount; - notifyAll(); - } + System.err.printf("%s: sendWindow (available:%s, released amount=%s) is now: %s%n", + server.name, + sendWindow.availablePermits(), amount, sendWindow.availablePermits() + amount); + sendWindow.release(amount); } // simplified output headers class. really just a type safe container // for the hashmap. public static class ResponseHeaders extends Http2Frame { + final HttpHeaders pseudoHeaders; final HttpHeaders headers; final BiPredicate insertionPolicy; final int maxFrameSize; - public ResponseHeaders(HttpHeaders headers) { - this(headers, (n,v) -> false); + public ResponseHeaders(HttpHeaders pseudoHeaders, HttpHeaders headers) { + this(pseudoHeaders, headers, (n,v) -> false); } - public ResponseHeaders(HttpHeaders headers, BiPredicate insertionPolicy) { - this(headers, insertionPolicy, Integer.MAX_VALUE); + public ResponseHeaders(HttpHeaders pseudoHeaders, HttpHeaders headers, BiPredicate insertionPolicy) { + this(pseudoHeaders, headers, insertionPolicy, Integer.MAX_VALUE); } - public ResponseHeaders(HttpHeaders headers, + public ResponseHeaders(HttpHeaders pseudoHeaders, + HttpHeaders headers, BiPredicate insertionPolicy, int maxFrameSize) { super(0, 0); + this.pseudoHeaders = pseudoHeaders; this.headers = headers; this.insertionPolicy = insertionPolicy; this.maxFrameSize = maxFrameSize; diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java index 908a901133a..ace975e94fb 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2023, 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 @@ -34,7 +34,8 @@ import jdk.internal.net.http.frame.Http2Frame; // will be converted to a PushPromiseFrame in the writeLoop // a thread is then created to produce the DataFrames from the InputStream public class OutgoingPushPromise extends Http2Frame { - final HttpHeaders headers; + final HttpHeaders reqHeaders; + final HttpHeaders rspHeaders; final URI uri; final InputStream is; final int parentStream; // not the pushed streamid @@ -42,19 +43,22 @@ public class OutgoingPushPromise extends Http2Frame { public OutgoingPushPromise(int parentStream, URI uri, - HttpHeaders headers, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, InputStream is) { - this(parentStream, uri, headers, is, List.of()); + this(parentStream, uri, reqHeaders, rspHeaders, is, List.of()); } public OutgoingPushPromise(int parentStream, URI uri, - HttpHeaders headers, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, InputStream is, List continuations) { super(0,0); this.uri = uri; - this.headers = headers; + this.reqHeaders = reqHeaders; + this.rspHeaders = rspHeaders; this.is = is; this.parentStream = parentStream; this.continuations = List.copyOf(continuations); diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java new file mode 100644 index 00000000000..4b0ea351a09 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java @@ -0,0 +1,801 @@ +/* + * 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. + * + * 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.httpclient.test.lib.http3; + +import java.io.IOException; +import java.net.SocketAddress; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +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.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import jdk.httpclient.test.lib.quic.QuicServerConnection; +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.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +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.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.QuicConnectionImpl; +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.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; +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.http3.frames.SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; + +public class Http3ServerConnection { + private final Http3TestServer server; + private final QuicServerConnection quicConnection; + private final ConnectionTerminator quicConnTerminator; + private final SocketAddress peerAddress; + private final String dbgTag; + private final Logger debug; + private final UniStreamPair controlStreams; + private final UniStreamPair encoderStreams; + private final UniStreamPair decoderStreams; + private final Encoder qpackEncoder; + private final Decoder qpackDecoder; + private final FramesDecoder controlFramesDecoder; + private final AtomicLong nextPushId = new AtomicLong(); + private volatile long maxPushId = 0; + private final ReentrantLock pushIdLock = new ReentrantLock(); + private final Condition pushIdChanged = pushIdLock.newCondition(); + private final ConcurrentHashMap> requests = + new ConcurrentHashMap<>(); + private volatile boolean closeRequested; + // the max stream id of a processed H3 request. -1 implies none were processed. + private final AtomicLong maxProcessedRequestStreamId = new AtomicLong(-1); + // the stream id that was sent in a GOAWAY frame. -1 implies no GOAWAY frame was sent. + private final AtomicLong goAwayRequestStreamId = new AtomicLong(-1); + + private final CompletableFuture afterSettings = new MinimalFuture<>(); + private final CompletableFuture clientSettings = new MinimalFuture<>(); + + private final ConcurrentLinkedQueue lcsWriterQueue = + new ConcurrentLinkedQueue<>(); + + // A class used to dispatch peer initiated unidirectional streams + // according to their type. + 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) { + if (debug.on()) { + debug.log("peerControlStream %s dispatched", stream.streamId()); + } + complete(description, stream, controlStreams.futureReceiverStream()); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + if (debug.on()) debug.log("peer opened QPack encoder stream"); + complete(description, stream, decoderStreams.futureReceiverStream()); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + if (debug.on()) debug.log("peer opened QPack decoder stream"); + complete(description, stream, encoderStreams.futureReceiverStream()); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + // From RFC 9114: + // Only servers can push; if a server receives a client-initiated push stream, + // this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR. + close(H3_STREAM_CREATION_ERROR.code(), + "Push Stream %s opened by client" + .formatted(stream.streamId())); + } + + // completes the given completable future with the given stream + private void complete(String description, QuicReceiverStream stream, + CompletableFuture cf) { + if (debug.on()) { + 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); + } + } + } + + static CompletableFuture dispatch(Http3ServerConnection conn, + QuicReceiverStream stream) { + var dispatcher = conn.new Http3StreamDispatcher(stream); + dispatcher.start(); + return dispatcher.dispatchCF(); + } + } + + /** + * Creates a new {@code Http3ServerConnection}. + * Once created, the connection must be {@linkplain #start()} started. + * @param server the HTTP/3 server creating this connection + * @param connection the underlying Quic connection + */ + Http3ServerConnection(Http3TestServer server, + QuicServerConnection connection, + SocketAddress peerAddress) { + this.server = server; + this.quicConnection = connection; + this.quicConnTerminator = connection.connectionTerminator(); + this.peerAddress = peerAddress; + var qtag = connection.dbgTag(); + dbgTag = "H3-Server(" + qtag + ")"; + debug = Utils.getDebugLogger(this::dbgTag); + controlFramesDecoder = new FramesDecoder("H3-Server-control("+qtag+")", + FramesDecoder::isAllowedOnClientControlStream); + controlStreams = new UniStreamPair(StreamType.CONTROL, + quicConnection, this::receiveControlBytes, + this::lcsWriterLoop, + this::onControlStreamError, debug); + qpackEncoder = new Encoder(this::qpackInsertionPolicy, + this::createEncoderStreams, + this::connectionError); + encoderStreams = qpackEncoder.encoderStreams(); + qpackDecoder = new Decoder(this::createDecoderStreams, + this::connectionError); + decoderStreams = qpackDecoder.decoderStreams(); + } + + boolean qpackInsertionPolicy(TableEntry entry) { + List allowedHeaders = Http3TestServer.ENCODER_ALLOWED_HEADERS; + if (allowedHeaders.isEmpty()) { + return false; + } + if (allowedHeaders.contains(Http3TestServer.ALL_ALLOWED)) { + return true; + } + return allowedHeaders.contains(entry.name()); + } + + /** + * Starts this {@code Http3ServerConnection}. + */ + public void start() { + quicConnection.addRemoteStreamListener(this::onNewRemoteStream); + quicConnection.onHandshakeCompletion(this::handshakeDone); + } + + // push bytes to the local control stream queue + void writeControlStream(ByteBuffer buffer) { + lcsWriterQueue.add(buffer); + controlStreams.localWriteScheduler().runOrSchedule(); + } + + QuicConnectionImpl quicConnection() { + return this.quicConnection; + } + + Http3TestServer server() { + return this.server; + } + + String connectionKey() { + // assuming the localConnectionId never changes; + // this will return QuicServerConnectionId(NNN), which should + // be enough to detect whether two exchanges are made on the + // same connection + return quicConnection.logTag(); + } + + // The local control stream write loop + private void lcsWriterLoop() { + var controlStreams = this.controlStreams; + if (controlStreams == null) return; + var writer = controlStreams.localWriter(); + if (writer == null) return; + ByteBuffer buffer; + if (debug.on()) + debug.log("start control writing loop: credit=" + writer.credit()); + while (writer.credit() > 0 && (buffer = lcsWriterQueue.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) { + var stream = writer.stream(); + Http3Streams.debugErrorCode(debug, stream, "Control stream"); + if (!closeRequested && quicConnection.isOpen()) { + if (!Http3Error.isNoError(stream.sndErrorCode())) { + if (debug.on()) debug.log("Failed to write to control stream", t); + } + close(Http3Error.H3_CLOSED_CRITICAL_STREAM, "Failed to write to control stream"); + return; + } + } + } + } + + private boolean hasHttp3Error(QuicStream stream) { + if (stream instanceof QuicReceiverStream rcvs) { + var code = rcvs.rcvErrorCode(); + if (code > 0 && !Http3Error.isNoError(code)) return true; + } + if (stream instanceof QuicSenderStream snds) { + var code = snds.sndErrorCode(); + if (code > 0 && !Http3Error.isNoError(code)) return true; + } + return false; + } + + + private void onControlStreamError(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + // TODO: implement this! + try { + Http3Streams.debugErrorCode(debug, stream, "Control stream"); + if (!closeRequested && quicConnection.isOpen()) { + if (hasHttp3Error(stream)) { + if (debug.on()) { + debug.log("control stream " + stream.mode() + " failed", throwable); + } + } + close(Http3Error.H3_CLOSED_CRITICAL_STREAM, + "Control stream " + stream.mode() + " failed"); + } + } catch (Throwable t) { + if (debug.on() && !closeRequested) { + debug.log("onControlStreamError: handling ", throwable); + debug.log("onControlStreamError: exception while handling error: ", t); + } + } + } + + private void receiveControlBytes(ByteBuffer buffer) { + if (debug.on()) debug.log("received client control: %s bytes", buffer.remaining()); + controlFramesDecoder.submit(buffer); + Http3Frame frame; + while ((frame = controlFramesDecoder.poll()) != null) { + if (debug.on()) debug.log("client control frame: %s", frame); + if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + close(malformed.getErrorCode(), malformed.getMessage()); + controlStreams.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } else if (frame instanceof PartialFrame) { + var payloadBytes = controlFramesDecoder.readPayloadBytes(); + if (debug.on()) { + debug.log("added %s bytes to %s", + Utils.remaining(payloadBytes), + frame); + } + } else if (frame instanceof CancelPushFrame cpf) { + cancelPushReceived(cpf.getPushId()); + } else if (frame instanceof MaxPushIdFrame mpf) { + maxPushIdReceived(mpf.getMaxPushId()); + } else if (frame instanceof SettingsFrame sf) { + ConnectionSettings clientSettings = ConnectionSettings.createFrom(sf); + // Set max and current capacity of the QPack encoder + qpackEncoder.configure(clientSettings); + long clientMaxTableCapacity = clientSettings.qpackMaxTableCapacity(); + long capacity = Math.min(Http3TestServer.ENCODER_CAPACITY_LIMIT, + clientMaxTableCapacity); + // 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 (clientMaxTableCapacity != 0) { + qpackEncoder.setTableCapacity(capacity); + } + this.clientSettings.complete(clientSettings); + } + if (controlFramesDecoder.eof()) break; + } + if (controlFramesDecoder.eof()) { + close(Http3Error.H3_CLOSED_CRITICAL_STREAM, + "EOF reached while reading client control stream"); + } + } + + private void handshakeDone(Throwable t) { + if (t == null) { + controlStreams.futureSenderStream() + .thenApply(this::sendSettings) + .exceptionally(this::exceptionallyAndClose) + .thenApply(afterSettings::complete); + } else { + if (debug.on()) debug.log("Handshake failed: " + t, t); + // the connection is probably closed already, but just in case... + close(Http3Error.H3_INTERNAL_ERROR, "Handshake failed"); + } + } + + private T exceptionallyAndClose(Throwable t) { + try { + return exceptionally(t); + } finally { + // TODO: should we distinguish close due to + // exception from graceful close? + close(Http3Error.H3_INTERNAL_ERROR, message(t)); + } + } + + String message(Throwable t) { + return t == null ? "No Error" : t.getClass().getSimpleName(); + } + + private T exceptionally(Throwable t) { + try { + if (debug.on()) 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); + } + } + + private QuicSenderStream sendSettings(final QuicSenderStream localControlStream) { + final ConnectionSettings settings = server.getConfiguredConnectionSettings(); + final SettingsFrame settingsFrame = new SettingsFrame(); + + settingsFrame.setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, settings.maxFieldSectionSize()); + settingsFrame.setParameter(SETTINGS_QPACK_MAX_TABLE_CAPACITY, settings.qpackMaxTableCapacity()); + settingsFrame.setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, settings.qpackBlockedStreams()); + qpackDecoder.configure(settings); + + if (debug.on()) { + debug.log("sending server settings %s for connection %s", settingsFrame, this); + } + final long size = settingsFrame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + var buf = ByteBuffer.allocate((int)settingsFrame.size()); + settingsFrame.writeFrame(buf); + buf.flip(); + writeControlStream(buf); + return localControlStream; + } + + + private boolean onNewRemoteStream(QuicReceiverStream stream) { + boolean closeRequested = this.closeRequested; + if (closeRequested) return false; + + if (stream instanceof QuicBidiStream bidiStream) { + onNewHttpRequest(bidiStream); + } else { + Http3StreamDispatcher.dispatch(this, stream).whenComplete((r, t) -> { + if (t != null) dispatchFailed(t); + }); + } + if (debug.on()) { + debug.log("New stream %s accepted", stream.streamId()); + } + return true; + } + + private void onNewHttpRequest(QuicBidiStream stream) { + if (!this.server.shouldProcessNewHTTPRequest(this)) { + if (debug.on()) { + debug.log("Rejecting HTTP request on stream %s of connection %s", + stream.streamId(), this); + } + // consider the request as unprocessed and send a GOAWAY on the connection + try { + sendGoAway(); + } catch (IOException ioe) { + System.err.println("Failed to send GOAWAY on connection " + this + + " due to: " + ioe); + ioe.printStackTrace(); + } + return; + } + var streamId = stream.streamId(); + // keep track of the largest request id that we have processed + long currentLargest = maxProcessedRequestStreamId.get(); + while (streamId > currentLargest) { + if (maxProcessedRequestStreamId.compareAndSet(currentLargest, streamId)) { + break; + } + currentLargest = maxProcessedRequestStreamId.get(); + } + if (debug.on()) { + debug.log("new incoming HTTP request on stream %s", streamId); + } + if (requests.containsKey(stream.streamId())) { + if (debug.on()) { + debug.log("Stream %s already created!", streamId); + } + quicConnTerminator.terminate(appLayerClose(H3_STREAM_CREATION_ERROR.code()) + .loggedAs("stream already created")); + return; + } + // creation of the Http3ServerExchangeImpl involves connecting its reader, which + // takes a fair amount of (JIT?) time. Since this method is called from + // within the decrypt loop, it prevents decrypting the following ONERTT packets, + // which can unnecessarily delay the processing of ACKs and cause excessive + // retransmission + MinimalFuture exchCf = new MinimalFuture<>(); + requests.put(stream.streamId(), exchCf); + if (debug.on()) { + debug.log("HTTP/3 exchange future for stream %s registered", streamId); + } + server.getQuicServer().executor().execute(() -> createExchange(exchCf, stream)); + if (debug.on()) { + debug.log("HTTP/3 exchange creation for stream %s triggered", streamId); + } + } + + private void createExchange(CompletableFuture exchCf, + QuicBidiStream stream) { + var streamId = stream.streamId(); + if (debug.on()) { + debug.log("Completing HTTP/3 exchange future for stream %s", streamId); + } + exchCf.complete(new Http3ServerStreamImpl(this, stream)); + if (debug.on()) { + debug.log("HTTP/3 exchange future for stream %s Completed", streamId); + } + } + + public final String dbgTag() { return dbgTag; } + + private void dispatchFailed(Throwable throwable) { + // TODO: anything to do? + if (debug.on()) debug.log("dispatch failed: " + throwable); + } + + 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) { + // TODO: implement this! + // close connection here. + if (!closeRequested) { + String message = stream != null ? stream.mode() + " failed" : "is null"; + if (quicConnection().isOpen()) { + if (debug.on()) { + debug.log("QPack encoder stream " + message, throwable); + } + } else { + if (debug.on()) { + debug.log("QPack encoder stream " + message + ": " + 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) { + // TODO: implement this! + // close connection here. + if (!closeRequested) { + String message = stream != null ? stream.mode() + " failed" : "is null"; + if (quicConnection().isOpen()) { + if (debug.on()) { + debug.log("QPack decoder stream " + message, throwable); + } + } else { + debug.log("QPack decoder stream " + message + ": " + throwable); + } + } + } + + // public, to allow invocations from within tests + public void sendGoAway() throws IOException { + final QuicStreamWriter writer = controlStreams.localWriter(); + if (writer == null || !quicConnection.isOpen()) { + return; + } + // RFC-9114, section 5.2: + // Requests ... with the indicated identifier or greater + // are rejected ... by the sender of the GOAWAY. + final long maxProcessedStreamId = maxProcessedRequestStreamId.get(); + // adding 4 gets us the next stream id for the stream type + final long streamIdToReject = maxProcessedStreamId == -1 ? 0 : maxProcessedStreamId + 4; + // 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, since clients might + // already have retried unprocessed requests on another HTTP connection. + long currentGoAwayReqStrmId = goAwayRequestStreamId.get(); + while (currentGoAwayReqStrmId != -1 && streamIdToReject < currentGoAwayReqStrmId) { + if (goAwayRequestStreamId.compareAndSet(currentGoAwayReqStrmId, streamIdToReject)) { + break; + } + currentGoAwayReqStrmId = goAwayRequestStreamId.get(); + } + final GoAwayFrame frame = new GoAwayFrame(streamIdToReject); + final long size = frame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + final var buf = ByteBuffer.allocate((int) size); + frame.writeFrame(buf); + buf.flip(); + if (debug.on()) { + debug.log("Sending GOAWAY frame %s from server connection %s", frame, this); + } + writer.scheduleForWriting(buf, false); + } + + public void close(Http3Error error, String reason) { + close(error.code(), reason); + } + + void connectionError(Throwable throwable, Http3Error error) { + close(error, throwable.getMessage()); + } + + private boolean markCloseRequested() { + var closeRequested = this.closeRequested; + if (!closeRequested) { + synchronized (this) { + closeRequested = this.closeRequested; + if (!closeRequested) { + return this.closeRequested = true; + } + } + } + assert closeRequested; + if (debug.on()) debug.log("close already requested"); + return false; + } + + public void close(long error, String reason) { + if (markCloseRequested()) { + try { + sendGoAway(); + } catch (IOException e) { + // it's OK if we couldn't send a GOAWAY + if (debug.on()) { + debug.log("ignoring failure to send GOAWAY from server connection " + + this + " due to " + e); + } + } + if (quicConnection.isOpen()) { + if (debug.on()) debug.log("closing quic connection: " + reason); + quicConnTerminator.terminate(appLayerClose(error).loggedAs(reason)); + } else { + if (debug.on()) debug.log("quic connection already closed"); + } + } + } + + HeaderFrameReader newHeaderFrameReader(DecodingCallback decodingCallback) { + return qpackDecoder.newHeaderFrameReader(decodingCallback); + } + + void exchangeClosed(Http3ServerStreamImpl http3ServerExchange) { + requests.remove(http3ServerExchange.streamId()); + } + + sealed interface PushPromise permits CancelledPush, CompletedPush, PendingPush { + long pushId(); + } + + record CancelledPush(long pushId) implements PushPromise {} + record CompletedPush(long pushId, HttpHeaders headers) implements PushPromise {} + record PendingPush(long pushId, + CompletableFuture stream, + HttpHeaders headers, + Http3ServerExchange exchange) implements PushPromise { + } + + private final Map promiseMap = new ConcurrentHashMap<>(); + + PushPromise addPendingPush(long pushId, + CompletableFuture stream, + HttpHeaders headers, + Http3ServerExchange exchange) { + var push = new PendingPush(pushId, stream, headers, exchange); + expungePromiseMap(); + var previous = promiseMap.putIfAbsent(pushId, push); + if (previous == null || !(previous instanceof CancelledPush)) { + // allow to open multiple streams for the same pushId + // in order to test client behavior. We will return + // push even if the map contains a pending or completed + // push; + return push; + } + return previous; + } + + void addPushPromise(final long promiseId, final PushPromise promise) { + this.promiseMap.put(promiseId, promise); + } + + PushPromise getPushPromise(final long promiseId) { + return this.promiseMap.get(promiseId); + } + + void cancelPush(long pushId) { + expungePromiseMap(); + var push = promiseMap.putIfAbsent(pushId, new CancelledPush(pushId)); + if (push == null || push instanceof CancelledPush) return; + if (push instanceof CompletedPush) return; + if (push instanceof PendingPush pp) { + promiseMap.put(pushId, new CancelledPush(pushId)); + var ps = pp.stream(); + if (ps == null) { + try { + sendCancelPush(pushId); + } catch (IOException io) { + if (debug.on()) { + debug.log("Failed to send CANCEL_PUSH pushId=%s: %s", pushId, io); + } + } + } else { + ps.thenAccept(s -> { + try { + s.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (IOException io) { + if (debug.on()) { + debug.log("Failed to reset push stream pushId=%s, stream=%s: %s", + pushId, s.streamId(), io); + } + } + }); + } + } + } + + void sendCancelPush(long pushId) throws IOException { + CancelPushFrame cancelPushFrame = new CancelPushFrame(pushId); + ByteBuffer buf = ByteBuffer.allocate((int)cancelPushFrame.size()); + cancelPushFrame.writeFrame(buf); + buf.flip(); + // need to wait until after settings are sent. + afterSettings.thenAccept((s) -> writeControlStream(buf)); + } + + + void cancelPushReceived(long pushId) { + cancelPush(pushId); + } + + void maxPushIdReceived(long pushId) { + pushIdLock.lock(); + try { + if (pushId > maxPushId) { + if (debug.on()) debug.log("max pushId: " + pushId); + maxPushId = pushId; + pushIdChanged.signalAll(); + } + } finally { + pushIdLock.unlock(); + } + } + + final AtomicLong minPush = new AtomicLong(); + static final int MAX_PUSH_HISTORY = 100; + void expungePromiseMap() { + assert MAX_PUSH_HISTORY > 0; + while (promiseMap.size() >= MAX_PUSH_HISTORY) { + long lowest = minPush.getAndIncrement(); + var pp = promiseMap.remove(lowest); + if (pp instanceof PendingPush ppp) { + cancelPush(ppp.pushId); + } + } + } + + List encodeHeaders(int bufferSize, long streamId, HttpHeaders... headers) { + HeaderFrameWriter writer = qpackEncoder.newHeaderFrameWriter(); + return qpackEncoder.encodeHeaders(writer, streamId, bufferSize, headers); + } + + void decodeHeaders(final HeadersFrame partialHeadersFrame, final ByteBuffer buffer, + final HeaderFrameReader headersReader) throws IOException { + ByteBuffer received = partialHeadersFrame.nextPayloadBytes(buffer); + boolean done = partialHeadersFrame.remaining() == 0; + this.qpackDecoder.decodeHeader(received, done, headersReader); + } + + long nextPushId() { + return this.nextPushId.getAndIncrement(); + } + + long waitForMaxPushId(long pushId) throws InterruptedException { + long maxPushId = this.maxPushId; + if (maxPushId > pushId) return maxPushId; + do { + this.pushIdLock.lock(); + try { + maxPushId = this.maxPushId; + if (maxPushId > pushId) return maxPushId; + this.pushIdChanged.await(); + } finally { + this.pushIdLock.unlock(); + } + } while (true); + } + + public Encoder qpackEncoder() { + return qpackEncoder; + } + + public CompletableFuture clientHttp3Settings() { + return clientSettings; + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java new file mode 100644 index 00000000000..9a39ba4358d --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java @@ -0,0 +1,801 @@ +/* + * 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. + * + * 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.httpclient.test.lib.http3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.LongSupplier; + +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.http2.Http2TestExchange; +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.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +public final class Http3ServerExchange implements Http2TestExchange { + + private final Http3ServerStreamImpl serverStream; + private final Http3ServerConnection serverConn; + private final Logger debug; + final HttpHeaders requestHeaders; + final String method; + final String scheme; + final String authority; + final String path; + final URI uri; + final HttpHeadersBuilder rspheadersBuilder; + final SSLSession sslSession; + volatile long responseLength = 0; // 0 is unknown, -1 is 0 + volatile int responseCode; + final Http3ServerStreamImpl.RequestBodyInputStream is; + final ResponseBodyOutputStream os; + private boolean unknownReservedFrameAlreadySent; + + Http3ServerExchange(Http3ServerStreamImpl serverStream, HttpHeaders requestHeaders, + Http3ServerStreamImpl.RequestBodyInputStream is, SSLSession sslSession) { + this.serverStream = serverStream; + this.serverConn = serverStream.serverConnection(); + this.debug = Utils.getDebugLogger(this.serverConn::dbgTag); + this.requestHeaders = requestHeaders; + this.sslSession = sslSession; + this.is = is; + this.os = new ResponseBodyOutputStream(connectionTag(), debug, serverStream.writer, serverStream.writeLock, + serverStream.writeEnabled, this::getResponseLength); + method = requestHeaders.firstValue(":method").orElse(""); + //System.out.println("method = " + method); + path = requestHeaders.firstValue(":path").orElse(""); + //System.out.println("path = " + path); + scheme = requestHeaders.firstValue(":scheme").orElse(""); + //System.out.println("scheme = " + scheme); + authority = requestHeaders.firstValue(":authority").orElse(""); + if (!path.isEmpty() && !path.startsWith("/")) { + throw new IllegalArgumentException("Path is not absolute: " + path); + } + uri = URI.create(scheme + "://" + authority + path); + rspheadersBuilder = new HttpHeadersBuilder(); + } + + String connectionTag() { + return serverConn.quicConnection().logTag(); + } + + long getResponseLength() { + return responseLength; + } + + @Override + public String toString() { + return "H3Server Http3ServerExchange(%s)".formatted(serverStream.streamId()); + } + + @Override + public HttpHeaders getRequestHeaders() { + return requestHeaders; + } + + @Override + public HttpHeadersBuilder getResponseHeaders() { + return rspheadersBuilder; + } + + @Override + public URI getRequestURI() { + return uri; + } + + @Override + public String getRequestMethod() { + return method; + } + + @Override + public SSLSession getSSLSession() { + return sslSession; + } + + @Override + public void close() { + try { + is.close(); + os.close(); + serverStream.close(); + } catch (IOException e) { + if (debug.on()) { + debug.log(this + ".close exception: " + e, e); + } + } + } + + @Override + public void close(IOException io) throws IOException { + if (debug.on()) { + debug.log(this + " closed with exception: " + io); + } + if (serverStream.writer.sendingState().isSending()) { + if (debug.on()) { + debug.log(this + " resetting writer with H3_INTERNAL_ERROR"); + } + serverStream.writer.reset(Http3Error.H3_INTERNAL_ERROR.code()); + } + is.close(io); + os.closeInternal(); + close(); + } + + public Http3ServerExchange streamResetByPeer(IOException io) { + try { + if (debug.on()) + debug.log("H3 Server closing exchange: " + io); + close(io); + } catch (IOException e) { + if (debug.on()) + debug.log("Failed to close stream %s", serverStream.streamId()); + } + return this; + } + + @Override + public InputStream getRequestBody() { + return is; + } + + @Override + public OutputStream getResponseBody() { + return os; + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + // occasionally send an unknown/reserved HTTP3 frame to exercise the case + // where the client is expected to ignore such frames + try { + optionallySendUnknownOrReservedFrame(); + this.responseLength = responseLength; + sendResponseHeaders(serverStream.streamId(), serverStream.writer, isHeadRequest(), + rCode, responseLength, rspheadersBuilder, os); + } catch (Exception ex) { + throw new IOException("failed to send headers: " + ex, ex); + } + } + + // WARNING: this method is also called for PushStreams, which has + // a different writer, streamId, request etc... + // The only fields that can be safely used in this method is debug and + // http3ServerConnection + private void sendResponseHeaders(long streamId, + QuicStreamWriter writer, + boolean isHeadRequest, + int rCode, + long responseLength, + HttpHeadersBuilder rspheadersBuilder, + ResponseBodyOutputStream os) + throws IOException { + String tag = "streamId=" + streamId + " "; + // in case of HEAD request the caller is supposed to set Content-Length + // directly - and the responseLength passed here is supposed to be -1 + if (responseLength != 0 && rCode != 204 && !isHeadRequest) { + long clen = responseLength > 0 ? responseLength : 0; + rspheadersBuilder.setHeader("Content-length", Long.toString(clen)); + } + final HttpHeadersBuilder pseudoHeadersBuilder = new HttpHeadersBuilder(); + pseudoHeadersBuilder.setHeader(":status", Integer.toString(rCode)); + final HttpHeaders pseudoHeaders = pseudoHeadersBuilder.build(); + final HttpHeaders headers = rspheadersBuilder.build(); + // order of headers matters - pseudo headers first followed by rest of the headers + var payload = serverConn.encodeHeaders(1024, streamId, pseudoHeaders, headers); + if (debug.on()) + debug.log(tag + "headers payload: " + Utils.remaining(payload)); + HeadersFrame frame = new HeadersFrame(Utils.remaining(payload)); + ByteBuffer buffer = ByteBuffer.allocate(frame.headersSize()); + frame.writeHeaders(buffer); + buffer.flip(); + if (debug.on()) { + debug.log(tag + "Writing HeaderFrame headers: " + Utils.asHexString(buffer)); + } + boolean noBody = rCode >= 200 && (responseLength < 0 || rCode == 204); + boolean last = frame.length() == 0 && noBody; + if (last) { + if (debug.on()) { + debug.log(tag + "last payload sent: empty headers, no body"); + } + writer.scheduleForWriting(buffer, true); + } else { + writer.queueForWriting(buffer); + } + int size = payload.size(); + for (int i = 0; i < size; i++) { + last = i == size - 1; + var buf = payload.get(i); + if (debug.on()) { + debug.log(tag + "Writing HeaderFrame payload: " + Utils.asHexString(buf)); + } + if (last) { + if (debug.on()) { + debug.log(tag + "last headers bytes sent, %s", + noBody ? "no body" : "body should follow"); + } + writer.scheduleForWriting(buf, noBody); + } else { + writer.queueForWriting(buf); + } + } + if (noBody) { + if (debug.on()) { + debug.log(tag + "no body: closing os"); + } + os.closeInternal(); + } + os.goodToGo(); + if (debug.on()) { + debug.log(this + " Sent response headers " + tag + rCode); + } + } + + private void optionallySendUnknownOrReservedFrame() { + if (this.unknownReservedFrameAlreadySent) { + // don't send it more than once + return; + } + UnknownOrReservedFrame.tryGenerateFrame().ifPresent((f) -> { + if (debug.on()) { + debug.log("queueing to send an unknown/reserved HTTP3 frame: " + f); + } + try { + serverStream.writer.queueForWriting(f.toByteBuffer()); + } catch (IOException e) { + // ignore + if (debug.on()) { + debug.log("failed to queue unknown/reserved HTTP3 frame: " + f, e); + } + } + this.unknownReservedFrameAlreadySent = true; + }); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return serverConn.quicConnection().peerAddress(); + } + + @Override + public int getResponseCode() { + return responseCode; + } + + @Override + public InetSocketAddress getLocalAddress() { + return (InetSocketAddress) serverConn.quicConnection().localAddress(); + } + + @Override + public String getConnectionKey() { + return serverConn.connectionKey(); + } + + @Override + public String getProtocol() { + return "HTTP/3"; + } + + @Override + public HttpClient.Version getServerVersion() { + return HttpClient.Version.HTTP_3; + } + + @Override + public HttpClient.Version getExchangeVersion() { + return HttpClient.Version.HTTP_3; + } + + @Override + public boolean serverPushAllowed() { + return true; + } + + @Override + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + try { + serverPushWithId(uri, reqHeaders, rspHeaders, content); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to push " + uri + ": " + io); + throw io; + } + } + + @Override + public long serverPushWithId(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + HttpHeaders combinePromiseHeaders = combinePromiseHeaders(uri, reqHeaders); + long pushId = serverConn.nextPushId(); + if (debug.on()) { + debug.log("Server sending serverPushWithId(" + pushId + "): " + uri); + } + // send PUSH_PROMISE frame + sendPushPromiseFrame(pushId, uri, combinePromiseHeaders); + if (debug.on()) + debug.log("Server sent PUSH_PROMISE(" + pushId + ")"); + // now open push stream and send response headers + body + Http3ServerConnection.PushPromise pp = sendPushResponse(pushId, combinePromiseHeaders, rspHeaders, content); + assert pushId == pp.pushId(); + return pp.pushId(); + } + + @Override + public long sendPushId(long pushId, URI uri, HttpHeaders headers) throws IOException { + HttpHeaders combinePromiseHeaders = combinePromiseHeaders(uri, headers); + return sendPushPromiseFrame(pushId, uri, combinePromiseHeaders); + } + + @Override + public void sendPushResponse(long pushId, URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + HttpHeaders combinePromiseHeaders = combinePromiseHeaders(uri, reqHeaders); + Http3ServerConnection.PushPromise pp = sendPushResponse(pushId, combinePromiseHeaders, rspHeaders, content); + assert pushId == pp.pushId(); + } + + @Override + public void resetStream(long code) throws IOException { + os.resetStream(code); + } + + @Override + public void cancelPushId(long pushId) throws IOException { + serverConn.sendCancelPush(pushId); + } + + @Override + public long waitForMaxPushId(long pushId) throws InterruptedException { + return serverConn.waitForMaxPushId(pushId); + } + + @Override + public Encoder qpackEncoder() { + return serverConn.qpackEncoder(); + } + + @Override + public CompletableFuture clientHttp3Settings() { + return serverConn.clientHttp3Settings(); + } + + private long sendPushPromiseFrame(long pushId, URI uri, HttpHeaders headers) + throws IOException { + if (pushId == -1) pushId = serverConn.nextPushId(); + List payload = serverConn.encodeHeaders(1024, serverStream.streamId(), headers); + PushPromiseFrame frame = new PushPromiseFrame(pushId, Utils.remaining(payload)); + ByteBuffer buffer = ByteBuffer.allocate(frame.headersSize()); + frame.writeHeaders(buffer); + buffer.flip(); + boolean last = frame.length() == 0; + if (last) { + if (debug.on()) { + debug.log("last payload sent: empty headers, no body"); + } + serverStream.writer.scheduleForWriting(buffer, false); + } else { + serverStream.writer.queueForWriting(buffer); + } + int size = payload.size(); + for (int i = 0; i < size; i++) { + last = i == size - 1; + var buf = payload.get(i); + if (last) { + serverStream.writer.scheduleForWriting(buf, false); + } else { + serverStream.writer.queueForWriting(buf); + } + } + return pushId; + } + + private static HttpHeaders combinePromiseHeaders(URI uri, HttpHeaders headers) { + HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder(); + headersBuilder.setHeader(":method", "GET"); + headersBuilder.setHeader(":scheme", uri.getScheme()); + headersBuilder.setHeader(":authority", uri.getAuthority()); + headersBuilder.setHeader(":path", uri.getPath()); + for (Map.Entry> entry : headers.map().entrySet()) { + for (String value : entry.getValue()) + headersBuilder.addHeader(entry.getKey(), value); + } + return headersBuilder.build(); + } + + @Override + public void requestStopSending(long errorCode) { + serverStream.reader.stream().requestStopSending(errorCode); + } + + private QuicSenderStream cancel(QuicSenderStream s) { + try { + switch (s.sendingState()) { + case READY, SEND, DATA_SENT -> s.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } catch (IOException io) { + throw new UncheckedIOException(io); + } + return s; + } + + private Http3ServerConnection.PushPromise sendPushResponse(long pushId, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream body) { + var stream = serverConn.quicConnection() + .openNewLocalUniStream(Duration.ofSeconds(10)); + final Http3ServerConnection.PushPromise promise = + serverConn.addPendingPush(pushId, stream, reqHeaders, this); + if (!(promise instanceof Http3ServerConnection.PendingPush)) { + stream.thenApply(this::cancel); + return promise; + } + stream.thenApplyAsync(s -> { + if (debug.on()) { + debug.log("Server open(streamId=" + s.streamId() + ", pushId=" + pushId + ")"); + } + String tag = "streamId=" + s.streamId() + ": "; + var push = serverConn.getPushPromise(pushId); + if (push instanceof Http3ServerConnection.CancelledPush) { + this.cancel(s); + return push; + } + // no write loop: just buffer everything + final ReentrantLock pushLock = new ReentrantLock(); + final Condition writePushEnabled = pushLock.newCondition(); + final Runnable writeLoop = () -> { + pushLock.lock(); + try { + writePushEnabled.signalAll(); + } finally { + pushLock.unlock(); + } + }; + var pushw = s.connectWriter(SequentialScheduler.lockingScheduler(writeLoop)); + int tlen = VariableLengthEncoder.getEncodedSize(Http3Streams.PUSH_STREAM_CODE); + int plen = VariableLengthEncoder.getEncodedSize(pushId); + ByteBuffer buf = ByteBuffer.allocate(tlen + plen); + VariableLengthEncoder.encode(buf, Http3Streams.PUSH_STREAM_CODE); + VariableLengthEncoder.encode(buf, pushId); + buf.flip(); + try { + pushw.queueForWriting(buf); + if (debug.on()) { + debug.log(tag + "Server queued push stream type pushId=" + pushId + + " 0x" + Utils.asHexString(buf)); + } + ResponseBodyOutputStream os = new ResponseBodyOutputStream(connectionTag(), + debug, pushw, pushLock, writePushEnabled, () -> 0); + sendResponseHeaders(s.streamId(), pushw, false, 200, 0, + new HttpHeadersBuilder(rspHeaders), os); + if (debug.on()) { + debug.log(tag + "Server push response headers sent pushId=" + pushId); + } + switch (s.sendingState()) { + case SEND, READY -> { + if (!s.stopSendingReceived()) { + body.transferTo(os); + serverConn.addPushPromise(pushId, new Http3ServerConnection.CompletedPush(pushId, reqHeaders)); + os.close(); + if (debug.on()) { + debug.log(tag + "Server push response body sent pushId=" + pushId); + } + } else { + if (debug.on()) { + debug.log(tag + "Server push response body cancelled pushId=" + pushId); + } + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + cancel(s); + } + } + case RESET_SENT, RESET_RECVD -> { + if (debug.on()) { + debug.log(tag + "Server push response body cancelled pushId=" + pushId); + } + // benign race if already cancelled, stateless marker + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + } + default -> { + if (debug.on()) { + debug.log(tag + "Server push response body cancelled pushId=" + pushId); + } + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + cancel(s); + } + } + body.close(); + } catch (IOException io) { + if (debug.on()) { + debug.log(tag + "Server failed to send pushId=" + pushId + ": " + io); + } + throw new UncheckedIOException(io); + } + return serverConn.getPushPromise(pushId); + }, serverConn.server().getQuicServer().executor()).exceptionally(t -> { + if (debug.on()) { + debug.log("Server failed to send PushPromise(pushId=" + pushId + "): " + t); + } + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + try { + body.close(); + } catch (IOException io) { + if (debug.on()) { + debug.log("Failed to close PushPromise stream(pushId=" + + pushId + "): " + io); + } + } + return serverConn.getPushPromise(pushId); + }); + return promise; + } + + @Override + public CompletableFuture sendPing() { + final QuicConnectionImpl quicConn = serverConn.quicConnection(); + var executor = quicConn.quicInstance().executor(); + return quicConn.requestSendPing() + // ensure that dependent actions will not be executed in the + // thread that completes the CF + .thenApplyAsync(Function.identity(), executor) + .exceptionallyAsync(this::rethrow, executor); + } + + private T rethrow(Throwable t) { + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + if (t instanceof ExecutionException x) return rethrow(x.getCause()); + throw new CompletionException(t); + } + + private boolean isHeadRequest() { + return "HEAD".equals(method); + } + + static class ResponseBodyOutputStream extends OutputStream { + + volatile boolean closed; + volatile boolean goodToGo; + boolean headersWritten; + long sent; + private final QuicStreamWriter osw; + private final ReentrantLock writeLock; + private final Condition writeEnabled; + private final LongSupplier responseLength; + private final Logger debug; + private final String connectionTag; + + ResponseBodyOutputStream(String connectionTag, + Logger debug, + QuicStreamWriter writer, + ReentrantLock writeLock, + Condition writeEnabled, + LongSupplier responseLength) { + this.debug = debug; + this.writeLock = writeLock; + this.writeEnabled = writeEnabled; + this.responseLength = responseLength; + this.osw = writer; + this.connectionTag = connectionTag; + } + + private void writeHeadersIfNeeded(ByteBuffer buffer) + throws IOException { + assert writeLock.isHeldByCurrentThread(); + long responseLength = this.responseLength.getAsLong(); + boolean streaming = responseLength == 0; + if (streaming) { + if (buffer.hasRemaining()) { + int len = buffer.remaining(); + if (debug.on()) { + debug.log("Streaming BodyResponse: streamId=%s writing DataFrame(%s)", + osw.stream().streamId(), len); + } + var data = new DataFrame(len); + var headers = ByteBuffer.allocate(data.headersSize()); + data.writeHeaders(headers); + headers.flip(); + osw.queueForWriting(headers); + } + } else if (!headersWritten) { + long len = responseLength > 0 ? responseLength : 0; + if (debug.on()) { + debug.log("BodyResponse: streamId=%s writing DataFrame(%s)", + osw.stream().streamId(), len); + } + var data = new DataFrame(len); + var headers = ByteBuffer.allocate(data.headersSize()); + data.writeHeaders(headers); + headers.flip(); + osw.queueForWriting(headers); + headersWritten = true; + } + } + + @Override + public void write(int b) throws IOException { + var buffer = ByteBuffer.allocate(1); + buffer.put((byte) b); + buffer.flip(); + submit(buffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + // the data is not written immediately, and therefore + // it needs to be copied. + // maybe we should find a way to wait until the data + // has been written, but that sounds complex. + ByteBuffer buffer = ByteBuffer.wrap(b.clone(), off, len); + submit(buffer); + } + + String logTag() { + return connectionTag + " streamId=" + osw.stream().streamId(); + } + + /** + * Schedule the ByteBuffer for writing. The buffer must never + * be reused. + * + * @param buffer response data + * @throws IOException if the channel is closed + */ + public void submit(ByteBuffer buffer) throws IOException { + writeLock.lock(); + try { + if (closed && buffer.hasRemaining()) { + throw new ClosedChannelException(); + } + if (osw.credit() <= 0) { + if (Log.requests()) { + Log.logResponse(() -> logTag() + ": HTTP/3 Server waiting for credits"); + } + writeEnabled.awaitUninterruptibly(); + if (Log.requests()) { + Log.logResponse(() -> logTag() + ": HTTP/3 Server unblocked - credits: " + + osw.credit() + ", closed: " + closed); + } + } + if (closed) { + if (buffer.hasRemaining()) { + throw new ClosedChannelException(); + } else return; + } + int len = buffer.remaining(); + sent = sent + len; + writeHeadersIfNeeded(buffer); + long responseLength = this.responseLength.getAsLong(); + boolean streaming = responseLength == 0; + boolean last = !streaming && (sent == responseLength + || (sent == 0 && responseLength == -1)); + osw.scheduleForWriting(buffer, last); + if (last) closeInternal(); + if (!streaming && sent != 0 && sent > responseLength) { + throw new IOException("sent more bytes than expected"); + } + } finally { + writeLock.unlock(); + } + } + + public void closeInternal() { + if (debug.on()) { + debug.log("BodyResponse: streamId=%s closeInternal", osw.stream().streamId()); + } + if (closed) return; + writeLock.lock(); + try { + closed = true; + } finally { + writeLock.unlock(); + } + } + + public void close() throws IOException { + if (debug.on()) { + debug.log("BodyResponse: streamId=%s close", osw.stream().streamId()); + } + if (closed) return; + writeLock.lock(); + try { + if (closed) return; + closed = true; + switch (osw.sendingState()) { + case READY, SEND -> { + if (debug.on()) { + debug.log("BodyResponse: streamId=%s sending EOF", + osw.stream().streamId()); + } + osw.scheduleForWriting(QuicStreamReader.EOF, true); + writeEnabled.signalAll(); + } + default -> { + } + } + } catch (IOException io) { + throw new IOException(io); + } finally { + writeLock.unlock(); + } + } + + public void goodToGo() { + this.goodToGo = true; + } + + public void resetStream(long code) throws IOException { + if (closed) return; + writeLock.lock(); + try { + if (closed) return; + closed = true; + switch (osw.sendingState()) { + case READY, SEND, DATA_SENT: + osw.reset(code); + default: + break; + } + } catch (IOException io) { + throw new IOException(io); + } finally { + writeLock.unlock(); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java new file mode 100644 index 00000000000..c5a8709346c --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java @@ -0,0 +1,489 @@ +/* + * 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. + * + * 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.httpclient.test.lib.http3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.UncheckedIOException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +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.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.CancelPushFrame; +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.MalformedFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +final class Http3ServerStreamImpl { + private final Http3ServerConnection serverConn; + private final Logger debug; + final QuicBidiStream stream; + final SequentialScheduler readScheduler = SequentialScheduler.lockingScheduler(this::readLoop); + final SequentialScheduler writeScheduler = SequentialScheduler.lockingScheduler(this::writeLoop); + final QuicStreamReader reader; + final QuicStreamWriter writer; + final BuffersReader.ListBuffersReader incoming = BuffersReader.list(); + final DecodingCallback headersConsumer; + final HeaderFrameReader headersReader; + final HttpHeadersBuilder requestHeadersBuilder; + final ReentrantLock writeLock = new ReentrantLock(); + final Condition writeEnabled = writeLock.newCondition(); + final CompletableFuture requestHeadersCF = new MinimalFuture<>(); + final CompletableFuture exchangeCF; + final BlockingQueue requestBodyQueue = new LinkedBlockingQueue<>(); + private volatile boolean eof; + + volatile PartialFrame partialFrame; + + Http3ServerStreamImpl(Http3ServerConnection http3ServerConnection, QuicBidiStream stream) { + this.serverConn = http3ServerConnection; + this.debug = Utils.getDebugLogger(this.serverConn::dbgTag); + this.stream = stream; + requestHeadersBuilder = new HttpHeadersBuilder(); + headersConsumer = new HeadersConsumer(); + headersReader = http3ServerConnection.newHeaderFrameReader(headersConsumer); + writer = stream.connectWriter(writeScheduler); + reader = stream.connectReader(readScheduler); + exchangeCF = requestHeadersCF.thenApply(this::startExchange); + // TODO: add a start() method that calls reader.start(), and + // call it outside of the constructor + reader.start(); + } + + Http3ServerConnection serverConnection() { + return this.serverConn; + } + + private void readLoop() { + try { + readLoop0(); + } catch (QPackException qe) { + boolean isConnectionError = qe.isConnectionError(); + Http3Error error = qe.http3Error(); + Throwable cause = qe.getCause(); + if (isConnectionError) { + headersConsumer.onConnectionError(cause, error); + } else { + headersConsumer.onStreamError(cause, error); + } + } + } + + private void readLoop0() { + ByteBuffer buffer; + + // reader can be null if the readLoop is invoked + // before reader is assigned. + if (reader == null) return; + + if (debug.on()) { + debug.log("H3Server: entering readLoop(stream=%s)", stream.streamId()); + } + try { + while (!reader.isReset() && (buffer = reader.poll()) != null) { + if (buffer == QuicStreamReader.EOF) { + if (debug.on()) { + debug.log("H3Server: EOF on stream=" + stream.streamId()); + } + if (!eof) requestBodyQueue.add(buffer); + eof = true; + return; + } + if (debug.on()) { + debug.log("H3Server: got %s bytes on stream %s", buffer.remaining(), stream.streamId()); + } + + var partialFrame = this.partialFrame; + if (partialFrame != null && partialFrame.remaining() == 0) { + this.partialFrame = partialFrame = null; + } + if (partialFrame instanceof HeadersFrame partialHeaders) { + serverConn.decodeHeaders(partialHeaders, buffer, headersReader); + } else if (partialFrame instanceof DataFrame partialData) { + receiveData(partialData, buffer); + } else if (partialFrame != null) { + partialFrame.nextPayloadBytes(buffer); + } + if (!buffer.hasRemaining()) { + continue; + } + + incoming.add(buffer); + Http3Frame frame = Http3Frame.decode(incoming, FramesDecoder::isAllowedOnRequestStream, debug); + if (frame == null) continue; + if (frame instanceof PartialFrame partial) { + this.partialFrame = partialFrame = partial; + if (frame instanceof HeadersFrame partialHeaders) { + if (debug.on()) { + debug.log("H3Server Got headers: " + frame + " on stream=" + stream.streamId()); + } + long remaining = partial.remaining(); + long available = incoming.remaining(); + long read = Math.min(remaining, available); + if (read > 0) { + for (ByteBuffer buf : incoming.getAndRelease(read)) { + serverConn.decodeHeaders(partialHeaders, buf, headersReader); + } + } + } else if (frame instanceof DataFrame partialData) { + if (debug.on()) { + debug.log("H3Server Got request body: " + frame + " on stream=" + stream.streamId()); + } + long remaining = partial.remaining(); + long available = incoming.remaining(); + long read = Math.min(remaining, available); + if (read > 0) { + for (ByteBuffer buf : incoming.getAndRelease(read)) { + receiveData(partialData, buf); + } + } + + } else if (frame instanceof UnknownFrame unknown) { + unknown.nextPayloadBytes(incoming); + } else { + if (debug.on()) { + debug.log("H3Server Got unexpected partial frame: " + + frame + " on stream=" + stream.streamId()); + } + serverConn.close(Http3Error.H3_FRAME_UNEXPECTED, + "unexpected frame type=" + frame.type() + + " on stream=" + stream.streamId()); + readScheduler.stop(); + writeScheduler.stop(); + return; + } + } else if (frame instanceof MalformedFrame malformed) { + if (debug.on()) { + debug.log("H3Server Got frame: " + frame + " on stream=" + stream.streamId()); + } + serverConn.close(malformed.getErrorCode(), + malformed.getMessage()); + readScheduler.stop(); + writeScheduler.stop(); + return; + } else { + if (debug.on()) { + debug.log("H3Server Got frame: " + frame + " on stream=" + stream.streamId()); + } + } + } + if (reader.isReset()) { + if (debug.on()) + debug.log("H3 Server: stream %s reset", reader.stream().streamId()); + readScheduler.stop(); + resetReceived(); + } + if (debug.on()) + debug.log("H3 Server: exiting read loop"); + } catch (Throwable t) { + if (debug.on()) + debug.log("H3 Server: read loop failed: " + t); + if (reader.isReset()) { + if (debug.on()) { + debug.log("H3 Server: stream %s reset", reader.stream()); + } + readScheduler.stop(); + resetReceived(); + } else { + if (debug.on()) { + debug.log("H3 Server: closing connection due to: " + t, t); + } + serverConn.close(Http3Error.H3_INTERNAL_ERROR, serverConn.message(t)); + readScheduler.stop(); + writeScheduler.stop(); + } + } + } + + String readErrorString(String defVal) { + return Http3Streams.errorCodeAsString(reader.stream()).orElse(defVal); + } + + void resetReceived() { + // If stop_sending sent and reset received (implied by this method being called) + // then exit normally and don't send a reset + if (debug.on()) { + debug.log("resetReceived: stream:%s, isStopSendingRequested:%s, errorCode:%s, isNoError:%s", + stream.streamId(), stream.isStopSendingRequested(), stream.rcvErrorCode(), + Http3Error.isNoError(reader.stream().rcvErrorCode())); + } + + if (reader.stream().isStopSendingRequested() + && requestHeadersCF.isDone()) { + // we can only request stop sending in the handler after having + // parsed the headers, therefore, if requestHeadersCF is not + // completed when we reach here we should reset the stream. + + // We have requested stopSending and received a reset in response: + // nothing to do - let the response be sent to the client, but throw an + // exception if `is` is used again. + exchangeCF.thenApply(en -> { + en.is.close(new IOException("stopSendingRequested")); + return en; + }); + return; + } + + String msg = "Stream %s reset by peer: %s" + .formatted(streamId(), readErrorString("no error code")); + if (debug.on()) + debug.log("H3 Server: reset received: " + msg); + var io = new IOException(msg); + requestHeadersCF.completeExceptionally(io); + exchangeCF.thenApply((e) -> e.streamResetByPeer(io)); + } + + void receiveData(DataFrame partialDataFrame, ByteBuffer buffer) { + if (debug.on()) { + debug.log("receiving data: " + buffer.remaining() + " on stream=" + stream.streamId()); + } + ByteBuffer received = partialDataFrame.nextPayloadBytes(buffer); + requestBodyQueue.add(received); + } + + void cancelPushFrameReceived(CancelPushFrame cancel) { + serverConn.cancelPush(cancel.getPushId()); + } + + class RequestBodyInputStream extends InputStream { + volatile IOException error; + volatile boolean closed; + // uses an unbounded blocking queue in which the readrLoop + // publishes the DataFrames payload... + ByteBuffer current; + // Use lock to avoid pinned threads on the blocking queue + final ReentrantLock lock = new ReentrantLock(); + + ByteBuffer current() throws IOException { + lock.lock(); + try { + while (true) { + if (current != null && current.hasRemaining()) { + return current; + } + if (current == QuicStreamReader.EOF) return current; + try { + if (debug.on()) + debug.log("Taking buffer from queue"); + // Blocking call + current = requestBodyQueue.take(); + } catch (InterruptedException e) { + var io = new InterruptedIOException(); + Thread.currentThread().interrupt(); + io.initCause(e); + close(io); + var error = this.error; + if (error != null) throw error; + } + } + } finally { + lock.unlock(); + } + } + + @Override + public int read() throws IOException { + ByteBuffer buffer = current(); + if (buffer == QuicStreamReader.EOF) { + var error = this.error; + if (error == null) return -1; + throw error; + } + return buffer.get() & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + int remaining = len; + while (remaining > 0) { + ByteBuffer buffer = current(); + if (buffer == QuicStreamReader.EOF) { + if (len == remaining) { + var error = this.error; + if (error == null) return -1; + throw error; + } else return len - remaining; + } + int count = Math.min(buffer.remaining(), remaining); + buffer.get(b, off + (len - remaining), count); + remaining -= count; + } + return len - remaining; + } + + @Override + public void close() throws IOException { + lock.lock(); + try { + if (closed) return; + closed = true; + + } finally { + lock.unlock(); + } + if (debug.on()) + debug.log("Closing request body input stream"); + requestBodyQueue.add(QuicStreamReader.EOF); + } + + void close(IOException io) { + lock.lock(); + try { + if (closed) return; + closed = true; + error = io; + } finally { + lock.unlock(); + } + if (debug.on()) { + debug.log("Closing request body input stream: " + io); + } + requestBodyQueue.clear(); + requestBodyQueue.add(QuicStreamReader.EOF); + } + } + + Http3ServerExchange startExchange(HttpHeaders headers) { + Http3ServerExchange exchange = new Http3ServerExchange(this, headers, + new RequestBodyInputStream(), + serverConn.quicConnection().getTLSEngine().getSession()); + try { + serverConn.server().submitExchange(exchange); + } catch (Exception e) { + try { + exchange.close(new IOException(e)); + } catch (IOException ex) { + if (debug.on()) + debug.log("Failed to close exchange: " + ex); + } + } + return exchange; + } + + long streamId() { + return stream.streamId(); + } + + private void writeLoop() { + writeLock.lock(); + try { + writeEnabled.signalAll(); + } finally { + writeLock.unlock(); + } + } + + void close() { + serverConn.exchangeClosed(this); + } + + private final class HeadersConsumer extends ValidatingHeadersConsumer + implements DecodingCallback { + + private HeadersConsumer() { + super(Context.REQUEST); + } + + @Override + public void reset() { + super.reset(); + requestHeadersBuilder.clear(); + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + @Override + public void onDecoded(CharSequence name, CharSequence value) + throws UncheckedIOException { + String n = name.toString(); + String v = value.toString(); + super.onDecoded(n, v); + requestHeadersBuilder.addHeader(n, v); + if (Log.headers() && Log.trace()) { + Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}", + streamId(), n, v); + } + } + + @Override + public void onComplete() { + HttpHeaders requestHeaders = requestHeadersBuilder.build(); + headersReader.reset(); + requestHeadersCF.complete(requestHeaders); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + try { + stream.reset(http3Error.code()); + serverConn.connectionError(throwable, http3Error); + } catch (IOException ioe) { + serverConn.close(http3Error.code(), + ioe.getMessage()); + } + } + + @Override + public void onStreamError(Throwable throwable, Http3Error http3Error) { + try { + stream.reset(http3Error.code()); + } catch (IOException ioe) { + serverConn.close(http3Error.code(), + ioe.getMessage()); + } + } + + @Override + public long streamId() { + return stream.streamId(); + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java new file mode 100644 index 00000000000..ff76c67ed7e --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java @@ -0,0 +1,371 @@ +/* + * 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. + * + * 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.httpclient.test.lib.http3; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.System.Logger.Level; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil; +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil.Resolved; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.common.Logger; +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.SettingsFrame; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class Http3TestServer implements QuicServer.ConnectionAcceptor, AutoCloseable { + private static final AtomicLong IDS = new AtomicLong(); + + static final long DECODER_MAX_CAPACITY = + Utils.getLongProperty("http3.test.server.decoderMaxTableCapacity", 4096); + static final long ENCODER_CAPACITY_LIMIT = + Utils.getLongProperty("http3.test.server.encoderTableCapacityLimit", 4096); + + private static final String ALLOWED_HEADERS_PROP_NAME = "http3.test.server.encoderAllowedHeaders"; + static final String ALL_ALLOWED = "*"; + static final List ENCODER_ALLOWED_HEADERS = readEncoderAllowedHeadersProp(); + + private static List readEncoderAllowedHeadersProp() { + String properties = Utils.getProperty(ALLOWED_HEADERS_PROP_NAME); + if (properties == null) { + // If the system property is not set all headers are allowed to be encoded + return List.of(ALL_ALLOWED); + } else if(properties.isBlank()) { + // If system property value is a blank string - no headers are + // allowed to be encoded + return List.of(); + } + var headers = Arrays.stream(properties.split(",")) + .filter(Predicate.not(String::isBlank)) + .toList(); + if (headers.contains(ALL_ALLOWED)) { + return List.of(ALL_ALLOWED); + } + return headers; + } + + private final QuicServer quicServer; + private volatile boolean stopping; + private final Map handlers = new ConcurrentHashMap<>(); + private final Function handlerProvider; + private final Logger debug; + private final InetSocketAddress serverAddr; + private volatile ConnectionSettings ourSettings; + // request approver which takes the server connection key as the input + private volatile Predicate newRequestApprover; + + private static String nextName() { + return "h3-server-" + IDS.incrementAndGet(); + } + + /** + * Same as calling {@code Http3TestServer(sslContext, null)} + * + * @param sslContext SSLContext + * @throws IOException if the server could not be created + */ + public Http3TestServer(final SSLContext sslContext) throws IOException { + this(sslContext, null); + } + + /** + * Same as calling {@code Http3TestServer(sslContext, + * new InetSocketAddress(InetAddress.getLoopbackAddress(), port), null)} + * + * @param sslContext SSLContext + * @throws IOException if the server could not be created + */ + public Http3TestServer(final SSLContext sslContext, int port) throws IOException { + this(sslContext, new InetSocketAddress(InetAddress.getLoopbackAddress(), port), null); + } + + public Http3TestServer(final SSLContext sslContext, InetSocketAddress address, final ExecutorService executor) throws IOException { + this(quicServerBuilder().sslContext(sslContext).executor(executor).bindAddress(address).build(), null); + } + + public Http3TestServer(final SSLContext sslContext, final ExecutorService executor) throws IOException { + this(quicServerBuilder().sslContext(sslContext).executor(executor).build(), null); + } + + public Http3TestServer(final QuicServer quicServer) throws IOException { + this(quicServer, null); + } + + public Http3TestServer(final QuicServer quicServer, + final Function handlerProvider) + throws IOException { + Objects.requireNonNull(quicServer); + this.debug = Utils.getDebugLogger(quicServer::name); + this.quicServer = quicServer; + this.handlerProvider = handlerProvider; + this.quicServer.setConnectionAcceptor(this); + this.serverAddr = bindServer(this.quicServer); + debug.log(Level.INFO, "H3 server is listening at " + this.serverAddr); + } + + public void start() { + quicServer.start(); + } + + public void stop() { + try { + close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public QuicServer getQuicServer() { + return this.quicServer; + } + + public InetSocketAddress getAddress() { + return this.serverAddr; + } + + public String serverAuthority() { + final InetSocketAddress inetSockAddr = getAddress(); + final String hostIP = inetSockAddr.getAddress().getHostAddress(); + // escape for ipv6 + final String h = hostIP.contains(":") ? "[" + hostIP + "]" : hostIP; + return h + ":" + inetSockAddr.getPort(); + } + + public void addHandler(final String path, final Http2Handler handler) { + if (this.handlerProvider != null) { + throw new IllegalStateException("Cannot add handler to H3 server which uses a handler provider"); + } + Objects.requireNonNull(path); + Objects.requireNonNull(handler); + this.handlers.put(path, handler); + } + + public void setRequestApprover(final Predicate approver) { + this.newRequestApprover = approver; + } + + /** + * Sets the connection settings that will be used by this server to generate a SETTINGS frame + * to send to client peers + * + * @param connectionSettings The connection settings + * @return The instance of this server + */ + public Http3TestServer setConnectionSettings(final ConnectionSettings connectionSettings) { + Objects.requireNonNull(connectionSettings); + this.ourSettings = connectionSettings; + return this; + } + + /** + * {@return the connection settings of this server, which will be sent to + * client peers in a SETTINGS frame. If none have been configured then this method returns + * {@link Optional#empty() empty}} + */ + public ConnectionSettings getConfiguredConnectionSettings() { + if (this.ourSettings == null) { + SettingsFrame settings = SettingsFrame.defaultRFCSettings(); + settings.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DECODER_MAX_CAPACITY); + return ConnectionSettings.createFrom(settings); + } + return this.ourSettings; + } + + private static InetSocketAddress bindServer(final QuicServer server) throws IOException { + // bind the quic server to the socket + final SocketAddress addr = server.getEndpoint().getLocalAddress(); + if (addr instanceof InetSocketAddress inetsaddr) { + return inetsaddr; + } + throw new IOException(new IOException("Unexpected socket address type " + + addr.getClass().getName())); + } + + void submitExchange(final Http2TestExchange exchange) { + debug.log("H3 server handling exchange for: %s%n\t\t" + + "(Memory: max=%s, free=%s, total=%s)%n", + exchange.getRequestURI(), Runtime.getRuntime().maxMemory(), + Runtime.getRuntime().freeMemory(), Runtime.getRuntime().totalMemory()); + final String reqPath = exchange.getRequestURI().getPath(); + final Http2Handler handler; + if (this.handlerProvider != null) { + handler = this.handlerProvider.apply(reqPath); + } else { + Optional> match = + RequestPathMatcherUtil.findHandler(reqPath, this.handlers); + handler = match.isPresent() ? match.get().handler() : null; + } + // The server Http3ServerExchange uses a BlockingQueue to + // read data so handling the exchange in the current thread would + // wedge it. The executor must have at least one thread and must not + // execute inline - otherwise, we'd be wedged. + Thread currentThread = Thread.currentThread(); + this.quicServer.executorService().execute(() -> { + try { + // if no handler was located, we respond with a 404 + if (handler == null) { + respondForMissingHandler(exchange); + return; + } + // This assertion is too strong: there are cases + // where the calling task might terminate before + // the submitted task is executed. In which case + // it can safely run on the same thread. + // assert Thread.currentThread() != currentThread + // : "HTTP/3 server executor must have at least one thread"; + handler.handle(exchange); + } catch (Throwable failure) { + System.err.println("Failed to handle exchange: " + failure); + failure.printStackTrace(); + final var ioException = (failure instanceof IOException) + ? (IOException) failure + : new IOException(failure); + try { + exchange.close(ioException); + } catch (IOException x) { + System.err.println("Failed to close exchange: " + x); + } + } + }); + } + + private void respondForMissingHandler(final Http2TestExchange exchange) + throws IOException { + final byte[] responseBody = (this.getClass().getSimpleName() + + " - No handler available to handle request " + + exchange.getRequestURI()).getBytes(US_ASCII); + try (final OutputStream os = exchange.getResponseBody()) { + exchange.sendResponseHeaders(404, responseBody.length); + os.write(responseBody); + } + } + + /** + * Called by the {@link QuicServer} when a new connection has been added to the endpoint's + * connection map. + * + * @param source The client address + * @param quicConn the new connection + * @return true if the new connection should be accepted, false + * if it should be closed + */ + @Override + public boolean acceptIncoming(final SocketAddress source, final QuicServerConnection quicConn) { + if (stopping) { + return false; + } + debug.log("New connection %s accepted from %s", quicConn.dbgTag(), source); + quicConn.onSuccessfulHandshake(() -> { + var http3Connection = new Http3ServerConnection(this, quicConn, source); + http3Connection.start(); + }); + return true; + } + + boolean shouldProcessNewHTTPRequest(final Http3ServerConnection serverConn) { + final Predicate approver = this.newRequestApprover; + if (approver == null) { + // by the default the server will process new requests + return true; + } + final String connKey = serverConn.connectionKey(); + return approver.test(connKey); + } + + @Override + public void close() throws IOException { + stopping = true; + if (debug.on()) { + debug.log("Stopping H3 server " + this.serverAddr); + } + // TODO: should we close the quic server only if we "own" it + if (this.quicServer != null) { + this.quicServer.close(); + } + } + + public static QuicServer.Builder quicServerBuilder() { + return new H3QuicBuilder(); + } + + private static final class H3QuicBuilder extends QuicServer.Builder { + + public H3QuicBuilder() { + alpn = "h3"; + } + + @Override + public QuicServer build() throws IOException { + QuicVersion[] versions = availableQuicVersions; + if (versions == null) { + // default to v1 and v2 + versions = new QuicVersion[]{QuicVersion.QUIC_V1, QuicVersion.QUIC_V2}; + } + if (versions.length == 0) { + throw new IllegalStateException("Empty available QUIC versions"); + } + InetSocketAddress addr = bindAddress; + if (addr == null) { + // default to loopback address and ephemeral port + addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + } + SSLContext ctx = sslContext; + if (ctx == null) { + try { + ctx = SSLContext.getDefault(); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(ctx); + final String name = serverId == null ? nextName() : serverId; + return new QuicServer(name, addr, executor, versions, compatible, quicTLSContext, sniMatcher, + incomingDeliveryPolicy, outgoingDeliveryPolicy, alpn, Http3Error::stringForCode); + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java new file mode 100644 index 00000000000..eddad466daa --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java @@ -0,0 +1,94 @@ +/* + * 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. + * + * 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.httpclient.test.lib.http3; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.Random; + +import jdk.internal.net.http.http3.frames.AbstractHttp3Frame; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A test-only HTTP3 frame used to exercise the case where (client) implementation + * is expected to ignore unknown/reserved frames, as expected by RFC-9114, section + * 7.2.8 and section 9 + */ +final class UnknownOrReservedFrame extends AbstractHttp3Frame { + + private static final Random random = new Random(getSeed()); + + private final int payloadLength; + private final byte[] payload; + + public UnknownOrReservedFrame() { + super(generateRandomFrameType()); + this.payloadLength = random.nextInt(13); // arbitrary upper bound + this.payload = new byte[this.payloadLength]; + random.nextBytes(this.payload); + } + + @Override + public long length() { + return this.payloadLength; + } + + ByteBuffer toByteBuffer() { + final int frameSize = (int) this.size(); // cast is OK - value expected to be within range + final ByteBuffer buf = ByteBuffer.allocate(frameSize); + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type()); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.payloadLength); + // write the payload + buf.put(this.payload); + buf.flip(); + return buf; + } + + private static long generateRandomFrameType() { + final boolean useReservedFrameType = random.nextBoolean(); + if (useReservedFrameType) { + final int N = random.nextInt(100); // arbitrary upper bound + // RFC-9114, section 7.2.8: 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 + return 0x1F * N + 0x21; + } + // arbitrary lower bound of 0x45 + return random.nextLong(0x45, VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + + private static long getSeed() { + Long seed = Long.getLong("seed"); + return seed != null ? seed : System.nanoTime() ^ new Random().nextLong(); + } + + static Optional tryGenerateFrame() { + // an arbitrary decision to create a new unknown/reserved frame + if (random.nextInt() % 8 == 0) { + return Optional.of(new UnknownOrReservedFrame()); + } + return Optional.empty(); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java new file mode 100644 index 00000000000..96bb04a4ac7 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java @@ -0,0 +1,134 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +/** + * A client initiated QUIC connection to a server + */ +public final class ClientConnection implements AutoCloseable { + + private static final Logger debug = Utils.getDebugLogger(() -> ClientConnection.class.getName()); + + private static final ByteBuffer EOF = ByteBuffer.allocate(0); + private static final String ALPN = QuicStandaloneServer.ALPN; + + private final QuicConnection connection; + private final QuicEndpoint endpoint; + + /** + * Establishes a connection between a Quic client and a target Quic server. This includes completing + * the handshake between the client and the server. + * + * @param client The Quic client + * @param serverAddr the target server address + * @return a ClientConnection + * @throws IOException If there was any exception while establishing the connection + */ + public static ClientConnection establishConnection(final QuicClient client, + final InetSocketAddress serverAddr) + throws IOException { + Objects.requireNonNull(client); + Objects.requireNonNull(serverAddr); + final QuicConnection conn = client.createConnectionFor(serverAddr, new String[]{ALPN}); + assert conn instanceof QuicConnectionImpl : "unexpected QUIC connection type: " + + conn.getClass(); + final CompletableFuture handshakeCf = + conn.startHandshake(); + final QuicEndpoint endpoint; + try { + endpoint = handshakeCf.get(); + } catch (InterruptedException e) { + throw new IOException(e); + } catch (ExecutionException e) { + throw new IOException(e.getCause()); + } + assert endpoint != null : "null endpoint after handshake completion"; + if (debug.on()) { + debug.log("Quic connection established for client: " + client.name() + + ", local addr: " + conn.localAddress() + + ", peer addr: " + serverAddr + + ", endpoint: " + endpoint); + } + return new ClientConnection(conn, endpoint); + } + + private ClientConnection(final QuicConnection connection, final QuicEndpoint endpoint) { + this.connection = Objects.requireNonNull(connection); + this.endpoint = endpoint; + } + + /** + * Creates a new client initiated bidirectional stream to the server. The returned + * {@code ConnectedBidiStream} will have the reader and writer tasks started, thus + * allowing the caller of this method to then use the returned {@code ConnectedBidiStream} + * for sending or received data through the {@link ConnectedBidiStream#outputStream() output stream} + * or {@link ConnectedBidiStream#inputStream() input stream} respectively. + * + * @return + * @throws IOException + */ + public ConnectedBidiStream initiateNewBidiStream() throws IOException { + final QuicBidiStream quicBidiStream; + try { + // TODO: review the duration being passed and whether it needs to be something + // that should be taken as an input to the initiateNewBidiStream() method + quicBidiStream = this.connection.openNewLocalBidiStream(Duration.ZERO).get(); + } catch (InterruptedException e) { + throw new IOException(e); + } catch (ExecutionException e) { + throw new IOException(e.getCause()); + } + return new ConnectedBidiStream(quicBidiStream); + } + + public QuicConnection underlyingQuicConnection() { + return this.connection; + } + + public QuicEndpoint endpoint() { + return this.endpoint; + } + + @Override + public void close() throws Exception { + this.connection.connectionTerminator().terminate(forTransportError(NO_ERROR)); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java new file mode 100644 index 00000000000..9aa2e9040e2 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java @@ -0,0 +1,129 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Semaphore; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * A {@code ConnectedBidiStream} represents a {@link + * jdk.internal.net.http.quic.streams.QuicBidiStream} + * which has a reader and writer task/loop started for it + */ +public class ConnectedBidiStream implements AutoCloseable { + + private final QuicBidiStream bidiStream; + private final QuicStreamReader quicStreamReader; + private final QuicStreamWriter quicStreamWriter; + private final BlockingQueue incomingData; + private final Semaphore writeSemaphore = new Semaphore(1); + private final OutputStream outputStream; + private final QueueInputStream inputStream; + private final SequentialScheduler readScheduler; + private volatile boolean closed; + + ConnectedBidiStream(final QuicBidiStream bidiStream) { + Objects.requireNonNull(bidiStream); + this.bidiStream = bidiStream; + incomingData = new ArrayBlockingQueue<>(1024, true); + this.quicStreamReader = bidiStream.connectReader( + readScheduler = SequentialScheduler.lockingScheduler(new ReaderLoop())); + this.inputStream = new QueueInputStream(this.incomingData, QuicStreamReader.EOF, quicStreamReader); + this.quicStreamWriter = bidiStream.connectWriter( + SequentialScheduler.lockingScheduler(() -> { + System.out.println("Server writer task called"); + writeSemaphore.release(); + })); + this.outputStream = new OutStream(this.quicStreamWriter, writeSemaphore); + // TODO: start the reader when inputStream() is called instead? + this.quicStreamReader.start(); + } + + public InputStream inputStream() { + return this.inputStream; + } + + public OutputStream outputStream() { + return this.outputStream; + } + + public QuicBidiStream underlyingBidiStream() { + return this.bidiStream; + } + + @Override + public void close() throws Exception { + this.closed = true; + // TODO: use runOrSchedule(executor)? + this.readScheduler.runOrSchedule(); + } + + + private final class ReaderLoop implements Runnable { + + private volatile boolean alreadyLogged; + + @Override + public void run() { + try { + if (quicStreamReader == null) return; + while (true) { + final var bb = quicStreamReader.poll(); + if (closed) { + return; + } + if (bb == null) { + return; + } + incomingData.add(bb); + if (bb == QuicStreamReader.EOF) { + break; + } + } + } catch (Throwable e) { + if (closed && e instanceof IOException) { + // the stream has been closed so we ignore any IOExceptions + return; + } + System.err.println("Error in " + getClass()); + e.printStackTrace(); + var in = inputStream; + if (in != null) { + in.error(e); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java new file mode 100644 index 00000000000..277e0b289a5 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java @@ -0,0 +1,315 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.text.ParseException; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.quic.packets.QuicPacket; + +/** + * Used by the {@link QuicServer Quic server} and the {@link QuicServerConnection Quic server + * connection} to decide whether an incoming datagram needs to be dropped + */ +public interface DatagramDeliveryPolicy { + + /** + * System property for configuring the incoming datagram delivery policy for Quic server + */ + public static final String SYS_PROP_INCOMING_DELIVERY_POLICY = + "jdk.internal.httpclient.test.quic.incoming"; + + /** + * System property for configuring the outgoing datagram delivery policy for Quic server + */ + public static final String SYS_PROP_OUTGOING_DELIVERY_POLICY = + "jdk.internal.httpclient.test.quic.outgoing"; + + /** + * Will be called to decide if an incoming or an outgoing datagram should be dropped + * + * @param address The source or the destination address for the datagram + * @param payload The datagram payload + * @param connection The connection which was chosen to handle the Quic packet + * @return true if the datagram should be dropped, false otherwise + */ + boolean shouldDrop(SocketAddress address, ByteBuffer payload, QuicServerConnection connection, + QuicPacket.HeadersType headersType); + + /** + * Will be called to decide if an incoming datagram, which wasn't matched against + * any specific Quic connection, should be dropped + * + * @param source The source address which transmitted the datagram + * @param payload The datagram payload + * @return true if the datagram should be dropped, false otherwise + */ + boolean shouldDrop(SocketAddress source, ByteBuffer payload, QuicPacket.HeadersType headersType); + + static final DatagramDeliveryPolicy ALWAYS_DELIVER = new DatagramDeliveryPolicy() { + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + return false; + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + return false; + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=always deliver]"; + } + }; + + static final DatagramDeliveryPolicy NEVER_DELIVER = new DatagramDeliveryPolicy() { + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + return true; + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + return true; + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=never deliver]"; + } + }; + + static final class FixedRate implements DatagramDeliveryPolicy { + private final int n; + private final AtomicLong counter = new AtomicLong(); + + FixedRate(final int n) { + if (n <= 0) { + throw new IllegalArgumentException("n should be greater than 0"); + } + this.n = n; + } + + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + final long current = counter.incrementAndGet(); + return current % n == 0; // drop every nth + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + final long current = counter.incrementAndGet(); + return current % n == 0; // drop every nth + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=drop every " + n + "]"; + } + } + + static final class RandomDrop implements DatagramDeliveryPolicy { + private final long seed; + private final Random random; + + RandomDrop() { + Long s = null; + try { + // note that Long.valueOf(null) also throws a + // NumberFormatException so if the property is undefined this + // will still work correctly + s = Long.valueOf(System.getProperty("seed")); + } catch (NumberFormatException e) { + // do nothing: seed is still null + } + this.seed = s != null ? s : new Random().nextLong(); + this.random = new Random(seed); + } + + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + return this.random.nextLong() % 42 == 0; + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + return this.random.nextLong() % 42 == 0; + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=drop randomly, seed=" + seed + "]"; + } + } + + /** + * {@return a DatagramDeliveryPolicy which always returns false from the {@code shouldDrop} + * methods} + */ + public static DatagramDeliveryPolicy alwaysDeliver() { + return ALWAYS_DELIVER; + } + + /** + * {@return a DatagramDeliveryPolicy which always returns true from the {@code shouldDrop} + * methods} + */ + public static DatagramDeliveryPolicy neverDeliver() { + return NEVER_DELIVER; + } + + /** + * @param n the repeat count at which the datagram will be dropped + * @return a DatagramDeliveryPolicy which will return true on every {@code n}th call to + * either of the {@code shouldDrop} methods + */ + public static DatagramDeliveryPolicy dropEveryNth(final int n) { + return new FixedRate(n); + } + + /** + * @return a DatagramDeliveryPolicy which will randomly return true from the {@code shouldDrop} + * methods. If the {@code seed} system property is set then the {@code Random} instance used by + * this policy will use that seed. + */ + public static DatagramDeliveryPolicy dropRandomly() { + return new RandomDrop(); + } + + private static String privilegedGetProperty(String property) { + return privilegedGetProperty(property, null); + } + + private static String privilegedGetProperty(String property, String defval) { + return System.getProperty(property, defval); + } + + /** + * Reads the system property {@code sysPropName} and parses the value into a + * {@link DatagramDeliveryPolicy}. If the {@code sysPropName} system property isn't set or + * is set to a value of {@link String#isBlank() blank}, then this method returns a + * {@link DatagramDeliveryPolicy#alwaysDeliver() always deliver policy}. + *

    + * The {@code sysPropName} if set is expected to have either of the following values: + *

      + *
    • {@code always} - this returns a {@link DatagramDeliveryPolicy#alwaysDeliver() + * always deliver policy}
    • + *
    • {@code never} - this returns a {@link DatagramDeliveryPolicy#neverDeliver() + * never deliver policy}
    • + *
    • {@code fixed=} - where n is a positive integer, this returns a + * {@link DatagramDeliveryPolicy#dropEveryNth(int) dropEveryNth policy}
    • + *
    • {@code random} - this returns a + * {@link DatagramDeliveryPolicy#dropRandomly() dropRandomly policy}
    • + *
    + *

    + * + * @param sysPropName The system property name to use + * @return a DatagramDeliveryPolicy + * @throws ParseException If the system property value cannot be parsed into a + * DatagramDeliveryPolicy + */ + private static DatagramDeliveryPolicy fromSystemProperty(final String sysPropName) + throws ParseException { + String val = privilegedGetProperty(sysPropName); + if (val == null || val.isBlank()) { + return ALWAYS_DELIVER; + } + val = val.trim(); + if (val.startsWith("fixed=")) { + // read the characters following "fixed=" + String rateVal = val.substring("fixed=".length()); + final int n; + try { + n = Integer.parseInt(rateVal); + } catch (NumberFormatException nfe) { + throw new ParseException("Unexpected value: " + val, "fixed=".length()); + } + return dropEveryNth(n); + } else if (val.equals("random")) { + return dropRandomly(); + } else if (val.equals("always")) { + return ALWAYS_DELIVER; + } else if (val.equals("never")) { + return NEVER_DELIVER; + } else { + throw new ParseException("Unexpected value: " + val, 0); + } + } + + /** + * Returns the default incoming datagram delivery policy. This takes into account the + * {@link DatagramDeliveryPolicy#SYS_PROP_INCOMING_DELIVERY_POLICY} system property to decide + * the default policy + * + * @return the default incoming datagram delivery policy + * @throws ParseException If the {@link DatagramDeliveryPolicy#SYS_PROP_INCOMING_DELIVERY_POLICY} + * was configured and there was a problem parsing its value + */ + public static DatagramDeliveryPolicy defaultIncomingPolicy() throws ParseException { + try { + return fromSystemProperty(SYS_PROP_INCOMING_DELIVERY_POLICY); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + } + + /** + * Returns the default outgoing datagram delivery policy. This takes into account the + * {@link DatagramDeliveryPolicy#SYS_PROP_OUTGOING_DELIVERY_POLICY} system property to decide + * the default policy + * + * @return the default outgoing datagram delivery policy + * @throws ParseException If the {@link DatagramDeliveryPolicy#SYS_PROP_OUTGOING_DELIVERY_POLICY} + * was configured and there was a problem parsing its value + */ + + public static DatagramDeliveryPolicy defaultOutgoingPolicy() throws ParseException { + try { + return fromSystemProperty(SYS_PROP_OUTGOING_DELIVERY_POLICY); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.java new file mode 100644 index 00000000000..659c68682ef --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.Semaphore; + +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * An {@link OutputStream} which writes using a {@link QuicStreamWriter} + */ +final class OutStream extends OutputStream { + + private final QuicStreamWriter quicStreamWriter; + private final Semaphore writeSemaphore; + + OutStream(final QuicStreamWriter quicStreamWriter, Semaphore writeSemaphore) { + Objects.requireNonNull(quicStreamWriter); + this.quicStreamWriter = quicStreamWriter; + this.writeSemaphore = Objects.requireNonNull(writeSemaphore); + } + + @Override + public void write(final int b) throws IOException { + this.write(new byte[]{(byte) (b & 0xff)}); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + while (quicStreamWriter.credit() < 0 + && !quicStreamWriter.stopSendingReceived()) { + try { + writeSemaphore.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + // the data will be queued and won't be written immediately, and therefore + // it needs to be copied. + final ByteBuffer data = ByteBuffer.wrap(b.clone(), off, len); + quicStreamWriter.scheduleForWriting(data, false); + } + + @Override + public void close() throws IOException { + quicStreamWriter.scheduleForWriting(QuicStreamReader.EOF, true); + super.close(); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java new file mode 100644 index 00000000000..2756fa732fa --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java @@ -0,0 +1,164 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; + +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * An {@code InputStream} which reads its data from a {@link BlockingQueue} + */ +final class QueueInputStream extends InputStream { + private final BlockingQueue incomingData; + private final ByteBuffer eofIndicator; + private final QuicStreamReader streamReader; + // error needs volatile access as it is set by a different thread + private volatile Throwable error; + // current might not need volatile access as it should only + // be read/set by the reading thread. However available() + // might conceivably be called by multiple threads. + private volatile ByteBuffer current; + + QueueInputStream(final BlockingQueue incomingData, + final ByteBuffer eofIndicator, + QuicStreamReader streamReader) { + this.incomingData = incomingData; + this.eofIndicator = eofIndicator; + this.streamReader = streamReader; + } + + private ByteBuffer current() throws InterruptedException { + ByteBuffer current = this.current; + // if eof, there should no more byte buffer + if (current == eofIndicator) return eofIndicator; + if (current == null || !current.hasRemaining()) { + return (current = this.current = incomingData.take()); + } + return current; + } + + private boolean eof() { + ByteBuffer current = this.current; + return current == eofIndicator; + } + + @Override + public int read() throws IOException { + final byte[] data = new byte[1]; + final int numRead = this.read(data, 0, data.length); + // can't be 0, since we block till we receive at least 1 byte of data + assert numRead != 0 : "No data read"; + if (numRead == -1) { + return -1; + } + return data[0]; + } + + // concurrent calls to read() should not and are not supported + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + int totalRead = 0; + while (totalRead < len) { + ByteBuffer bb = null; + checkError(); + try { + bb = current(); + } catch (InterruptedException e) { + streamReader.stream().requestStopSending(QuicTransportErrors.NO_ERROR.code()); + // TODO: should close here + error(e); + Thread.currentThread().interrupt(); + throw toIOException(e); + } + if (bb == eofIndicator) { + return totalRead == 0 ? -1 : totalRead; + } + final int available = bb.remaining(); + if (available > 0) { + final int numToTransfer = Math.min(available, (len - totalRead)); + bb.get(b, off + totalRead, numToTransfer); + totalRead += numToTransfer; + } + // if more data is available, take more, else if we have read at least 1 byte + // then return back + if (totalRead > 0 && incomingData.peek() == null) { + return totalRead; + } + } + if (totalRead > 0) return totalRead; + // if we reach here then len must be 0 + checkError(); + assert len == 0; + return eof() ? -1 : 0; + } + + @Override + public int available() throws IOException { + var bb = current; + if (bb == null || !bb.hasRemaining()) bb = incomingData.peek(); + if (bb == null || bb == eofIndicator) return 0; + return bb.remaining(); + } + + // we only check for errors after the incoming data queue + // has been emptied - except for interrupt. + private void checkError() throws IOException { + var error = this.error; + if (error == null) return; + if (error instanceof InterruptedException) + throw new IOException("closed by interrupt"); + var bb = current; + if (bb == eofIndicator || (bb != null && bb.hasRemaining())) return; + // we create a new exception to have the caller in the + // stack trace. + if (incomingData.isEmpty()) throw toIOException(error); + } + + // called if an error comes from upstream + void error(Throwable error) { + boolean firstError = false; + // only keep the first error + synchronized (this) { + var e = this.error; + if (e == null) { + e = this.error = error; + firstError = true; + } + } + // unblock read if needed + if (firstError) { + incomingData.add(ByteBuffer.allocate(0)); + } + } + + static IOException toIOException(Throwable error) { + return new IOException(error); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java new file mode 100644 index 00000000000..ae600a7a592 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java @@ -0,0 +1,913 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.security.AlgorithmConstraints; +import java.security.AlgorithmParameters; +import java.security.CryptoPrimitive; +import java.security.Key; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.LongFunction; + +import javax.net.ssl.SNIMatcher; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.ServerNameMatcher; +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.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.QuicEndpoint.QuicEndpointFactory; +import jdk.internal.net.http.quic.QuicInstance; +import jdk.internal.net.http.quic.QuicSelector; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.CONNECTION_REFUSED; + +/** + * This class represents a QuicServer. + */ +public sealed class QuicServer implements QuicInstance, AutoCloseable permits QuicStandaloneServer { + + public interface ConnectionAcceptor { + boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConnection); + } + + final Logger debug = Utils.getDebugLogger(this::name); + + private final String serverId; + private final String name; + private final ExecutorService executor; + private final boolean ownExecutor; + private volatile ConnectionAcceptor newConnectionAcceptor; + private final String alpn; + private final InetSocketAddress bindAddress; + private final SNIMatcher sniMatcher; + private volatile InetSocketAddress listenAddress; + private final QuicTLSContext quicTLSContext; + private volatile boolean started; + private volatile boolean sendRetry; + protected final List availableQuicVersions; + private final QuicVersion preferredVersion; + private volatile QuicEndpoint endpoint; + private volatile QuicSelector selector; + private volatile boolean closed; + private volatile QuicTransportParameters transportParameters; + private final byte[] retryTokenPrefixBytes = new byte[4]; + private final byte[] newTokenPrefixBytes = new byte[4]; + private final DatagramDeliveryPolicy incomingDeliveryPolicy; + private final DatagramDeliveryPolicy outgoingDeliveryPolicy; + private boolean wantClientAuth; + private boolean needClientAuth; + // set of KeyAgreement algorithms to reject; used to force a HelloRetryRequest + private Set rejectedKAAlgos; + // used to compute MAX_STREAMS limit by connections created on this server instance. + // if null, then an internal algorithm is used to compute the limit. + // The Function takes a boolean argument whose value is true if the computation is for bidi + // streams and false for uni streams. The returned value from this function is expected + // to be the MAX_STREAMS limit to be imposed on the peer for that particular stream type + private volatile Function maxStreamLimitComputer; + + private final ReentrantLock quickServerLock = new ReentrantLock(); + private final LongFunction appErrorCodeToString; + + record RetryData(QuicConnectionId originalServerConnId, + QuicConnectionId serverChosenConnId) { + } + + public static abstract class Builder { + protected SSLContext sslContext; + protected InetSocketAddress bindAddress; + protected DatagramDeliveryPolicy incomingDeliveryPolicy; + protected DatagramDeliveryPolicy outgoingDeliveryPolicy; + protected String serverId; + protected SNIMatcher sniMatcher; + protected ExecutorService executor; + protected QuicVersion[] availableQuicVersions; + protected boolean compatible; + protected LongFunction appErrorCodeToString; + + protected ConnectionAcceptor connAcceptor = + (source, conn) -> { + System.err.println("Rejecting connection " + conn + " attempt from source " + + source); + return false; + }; + + protected String alpn; + + protected Builder() { + try { + incomingDeliveryPolicy = DatagramDeliveryPolicy.defaultIncomingPolicy(); + outgoingDeliveryPolicy = DatagramDeliveryPolicy.defaultOutgoingPolicy(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public Builder availableVersions(final QuicVersion[] available) { + Objects.requireNonNull(available); + if (available.length == 0) { + throw new IllegalArgumentException("Empty available versions"); + } + this.availableQuicVersions = available; + return this; + } + + public Builder appErrorCodeToString(LongFunction appErrorCodeToString) { + this.appErrorCodeToString = appErrorCodeToString; + return this; + } + + public Builder compatibleNegotiation(boolean compatible) { + this.compatible = compatible; + return this; + } + + public Builder sslContext(final SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public Builder sniMatcher(final SNIMatcher sniMatcher) { + this.sniMatcher = sniMatcher; + return this; + } + + public Builder bindAddress(final InetSocketAddress addr) { + Objects.requireNonNull(addr); + this.bindAddress = addr; + return this; + } + + public Builder serverId(final String serverId) { + Objects.requireNonNull(serverId); + this.serverId = serverId; + return this; + } + + public Builder incomingDeliveryPolicy(final DatagramDeliveryPolicy policy) { + Objects.requireNonNull(policy); + this.incomingDeliveryPolicy = policy; + return this; + } + + public Builder outgoingDeliveryPolicy(final DatagramDeliveryPolicy policy) { + Objects.requireNonNull(policy); + this.outgoingDeliveryPolicy = policy; + return this; + } + + public Builder executor(final ExecutorService executor) { + this.executor = executor; + return this; + } + + public Builder alpn(String alpn) { + this.alpn = alpn; + return this; + } + + public abstract T build() throws IOException; + } + + private final QuicEndpointFactory endpointFactory; + public QuicServer(final String serverId, final InetSocketAddress bindAddress, + final ExecutorService executor, final QuicVersion[] availableQuicVersions, + boolean compatible, final QuicTLSContext quicTLSContext, final SNIMatcher sniMatcher, + final DatagramDeliveryPolicy incomingDeliveryPolicy, + final DatagramDeliveryPolicy outgoingDeliveryPolicy, String alpn, + final LongFunction appErrorCodeToString) { + this.bindAddress = bindAddress; + this.sniMatcher = sniMatcher == null + ? new ServerNameMatcher(this.bindAddress.getHostName()) + : sniMatcher; + this.alpn = Objects.requireNonNull(alpn); + this.appErrorCodeToString = appErrorCodeToString == null + ? QuicInstance.super::appErrorToString + : appErrorCodeToString; + if (executor != null) { + this.executor = executor; + this.ownExecutor = false; + } else { + this.executor = Utils.safeExecutor( + createExecutor(serverId), + (_, t) -> debug.log("rejected task - using ASYNC_POOL", t)); + this.ownExecutor = true; + } + this.serverId = serverId; + this.quicTLSContext = quicTLSContext; + this.name = "QuicServer(%s)".formatted(serverId); + if (compatible) { + this.availableQuicVersions = Arrays.asList(QuicVersion.values()); + } else { + this.availableQuicVersions = Arrays.asList(availableQuicVersions); + } + this.preferredVersion = availableQuicVersions[0]; + this.incomingDeliveryPolicy = incomingDeliveryPolicy; + this.outgoingDeliveryPolicy = outgoingDeliveryPolicy; + final Random random = new Random(); + random.nextBytes(retryTokenPrefixBytes); + random.nextBytes(newTokenPrefixBytes); + this.endpointFactory = newQuicEndpointFactory(); + if (debug.on()) { + debug.log("server created, incoming delivery policy %s, outgoing delivery policy %s", + this.incomingDeliveryPolicy, this.outgoingDeliveryPolicy); + } + } + + private static ExecutorService createExecutor(String name) { + String threadNamePrefix = "%s-quic-pool".formatted(name); + ThreadFactory threadFactory = Thread.ofPlatform().name(threadNamePrefix, 0).factory(); + return Executors.newCachedThreadPool(threadFactory); + } + + @Override + public String appErrorToString(long errorCode) { + return appErrorCodeToString.apply(errorCode); + } + + static QuicEndpointFactory newQuicEndpointFactory() { + return new QuicEndpointFactory(); + } + + public void setConnectionAcceptor(final ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor); + quickServerLock.lock(); + try { + var current = this.newConnectionAcceptor; + if (current != null) { + throw new IllegalStateException("An connection acceptor already exists for" + + " this quic server " + this); + } + this.newConnectionAcceptor = acceptor; + } finally { + quickServerLock.unlock(); + } + } + + public void setWantClientAuth(boolean wantClientAuth) { + this.wantClientAuth = wantClientAuth; + } + + public void setNeedClientAuth(boolean needClientAuth) { + this.needClientAuth = needClientAuth; + } + + public void setRejectKeyAgreement(Set rejectedKAAlgos) { + this.rejectedKAAlgos = rejectedKAAlgos; + } + + Function getMaxStreamLimitComputer() { + return this.maxStreamLimitComputer; + } + + /** + * Sets a new MAX_STREAMS limit computer for this server. + * @param computer the limit computer. can be null, in which case an internal computation + * algorithm with decide the MAX_STREAMS limit for connections on this server + * instance + */ + public void setMaxStreamLimitComputer(final Function computer) { + this.maxStreamLimitComputer = computer; + } + + @Override + public String instanceId() { + return serverId; + } + + @Override + public QuicTLSContext getQuicTLSContext() { + return quicTLSContext; + } + + @Override + public boolean isVersionAvailable(QuicVersion quicVersion) { + return availableQuicVersions.contains(quicVersion); + } + + @Override + public List getAvailableVersions() { + return availableQuicVersions; + } + + public void sendRetry(final boolean enable) { + this.sendRetry = enable; + } + + public void start() { + this.started = true; + try { + final QuicEndpoint endpoint = getEndpoint(); + final InetSocketAddress addr = this.listenAddress = (InetSocketAddress) endpoint.getLocalAddress(); + if (debug.on()) { + debug.log("Quic server listening at: " + addr + + " supported versions " + this.availableQuicVersions); + } + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + /** + * {@return the address on which the server is listening on} + * + * @throws IllegalStateException If server hasn't yet started + */ + public InetSocketAddress getAddress() { + final var addr = this.listenAddress; + if (addr == null) { + throw new IllegalArgumentException("Server hasn't started"); + } + return addr; + } + + /** + * The name identifying this QuicServer, used in debug traces. + * + * @return the name identifying this QuicServer. + * @implNote This is {@code "QuicServer()"}. + */ + @Override + public String name() { + return name; + } + + /** + * The executor used by this QuicServer when a task needs to + * be offloaded to a separate thread. + * + * @return the executor used by this QuicServer. + * @implNote This is the server internal executor. + */ + @Override + public Executor executor() { + return executor; + } + + public ExecutorService executorService() { + return this.executor; + } + + /** + * Get the QuicEndpoint for the given transport. + * + * @return the QuicEndpoint for the given transport. + * @throws IOException if an error occurs when setting up the endpoint + * or linking the transport with the endpoint. + * @throws IllegalStateException if the server is closed. + */ + @Override + public QuicEndpoint getEndpoint() throws IOException { + var endpoint = this.endpoint; + if (endpoint != null) return endpoint; + var selector = getSelector(); + quickServerLock.lock(); + try { + if (closed) throw new IllegalStateException("QuicServer is closed"); + endpoint = this.endpoint; + if (endpoint != null) return endpoint; + final String endpointName = "QuicEndpoint(" + serverId + ")"; + endpoint = this.endpoint = switch (QuicEndpoint.CONFIGURED_CHANNEL_TYPE) { + case NON_BLOCKING_WITH_SELECTOR -> + endpointFactory.createSelectableEndpoint(this, endpointName, + bindAddress, selector.timer()); + case BLOCKING_WITH_VIRTUAL_THREADS -> + endpointFactory.createVirtualThreadedEndpoint(this, endpointName, + bindAddress, selector.timer()); + }; + } finally { + quickServerLock.unlock(); + } + // register the newly created endpoint with the selector + QuicEndpoint.registerWithSelector(endpoint, selector, debug); + return endpoint; + } + + /** + * Gets the QuicSelector for the transport. + * + * @return the QuicSelector for the given transport. + * @throws IOException if an error occurs when setting up the selector + * or linking the transport with the selector. + * @throws IllegalStateException if the server is closed. + */ + private QuicSelector getSelector() throws IOException { + var selector = this.selector; + if (selector != null) return selector; + quickServerLock.lock(); + try { + if (closed) throw new IllegalStateException("QuicServer is closed"); + selector = this.selector; + if (selector != null) return selector; + final String selectorName = "QuicSelector(" + serverId + ")"; + selector = this.selector = switch (QuicEndpoint.CONFIGURED_CHANNEL_TYPE) { + case NON_BLOCKING_WITH_SELECTOR -> + QuicSelector.createQuicNioSelector(this, selectorName); + case BLOCKING_WITH_VIRTUAL_THREADS -> + QuicSelector.createQuicVirtualThreadPoller(this, selectorName); + }; + } finally { + quickServerLock.unlock(); + } + // 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) + debug.log("starting selector"); + selector.start(); + return selector; + } + + @Override + public void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer) { + if (debug.on()) { + debug.log("Received datagram %s(src=%s, payload(%d))", type, source, buffer.remaining()); + } + // consult the delivery policy to see if we should silently drop this packet + if (this.incomingDeliveryPolicy.shouldDrop(source, buffer, type)) { + silentIgnorePacket(source, buffer, type, false); + return; + } + // check packet type. If Initial, it may be a connection attempt + int pos = buffer.position(); + if (type != QuicPacket.HeadersType.LONG) { + if (debug.on()) { + debug.log("Dropping unmatched datagram %s(src=%s, payload(%d))", + type, source, buffer.remaining()); + } + return; + } + // INITIAL packet + // decode packet here + // TODO: FIXME + // Transport: is this needed? + // ALPN, etc... + // Move this to a dedicated method + // Double check how the serverId provided by the client should + // be replaced + // Should the new connection have 2 connections id for a time? + // => the initial one that the client sent, and that will + // be used until the client receives our response, + // => the new connection id that we are sending back to the + // client? + LongHeader header = QuicPacketDecoder.peekLongHeader(buffer); + if (header == null) { + if (debug.on()) { + debug.log("Dropping invalid datagram %s(src=%s, payload(%d))", + type, source, buffer.remaining()); + } + return; + } + // need to assert that dest.remaining() >= 8 and drop the packet + // if this is not the case. + if (header.destinationId().length() < 8) { + debug.log("destination connection id has not enough bytes: %d", + header.destinationId().length()); + return; + } + + final QuicVersion version = QuicVersion.of(header.version()).orElse(null); + try { + // check that the server supports the version, send a version + if (header.version() == 0) { + if (debug.on()) { + debug.log("Stray version negotiation packet"); + } + return; + } + if (version == null || !availableQuicVersions.contains(version)) { + if (debug.on()) { + debug.log("Unsupported version number 0x%x in incoming packet (len=%d)", header.version(), buffer.remaining()); + } + if (buffer.remaining() >= QuicConnectionImpl.SMALLEST_MAXIMUM_DATAGRAM_SIZE) { + // A server might not send a Version Negotiation packet if the datagram it receives is smaller than the minimum size specified in a different version + int[] supported = availableQuicVersions.stream().mapToInt(QuicVersion::versionNumber).toArray(); + var negotiate = QuicPacketEncoder + .newVersionNegotiationPacket(header.destinationId(), + header.sourceId(), supported); + ByteBuffer datagram = ByteBuffer.allocateDirect(negotiate.size()); + QuicPacketEncoder.of(QuicVersion.QUIC_V1).encode(negotiate, datagram, null); + datagram.flip(); + sendDatagram(source, datagram); + } + return; + } + } catch (Throwable t) { + debug.log("Failed to decode packet", t); + return; + } + assert availableQuicVersions.contains(version); + final InetSocketAddress peerAddress = (InetSocketAddress) source; + final ByteBuffer token = QuicPacketDecoder.peekInitialPacketToken(buffer); + if (token == null) { + // packet is malformed: token will be an empty ByteBuffer if + // the packet doesn't contain a token. + debug.log("failed to read connection token"); + return; + } + var localAddress = this.getAddress(); + var conflict = Utils.addressConflict(localAddress, peerAddress); + if (conflict != null) { + String msg = "%s: %s (local:%s == peer:%s)!"; + System.out.println(msg.formatted(this, conflict, localAddress, peerAddress)); + debug.log(msg, "WARNING", conflict, localAddress, peerAddress); + Log.logError(msg.formatted(this, conflict, localAddress, peerAddress)); + } + final QuicServerConnection connection; + if (token.hasRemaining()) { + // the INITIAL packet contains a token. This token is then expected to either match + // the RETRY packet token that this server might have sent (if any) or a NEW_TOKEN frame + // token that this server might have sent (if any). If the token doesn't match either + // of these expectations then drop the packet (or send CLOSE_CONNECTION frame) + final RetryData retryData = isRetryToken(token.asReadOnlyBuffer()); + if (retryData != null) { + // the token matches one that this server could have sent as a RETRY token. verify + // that this server was indeed configured to send a RETRY token. + if (!sendRetry) { + // although the token looks like a RETRY token, this server wasn't configured + // to send a RETRY token, so consider this an invalid token and drop the packet + // (or send CLOSE_CONNECTION frame) + debug.log("Server dropping INITIAL packet due to token " + + "(which looks like an unexpected retry token) from " + peerAddress); + return; + } + // verify the dest connection id in the INITIAL packet is the one that we had asked + // the client to use through our RETRY packet + if (!retryData.serverChosenConnId.equals(header.destinationId())) { + // drop the packet + debug.log("Invalid dest connection id in INITIAL packet," + + " expected the one sent in RETRY packet " + retryData.serverChosenConnId + + " but found a different one " + header.destinationId()); + return; + } + // at this point we have verified that the token is a valid retry token that this server + // sent. We can now create a connection + final SSLParameters sslParameters = createSSLParameters(peerAddress); + final byte[] clientInitialToken = new byte[token.remaining()]; + token.get(clientInitialToken); + connection = new QuicServerConnection(this, version, preferredVersion, + peerAddress, header.destinationId(), + sslParameters, clientInitialToken, retryData); + debug.log("Created new server connection " + connection + " (with a retry token) " + + "to client " + peerAddress); + } else { + // token doesn't match a RETRY token. check if it is a NEW_TOKEN that this server + // sent + final boolean isNewToken = isNewToken(token.asReadOnlyBuffer()); + if (!isNewToken) { + // invalid token in the INITIAL packet. drop packet (or send CLOSE_CONNECTION + // frame) + debug.log("Server dropping INITIAL packet due to unexpected token from " + + peerAddress); + return; + } + // matches a NEW_TOKEN token. create the connection + final SSLParameters sslParameters = createSSLParameters(peerAddress); + final byte[] clientInitialToken = new byte[token.remaining()]; + token.get(clientInitialToken); + connection = new QuicServerConnection(this, version, preferredVersion, + peerAddress, header.destinationId(), + sslParameters, clientInitialToken); + debug.log("Created new server connection " + connection + + " (with NEW_TOKEN initial token) to client " + peerAddress); + } + } else { + // token is empty in INITIAL packet. send a RETRY packet if the server is configured + // to do so. The spec allows us to send the RETRY packet more than once to the same + // client, so we don't have to maintain any state to check if we already have sent one. + if (sendRetry) { + // send RETRY packet + final QuicConnectionId serverConnId = this.endpoint.idFactory().newConnectionId(); + final byte[] retryToken = buildRetryToken(header.destinationId(), serverConnId); + QuicPacketEncoder encoder = QuicPacketEncoder.of(version); + final var retry = encoder + .newRetryPacket(serverConnId, header.sourceId(), retryToken); + final ByteBuffer datagram = ByteBuffer.allocateDirect(retry.size()); + try { + encoder.encode(retry, datagram, new RetryCodingContext(header.destinationId(), quicTLSContext)); + } catch (Throwable t) { + // TODO: should we throw exception? + debug.log("Failed to encode packet", t); + return; + } + datagram.flip(); + debug.log("Sending RETRY packet to client " + peerAddress); + sendDatagram(source, datagram); + return; + } + // no token in INITIAL frame and the server isn't configured to send a RETRY packet. + // we are now ready to create the connection + final SSLParameters sslParameters = createSSLParameters(peerAddress); + connection = new QuicServerConnection(this, version, preferredVersion, + peerAddress, header.destinationId(), + sslParameters, null); + debug.log("Created new server connection " + connection + + " (without any initial token) to client " + peerAddress); + } + assert connection.quicVersion() == version; + + // TODO: maybe we should coalesce some dummy packet in the datagram + // to make sure the client will ignore it + // => this might require slightly altering the algorithm for + // encoding packets: + // we may need to build a packet, and then only encode it + // instead of having a toByteBuffer() method. + try { + endpoint.registerNewConnection(connection); + } catch (IOException io) { + if (closed) { + debug.log("Can't register new connection: server closed"); + } else if (debug.on()) { + debug.log("Can't register new connection", io); + } + // drop all bytes in the payload + buffer.position(buffer.limit()); + connection.connectionTerminator().terminate( + forTransportError(CONNECTION_REFUSED).loggedAs(io.getMessage())); + return; + } + connection.processIncoming(source, header.destinationId().asReadOnlyBuffer(), type, buffer); + + final ConnectionAcceptor connAcceptor = this.newConnectionAcceptor; + if (connAcceptor == null || !connAcceptor.acceptIncoming(source, connection)) { + buffer.position(buffer.limit()); + final String msg = "Quic server " + this.serverId + " refused connection"; + connection.connectionTerminator().terminate( + forTransportError(CONNECTION_REFUSED).loggedAs(msg)); + return; + } + } + + void sendDatagram(final SocketAddress dest, final ByteBuffer datagram) { + final QuicPacket.HeadersType headersType = QuicPacketDecoder.peekHeaderType(datagram, + datagram.position()); + if (this.outgoingDeliveryPolicy.shouldDrop(dest, datagram, headersType)) { + silentIgnorePacket(dest, datagram, headersType, true); + return; + } + endpoint.pushDatagram(null, dest, datagram); + return; + } + + private SSLParameters createSSLParameters(final InetSocketAddress peerAddress) { + final SSLParameters sslParameters = Utils.copySSLParameters(this.getSSLParameters()); + sslParameters.setApplicationProtocols(new String[]{this.alpn}); + sslParameters.setProtocols(new String[] {"TLSv1.3"}); + if (this.sniMatcher != null) { + sslParameters.setSNIMatchers(List.of(this.sniMatcher)); + } + if (wantClientAuth) { + sslParameters.setWantClientAuth(true); + } else if (needClientAuth) { + sslParameters.setNeedClientAuth(true); + } + if (rejectedKAAlgos != null) { + sslParameters.setAlgorithmConstraints(new TestAlgorithmConstraints(rejectedKAAlgos)); + } + return sslParameters; + } + + private byte[] buildRetryToken(final QuicConnectionId originalServerConnId, + final QuicConnectionId serverChosenNewConnId) { + // TODO this token is too simple to provide authenticity guarantee; use for testing only + final int NUM_BYTES_FOR_CONN_ID_LENGTH = 1; + final byte[] result = new byte[retryTokenPrefixBytes.length + + NUM_BYTES_FOR_CONN_ID_LENGTH + originalServerConnId.length() + + NUM_BYTES_FOR_CONN_ID_LENGTH + serverChosenNewConnId.length()]; + // copy the retry token prefix + System.arraycopy(retryTokenPrefixBytes, 0, result, 0, retryTokenPrefixBytes.length); + int currentIndex = retryTokenPrefixBytes.length; + // copy over the length of the original dest conn id + result[currentIndex++] = (byte) originalServerConnId.length(); + // copy over the original dest connection id sent by the client + originalServerConnId.asReadOnlyBuffer().get(0, result, currentIndex, originalServerConnId.length()); + currentIndex += originalServerConnId.length(); + // copy over the length of the server chosen dest conn id + result[currentIndex++] = (byte) serverChosenNewConnId.length(); + // copy over the connection id that the server has chosen and expects clients to use as new dest conn id + serverChosenNewConnId.asReadOnlyBuffer().get(0, result, currentIndex, serverChosenNewConnId.length()); + return result; + } + + private RetryData isRetryToken(final ByteBuffer token) { + Objects.requireNonNull(token); + if (!token.hasRemaining()) { + return null; + } + final int NUM_BYTES_FOR_CONN_ID_LENGTH = 1; + // we expect the retry token prefix and 2 connection ids. so expected length = retry token prefix + // length plus the length of each of the connection ids + final int expectedLength = retryTokenPrefixBytes.length + NUM_BYTES_FOR_CONN_ID_LENGTH + + NUM_BYTES_FOR_CONN_ID_LENGTH; + if (token.remaining() <= expectedLength) { + return null; + } + final byte[] tokenPrefixBytes = new byte[retryTokenPrefixBytes.length]; + token.get(tokenPrefixBytes); + int mismatchIndex = Arrays.mismatch(retryTokenPrefixBytes, 0, retryTokenPrefixBytes.length, + tokenPrefixBytes, 0, retryTokenPrefixBytes.length); + if (mismatchIndex != -1) { + // token doesn't start with the expected retry token prefix. Not a valid retry token + return null; + } + // now find the length of the original connection id + final int originalServerConnIdLen = token.get(); + final byte[] originalServerConnId = new byte[originalServerConnIdLen]; + // read the original dest conn id + token.get(originalServerConnId); + + // now find the length of the server generated dest connection id + final int serverChosenDestConnIdLen = token.get(); + final byte[] serverChosenDestConnId = new byte[serverChosenDestConnIdLen]; + // read the server chosen dest conn id + token.get(serverChosenDestConnId); + + // TODO: the use of PeerConnectionId is only for convenience + return new RetryData(new PeerConnectionId(originalServerConnId), new PeerConnectionId(serverChosenDestConnId)); + } + + DatagramDeliveryPolicy incomingDeliveryPolicy() { + return this.incomingDeliveryPolicy; + } + + DatagramDeliveryPolicy outgoingDeliveryPolicy() { + return this.outgoingDeliveryPolicy; + } + + byte[] buildNewToken() { + // TODO this token is too simple to provide authenticity guarantee; use for testing only + final byte[] token = new byte[newTokenPrefixBytes.length]; + // copy the new_token prefix + System.arraycopy(newTokenPrefixBytes, 0, token, 0, newTokenPrefixBytes.length); + return token; + } + + private boolean isNewToken(final ByteBuffer token) { + Objects.requireNonNull(token); + if (!token.hasRemaining()) { + return false; + } + if (token.remaining() != newTokenPrefixBytes.length) { + return false; + } + final byte[] tokenBytes = new byte[newTokenPrefixBytes.length]; + token.get(tokenBytes); + int mismatchIndex = Arrays.mismatch(newTokenPrefixBytes, 0, newTokenPrefixBytes.length, + tokenBytes, 0, newTokenPrefixBytes.length); + if (mismatchIndex != -1) { + // token doesn't start with the expected new_token prefix. Not a valid token + return false; + } + return true; + } + + private void silentIgnorePacket(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType, final boolean outgoing) { + if (debug.on()) debug.log("silently dropping %s packet %s %s", headersType, + (outgoing ? "to dest" : "from source"), source); + } + + @Override + public void close() throws IOException { + // TODO: ignore exceptions while closing? + quickServerLock.lock(); + try { + if (closed) return; + closed = true; + } finally { + quickServerLock.unlock(); + } + //http3Server.stop(); + var endpoint = this.endpoint; + if (endpoint != null) endpoint.close(); + debug.log("endpoint closed"); + var selector = this.selector; + if (selector != null) selector.close(); + debug.log("selector closed"); + if (ownExecutor && executor != null) { + debug.log("shutting down executor"); + this.executor.shutdown(); + try { + debug.log("awaiting termination"); + this.executor.awaitTermination(QuicSelector.IDLE_PERIOD_MS, + TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + this.executor.shutdownNow(); + } + } + } + + /** + * The transport parameters that will be sent to the peer by any new subsequent server connections + * that are created by this server + * + * @param params transport parameters. Can be null, in which case the new server connection + * (whenever it is created) will use internal defaults + */ + public void setTransportParameters(QuicTransportParameters params) { + this.transportParameters = params; + } + + /** + * {@return the current configured transport parameters for new server connections. null if none + * configured} + */ + @Override + public QuicTransportParameters getTransportParameters() { + final QuicTransportParameters qtp = this.transportParameters; + if (qtp == null) { + return null; + } + // return a copy + return new QuicTransportParameters(qtp); + } + + /** + * Called + * + * @param connection + * @param originalConnectionId + * @param localConnectionId + */ + void connectionAcknowledged(QuicConnection connection, + QuicConnectionId originalConnectionId, + QuicConnectionId localConnectionId) { + // endpoint.removeConnectionId(originalConnectionId); + } + + private static class TestAlgorithmConstraints implements AlgorithmConstraints { + private final Set rejectedKAAlgos; + + public TestAlgorithmConstraints(Set rejectedKAAlgos) { + this.rejectedKAAlgos = rejectedKAAlgos; + } + + @Override + public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { + if (primitives.contains(CryptoPrimitive.KEY_AGREEMENT) && + rejectedKAAlgos.contains(algorithm)) { + return false; + } + return true; + } + + @Override + public boolean permits(Set primitives, Key key) { + return true; + } + + @Override + public boolean permits(Set primitives, String algorithm, Key key, AlgorithmParameters parameters) { + return true; + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java new file mode 100644 index 00000000000..53a9129d452 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java @@ -0,0 +1,570 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.QuicTransportParameters.VersionInformation; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.NewTokenFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.OneRttPacket; +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.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.HandshakeState; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +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.original_destination_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.retry_source_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.stateless_reset_token; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.version_information; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +public final class QuicServerConnection extends QuicConnectionImpl { + private static final AtomicLong CONNECTIONS = new AtomicLong(); + private final QuicVersion preferredQuicVersion; + private volatile boolean connectionIdAcknowledged; + private final QuicServer server; + private final byte[] clientInitialToken; + private final QuicConnectionId clientSentDestConnId; + private final QuicConnectionId originalServerConnId; + private final QuicServer.RetryData retryData; + private final AtomicBoolean firstHandshakePktProcessed = new AtomicBoolean(); + + public static final boolean FILTER_SENDER_ADDRESS = Utils.getBooleanProperty( + "test.quic.server.filterSenderAddress", true); + + QuicServerConnection(QuicServer server, + QuicVersion quicVersion, + QuicVersion preferredQuicVersion, + InetSocketAddress peerAddress, + QuicConnectionId clientSentDestConnId, + SSLParameters sslParameters, + byte[] initialToken) { + this(server, quicVersion, preferredQuicVersion, peerAddress, clientSentDestConnId, + sslParameters, initialToken, null); + + } + + QuicServerConnection(QuicServer server, + QuicVersion quicVersion, + QuicVersion preferredQuicVersion, + InetSocketAddress peerAddress, + QuicConnectionId clientSentDestConnId, + SSLParameters sslParameters, + byte[] initialToken, + QuicServer.RetryData retryData) { + super(quicVersion, server, peerAddress, null, -1, sslParameters, + "QuicServerConnection(%s)", CONNECTIONS.incrementAndGet()); + this.preferredQuicVersion = preferredQuicVersion; + // this should have been first statement in this constructor but compiler doesn't allow it + Objects.requireNonNull(quicVersion, "quic version"); + this.clientInitialToken = initialToken; + this.server = server; + this.clientSentDestConnId = clientSentDestConnId; + this.retryData = retryData; + this.originalServerConnId = retryData == null ? clientSentDestConnId : retryData.originalServerConnId(); + handshakeFlow().handshakeCF().thenAccept((hs) -> { + try { + onHandshakeCompletion(hs); + } catch (Exception e) { + // TODO: consider if this needs to be propagated somehow. for now just log + System.err.println("onHandshakeCompletion() failed: " + e); + e.printStackTrace(); + } + }); + assert quicVersion == quicVersion() : "unexpected quic version on" + + " server connection, expected " + quicVersion + " but found " + quicVersion(); + try { + getTLSEngine().deriveInitialKeys(quicVersion, clientSentDestConnId.asReadOnlyBuffer()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected QuicConnectionId originalServerConnId() { + return this.originalServerConnId; + } + + @Override + protected boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return Arrays.equals(clientInitialToken, token); + } + + @Override + public List connectionIds() { + var connectionIds = super.connectionIds(); + // we can stop using the original connection id if we have + // received the ClientHello fully. + // TODO: find when/where to switch connectionIdAcknowledged to true + // TODO: what if the ClientHello is in 3 initial packets and the packet number 2 + // gets lost? How do we know? I guess we can assume that the client hello + // was fully receive when we send (or receive) the first handshake packet. + if (!connectionIdAcknowledged) { + // Add client's initial connection ID (original or retry) + QuicConnectionId initial = this.clientSentDestConnId; + connectionIds = Stream.concat(connectionIds.stream(), Stream.of(initial)).toList(); + } + return connectionIds; + } + + @Override + public Optional initialConnectionId() { + return Optional.ofNullable(clientSentDestConnId); + } + + @Override + public void processIncoming(final SocketAddress source, final ByteBuffer destConnId, + final QuicPacket.HeadersType headersType, final ByteBuffer buffer) { + // consult the delivery policy if this packet should be dropped + if (this.server.incomingDeliveryPolicy().shouldDrop(source, buffer, this, headersType)) { + silentIgnorePacket(source, buffer, headersType, false, "incoming delivery policy"); + return; + } + if (!connectionIdAcknowledged && localConnectionId().matches(destConnId)) { + debug.log("connection acknowledged"); + connectionIdAcknowledged = true; + server.connectionAcknowledged(this, clientSentDestConnId, localConnectionId()); + endpoint.removeConnectionId(clientSentDestConnId, this); + } + super.processIncoming(source, destConnId, headersType, buffer); + } + + @Override + public boolean accepts(SocketAddress source) { + if (FILTER_SENDER_ADDRESS && !source.equals(peerAddress())) { + // We do not support path migration yet, so 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; + } + + @Override + protected void processRetryPacket(final QuicPacket quicPacket) { + // server is not supposed to receive retry packet: + // ignore it? + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.RETRY) { + throw new IllegalArgumentException("Not a RETRY packet: " + quicPacket.packetType()); + } + if (Log.errors()) { + Log.logError("Server received RETRY packet - discarding it"); + } + } + + @Override + protected void incoming1RTTFrame(HandshakeDoneFrame frame) throws QuicTransportException { + // RFC-9000, section 19.20: A HANDSHAKE_DONE frame can only be sent by + // the server. ... A server MUST treat receipt of a HANDSHAKE_DONE frame + // as a connection error of type PROTOCOL_VIOLATION + throw new QuicTransportException("HANDSHAKE_DONE frame isn't allowed from clients", + null, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + @Override + protected void incoming1RTTFrame(NewTokenFrame frame) throws QuicTransportException { + // This is a server connection and as per RFC-9000, section 19.7, clients + // aren't supposed to send NEW_TOKEN frames and if a server receives such + // a frame then it is considered a connection error + // of type PROTOCOL_VIOLATION. + throw new QuicTransportException("NEW_TOKEN frame isn't allowed from clients", + null, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + @Override + protected void pushDatagram(final SocketAddress destination, final ByteBuffer datagram) { + final QuicPacket.HeadersType headersType = QuicPacketDecoder.peekHeaderType(datagram, + datagram.position()); + // consult the delivery policy if this packet should be dropped + if (this.server.outgoingDeliveryPolicy().shouldDrop(destination, datagram, + this, headersType)) { + silentIgnorePacket(destination, datagram, headersType, true, "outgoing delivery policy"); + return; + } + super.pushDatagram(destination, datagram); + } + + @Override + protected void processInitialPacket(final QuicPacket quicPacket) { + try { + if (!(quicPacket instanceof InitialPacket initialPacket)) { + throw new AssertionError("Bad packet type: " + quicPacket); + } + updatePeerConnectionId(initialPacket); + var initialPayloadLength = initialPacket.payloadSize(); + assert initialPayloadLength < Integer.MAX_VALUE; + if (debug.on()) { + debug.log("Initial payload (count=%d, remaining=%d)", + initialPacket.frames().size(), initialPayloadLength); + } + long total = processInitialPacketPayload(initialPacket); + assert total == initialPayloadLength; + if (initialPacket.frames().stream().anyMatch(f -> f instanceof CryptoFrame)) { + debug.log("ClientHello received"); + } + var hsState = getTLSEngine().getHandshakeState(); + debug.log("hsState: " + hsState); + if (hsState == QuicTLSEngine.HandshakeState.NEED_SEND_CRYPTO) { + debug.log("Continuing handshake"); + continueHandshake(); + } else if (quicPacket.isAckEliciting() && + getTLSEngine().getCurrentSendKeySpace() == QuicTLSEngine.KeySpace.HANDSHAKE) { + packetNumberSpaces().initial().fastRetransmit(); + } + } catch (Throwable t) { + debug.log("Unexpected exception handling initial packet", t); + connectionTerminator().terminate(TerminationCause.forException(t)); + } + } + + @Override + protected void processHandshakePacket(final QuicPacket quicPacket) { + super.processHandshakePacket(quicPacket); + if (this.firstHandshakePktProcessed.compareAndSet(false, true)) { + // close INITIAL packet space and discard INITIAL keys as expected by + // RFC-9001, section 4.9.1: ... a server MUST discard Initial keys when + // it first successfully processes a Handshake packet. Endpoints MUST NOT send + // Initial packets after this point. + if (debug.on()) { + debug.log("server processed first handshake packet, initiating close of" + + " INITIAL packet space"); + } + packetNumberSpaces().initial().close(); + } + QuicTLSEngine engine = getTLSEngine(); + switch (engine.getHandshakeState()) { + case NEED_SEND_HANDSHAKE_DONE -> { + // should ack handshake and possibly send HandshakeDoneFrame + // the HANDSHAKE space will be closed after sending the + // HANDSHAKE_DONE frame (see sendStreamData) + packetSpace(PacketNumberSpace.HANDSHAKE).runTransmitter(); + engine.tryMarkHandshakeDone(); + enqueue1RTTFrame(new HandshakeDoneFrame()); + debug.log("Adding HandshakeDoneFrame"); + completeHandshakeCF(); + packetSpace(PacketNumberSpace.APPLICATION).runTransmitter(); + } + } + } + + @Override + protected void completeHandshakeCF() { + completeHandshakeCF(null); + } + + @Override + protected void send1RTTPacket(final OneRttPacket packet) + throws QuicKeyUnavailableException, QuicTransportException { + boolean closeHandshake = false; + var handshakeSpace = packetNumberSpaces().handshake(); + if (!handshakeSpace.isClosed()) { + closeHandshake = packet.frames() + .stream() + .anyMatch(HandshakeDoneFrame.class::isInstance); + } + super.send1RTTPacket(packet); + if (closeHandshake) { + // close handshake space after sending + // HANDSHAKE_DONE + handshakeSpace.close(); + } + } + + /** + * This method can be invoked if a certain {@code action} needs to be performed, + * by this server connection, on a successful completion of Quic connection handshake + * (initiated by a client). + * + * @param action The action to be performed on successful completion of the handshake + */ + public void onSuccessfulHandshake(final Runnable action) { + this.handshakeFlow().handshakeCF().thenRun(action); + } + + /** + * This method can be invoked if a certain {@code action} needs to be performed, + * by this server connection, when a Quic connection handshake (initiated by a client), completes. + * The handshake could either have succeeded or failed. If the handshake succeeded, then the + * {@code Throwable} passed to the {@code action} will be {@code null}, else it will represent + * the handshake failure. + * + * @param action The action to be performed on completion of the handshake + */ + public void onHandshakeCompletion(final Consumer action) { + this.handshakeFlow().handshakeCF().handle((unused, failure) -> { + action.accept(failure); + return null; + }); + } + + + @Override + public QuicServer quicInstance() { + return server; + } + + @Override + protected ByteBuffer buildInitialParameters() { + final QuicTransportParameters params = new QuicTransportParameters(this.transportParams); + if (!params.isPresent(original_destination_connection_id)) { + params.setParameter(original_destination_connection_id, originalServerConnId.getBytes()); + } + if (!params.isPresent(initial_source_connection_id)) { + params.setParameter(initial_source_connection_id, localConnectionId().getBytes()); + } + if (!params.isPresent(stateless_reset_token)) { + params.setParameter(stateless_reset_token, + endpoint.idFactory().statelessTokenFor(localConnectionId())); + } + if (!params.isPresent(max_idle_timeout)) { + final long idleTimeoutMillis = TimeUnit.SECONDS.toMillis( + Utils.getLongProperty("jdk.test.server.quic.idleTimeout", 30)); + params.setIntParameter(max_idle_timeout, idleTimeoutMillis); + } + if (retryData != null && !params.isPresent(retry_source_connection_id)) { + // include the connection id that was directed by this server's RETRY packet + // for usage in INITIAL packets sent by client + params.setParameter(retry_source_connection_id, + retryData.serverChosenConnId().getBytes()); + } + 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_bidi_remote, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_uni, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_streams_bidi, () -> (long) DEFAULT_MAX_BIDI_STREAMS); + setIntParamIfNotSet(params, initial_max_streams_uni, () -> (long) DEFAULT_MAX_UNI_STREAMS); + // params.setParameter(QuicTransportParameters.ParameterId.stateless_reset_token, ...); // no token + // params.setIntParameter(QuicTransportParameters.ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(QuicTransportParameters.ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(QuicTransportParameters.ParameterId.disable_active_migration, false); + // params.setPreferedAddressParameter(QuicTransportParameters.ParameterId.preferred_address, ...); + // params.setIntParameter(QuicTransportParameters.ParameterId.active_connection_id_limit, 2); + if (!params.isPresent(version_information)) { + final VersionInformation vi = QuicTransportParameters.buildVersionInformation( + quicVersion(), quicInstance().getAvailableVersions()); + params.setVersionInformationParameter(version_information, vi); + } + final byte[] unsupportedTransportParam = encodeRandomUnsupportedTransportParameter(); + final int capacity = params.size() + unsupportedTransportParam.length; + final ByteBuffer buf = ByteBuffer.allocate(capacity); + params.encode(buf); + // add an unsupported transport param id so that we can exercise the case where endpoints + // are expected to ignore unsupported transport parameters (RFC-9000, section 7.4.2) + buf.put(unsupportedTransportParam); + buf.flip(); + newLocalTransportParameters(params); + return buf; + } + + // returns the encoded representation of a random unsupported transport parameter + private static byte[] encodeRandomUnsupportedTransportParameter() { + final int n = new Random().nextInt(1, 100); + final long unsupportedParamId = 31 * n + 27; // RFC-9000, section 18.1 + final int value = 42; + // Transport Parameter { + // Transport Parameter ID (i), + // Transport Parameter Length (i), + // Transport Parameter Value (..), + // } + int size = 0; + size += VariableLengthEncoder.getEncodedSize(unsupportedParamId); + final int paramLength = VariableLengthEncoder.getEncodedSize(value); + size += VariableLengthEncoder.getEncodedSize(paramLength); + size += paramLength; + final byte[] encoded = new byte[size]; + final ByteBuffer buf = ByteBuffer.wrap(encoded); + + VariableLengthEncoder.encode(buf, unsupportedParamId); // write out the id, as a variable length integer + VariableLengthEncoder.encode(buf, paramLength); // write out the len, as a variable length integer + VariableLengthEncoder.encode(buf, value); // write out the actual value + return encoded; + } + + @Override + protected void consumeQuicParameters(ByteBuffer byteBuffer) + throws QuicTransportException { + final QuicTransportParameters params = QuicTransportParameters.decode(byteBuffer); + if (debug.on()) { + debug.log("Received (from client) Quic transport params: " + params.toStringWithValues()); + } + if (params.isPresent(retry_source_connection_id)) { + throw new QuicTransportException("Retry connection ID not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(original_destination_connection_id)) { + throw new QuicTransportException("Original connection ID not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(stateless_reset_token)) { + throw new QuicTransportException("Reset token not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(ParameterId.preferred_address)) { + throw new QuicTransportException("Preferred address not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (!params.matches(initial_source_connection_id, + getIncomingInitialPacketSourceId())) { + throw new QuicTransportException("Peer connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + VersionInformation vi = + params.getVersionInformationParameter(version_information); + if (vi != null) { + boolean found = false; + for (int v: vi.availableVersions()) { + if (v == vi.chosenVersion()) { + found = true; + break; + } + } + if (!found) { + throw new QuicTransportException( + "[version_information] Chosen Version not in Available Versions", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (vi.chosenVersion() != quicVersion().versionNumber()) { + throw new QuicTransportException( + "[version_information] Chosen Version %s does not match version in use %s" + .formatted(vi.chosenVersion(), quicVersion().versionNumber()), + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + assert Arrays.stream(vi.availableVersions()).anyMatch(v -> v == preferredQuicVersion.versionNumber()); + if (preferredQuicVersion != quicVersion()) { + if (!switchVersion(preferredQuicVersion)) { + throw new QuicTransportException("Switching version failed", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + } else { + assert preferredQuicVersion == quicVersion(); + } + markVersionNegotiated(preferredQuicVersion.versionNumber()); + handleIncomingPeerTransportParams(params); + + // build our parameters + final ByteBuffer quicInitialParameters = buildInitialParameters(); + getTLSEngine().setLocalQuicTransportParameters(quicInitialParameters); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_data, DEFAULT_INITIAL_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_local, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_stream_data_uni, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_streams_bidi, DEFAULT_MAX_STREAMS); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_streams_uni, DEFAULT_MAX_STREAMS); + // params.setIntParameter(QuicTransportParameters.ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(QuicTransportParameters.ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(QuicTransportParameters.ParameterId.disable_active_migration, false); + // params.setIntParameter(QuicTransportParameters.ParameterId.active_connection_id_limit, 2); + } + + @Override + protected void processVersionNegotiationPacket(final QuicPacket quicPacket) { + // ignore the packet: the server doesn't reply to version negotiation. + debug.log("Server ignores version negotiation packet: " + quicPacket); + } + + @Override + public boolean isClientConnection() { + return false; + } + + @Override + protected QuicEndpoint onHandshakeCompletion(HandshakeState result) { + super.onHandshakeCompletion(result); + // send a new token frame to the client, for use in new connection attempts. + sendNewToken(); + return this.endpoint; + } + + private void sendNewToken() { + final byte[] token = server.buildNewToken(); + final QuicFrame newTokenFrame = new NewTokenFrame(ByteBuffer.wrap(token)); + enqueue1RTTFrame(newTokenFrame); + } + + @Override + public long nextMaxStreamsLimit(final boolean bidi) { + final Function limitComputer = this.server.getMaxStreamLimitComputer(); + if (limitComputer != null) { + return limitComputer.apply(bidi); + } + return super.nextMaxStreamsLimit(bidi); + } + + private void silentIgnorePacket(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType, + final boolean outgoing, + final String reason) { + if (debug.on()) { + debug.log("silently dropping %s packet %s %s, reason: %s", headersType, + (outgoing ? "to dest" : "from source"), source, reason); + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java new file mode 100644 index 00000000000..886f15821a6 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java @@ -0,0 +1,87 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.SocketAddress; + +import jdk.internal.net.http.quic.streams.QuicBidiStream; + +/** + * Used by server side application code to handle incoming Quic connections and streams associated + * with those connections + * + * @see QuicStandaloneServer#addHandler(QuicServerHandler) + */ +// TODO: should we make this an abstract class instead of interface? +public interface QuicServerHandler { + + /** + * @param source The (client) source of the incoming connection + * @param serverConn The {@link QuicServerConnection}, constructed on the server side, + * representing in the incoming connection + * {@return true if the incoming connection should be accepted. false otherwise} + */ + default boolean acceptIncomingConnection(final SocketAddress source, + final QuicServerConnection serverConn) { + // by default accept new connections + return true; + } + + /** + * Called whenever a client initiated bidirectional stream has been received on the + * Quic connection which this {@code QuicServerHandler} previously + * {@link #acceptIncomingConnection(SocketAddress, QuicServerConnection) accepted} + * + * @param conn The connection on which the stream was created + * @param bidi The client initiated bidirectional stream + * @throws IOException + */ + default void onClientInitiatedBidiStream(final QuicServerConnection conn, + final QuicBidiStream bidi) throws IOException { + // start the reader/writer loops for this stream, by creating a ConnectedBidiStream + try (final ConnectedBidiStream connectedBidiStream = new ConnectedBidiStream(bidi)) { + // let the handler use this connected stream to read/write data, if it wants to + this.handleBidiStream(conn, connectedBidiStream); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + + } + + /** + * Called whenever a client initiated bidirectional stream has been received on the connection + * previously accepted by this handler. This method is called from within + * {@link #onClientInitiatedBidiStream(QuicBidiStream)} with a {@code ConnectedBidiStream} which + * has the reader and writer tasks started. + * + * @param conn The connection on which the stream was created + * @param bidiStream The bidirectional stream which has the reader and writer tasks started + * @throws IOException + */ + void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException; + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java new file mode 100644 index 00000000000..b1697064556 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java @@ -0,0 +1,185 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongFunction; + +import javax.net.ssl.SNIMatcher; +import javax.net.ssl.SSLContext; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +public final class QuicStandaloneServer extends QuicServer { + public static final String ALPN = "quic-standalone-test-alpn"; + + private static final AtomicLong IDS = new AtomicLong(); + + private final Logger debug = Utils.getDebugLogger(this::name); + private QuicServerHandler handler; + + private static String nextName() { + return "quic-standalone-server-" + IDS.incrementAndGet(); + } + + QuicStandaloneServer(String serverId, InetSocketAddress bindAddress, + ExecutorService executor, QuicVersion[] supportedQuicVersions, + boolean compatible, QuicTLSContext quicTLSContext, SNIMatcher sniMatcher, + DatagramDeliveryPolicy incomingDeliveryPolicy, + DatagramDeliveryPolicy outgoingDeliveryPolicy, String alpn, + LongFunction appErrorCodeToString) { + super(serverId, bindAddress, executor, supportedQuicVersions, compatible, quicTLSContext, sniMatcher, + incomingDeliveryPolicy, outgoingDeliveryPolicy, alpn, appErrorCodeToString); + // set a connection acceptor + setConnectionAcceptor(QuicStandaloneServer::acceptIncoming); + } + + public void addHandler(final QuicServerHandler handler) { + this.handler = handler; + } + + QuicServerHandler getHandler() { + return this.handler; + } + + static boolean acceptIncoming(final SocketAddress source, final QuicServerConnection serverConn) { + try { + final QuicStandaloneServer server = (QuicStandaloneServer) serverConn.quicInstance(); + final QuicServerHandler handler = server.getHandler(); + if (handler == null) { + if (server.debug.on()) { + server.debug.log("Handler absent - rejecting new connection " + + serverConn + " from source " + source); + } + return false; + } + if (!handler.acceptIncomingConnection(source, serverConn)) { + if (server.debug.on()) { + server.debug.log("Handler " + handler + " rejected new connection " + + serverConn + " from source " + source); + } + return false; + } + if (server.debug.on()) { + server.debug.log("New connection " + serverConn + " accepted from " + source); + } + serverConn.onSuccessfulHandshake(() -> { + if (server.debug.on()) { + server.debug.log("Registering a listener for remote streams on connection " + + serverConn); + } + // add a listener for streams that have been created by the remote side + // (i.e. initiated by the client) + serverConn.addRemoteStreamListener((stream) -> { + if (stream.isBidirectional()) { + // invoke the handler (application code) as a async work + server.asyncHandleBidiStream(source, serverConn, (QuicBidiStream) stream); + return true; // true implies that this listener wishes to acquire the stream + } else { + if (server.debug.on()) { + server.debug.log("Ignoring stream " + stream + " on connection " + serverConn); + } + return false; + } + }); + }); + return true; // true implies we wish to accept this incoming connection + } catch (Throwable t) { + // TODO: re-evaluate why this try/catch block is there. it's likely + // that in the absence of this block, the call just "disappears"/hangs when an exception + // occurs in this method + System.err.println("Exception while accepting incoming connection: " + t.getMessage()); + t.printStackTrace(); + return false; + } + } + + private void asyncHandleBidiStream(final SocketAddress source, final QuicServerConnection serverConn, + final QuicBidiStream stream) { + this.executor().execute(() -> { + try { + if (debug.on()) { + debug.log("Invoking handler " + handler + " for handling bidi stream " + + stream + " on connection " + serverConn); + } + handler.onClientInitiatedBidiStream(serverConn, stream); + } catch (Throwable t) { + System.err.println("Failed to handle client initiated" + + " bidi stream for connection " + serverConn + + " from source " + source); + t.printStackTrace(); + } + }); + } + + public static Builder newBuilder() { + return new StandaloneBuilder(); + } + + private static final class StandaloneBuilder extends Builder { + + public StandaloneBuilder() { + this.alpn = ALPN; + } + + @Override + public QuicStandaloneServer build() throws IOException { + QuicVersion[] versions = availableQuicVersions; + if (versions == null) { + // default to v1 and v2 + versions = new QuicVersion[]{QuicVersion.QUIC_V1, QuicVersion.QUIC_V2}; + } + if (versions.length == 0) { + throw new IllegalStateException("Empty supported QUIC versions"); + } + InetSocketAddress addr = bindAddress; + if (addr == null) { + // default to loopback address and ephemeral port + addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + } + SSLContext ctx = sslContext; + if (ctx == null) { + try { + ctx = SSLContext.getDefault(); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(ctx); + final String name = serverId == null ? nextName() : serverId; + return new QuicStandaloneServer(name, addr, executor, versions, compatible, quicTLSContext, + sniMatcher, incomingDeliveryPolicy, outgoingDeliveryPolicy, alpn, appErrorCodeToString); + } + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java new file mode 100644 index 00000000000..c627ed52fab --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java @@ -0,0 +1,82 @@ +/* + * 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. + * + * 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.httpclient.test.lib.quic; + +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class RetryCodingContext implements CodingContext { + private final QuicConnectionId connectionId; + private final QuicTLSEngine engine; + + public RetryCodingContext(QuicConnectionId connectionId, QuicTLSContext quicTLSContext) { + this.connectionId = connectionId; + engine = quicTLSContext.createEngine(); + } + + @Override + public long largestProcessedPN(QuicPacket.PacketNumberSpace packetSpace) { + throw new UnsupportedOperationException(); + } + + @Override + public long largestAckedPN(QuicPacket.PacketNumberSpace packetSpace) { + throw new UnsupportedOperationException(); + } + + @Override + public int connectionIdLength() { + throw new UnsupportedOperationException(); + } + + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public QuicConnectionId originalServerConnId() { + return connectionId; + } + + @Override + public QuicTLSEngine getTLSEngine() { + return engine; + } + + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + throw new UnsupportedOperationException(); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java b/test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java new file mode 100644 index 00000000000..c588f8117ae --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java @@ -0,0 +1,374 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertNotEquals; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA BlockingDecodingTest + */ + + +public class BlockingDecodingTest { + @Test + public void blockedStreamsSettingDefaultValueTest() throws Exception { + // Default SETTINGS_QPACK_BLOCKED_STREAMS value (0) doesn't allow blocked streams + var encoderEh = new TestErrorHandler(); + var decoderEh = new TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair pair = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, + streamError, -1L, 1); + + // Get Encoder and Decoder instances from a newly established connector + var encoder = pair.encoder(); + var decoder = pair.decoder(); + + // Create a decoding callback to check for completion and to log failures + TestDecodingCallback decodingCallback = new TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + var header = TestHeader.withId(0); + encoder.header(context, header.name(), header.value(), + false, IGNORE_RECEIVED_COUNT_CHECK); + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + + // It is expected to get QPACK_DECOMPRESSION_FAILED here since decoder is + // expected to be blocked due to missing entry with index 0 in the decoder table, + // and the default number of blocked streams (0) will be exceeded (1). + var lastHttp3Error = decodingCallback.lastHttp3Error.get(); + System.err.println("Last Http3Error: " + lastHttp3Error); + Assert.assertEquals(lastHttp3Error, Http3Error.QPACK_DECOMPRESSION_FAILED); + Assert.assertFalse(decodingCallback.completed.isDone()); + } + + @Test + public void noBlockedStreamsTest() throws Exception { + // No blocked streams - with default SETTINGS_QPACK_BLOCKED_STREAMS value + // Default SETTINGS_QPACK_BLOCKED_STREAMS value (0) doesn't allow blocked streams + var encoderEh = new TestErrorHandler(); + var decoderEh = new TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair pair = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, + streamError, -1L, 1); + + // Populate decoder table with an entry - there should be no blocked streams + // observed during decoding + prepopulateDynamicTable(pair.decoderTable(), 1); + + // Get Encoder and Decoder instances from a newly established connector + var encoder = pair.encoder(); + var decoder = pair.decoder(); + + // Create a decoding callback to check for completion and to log failures + TestDecodingCallback decodingCallback = new TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + var expectedHeader = TestHeader.withId(0); + encoder.header(context, expectedHeader.name, + expectedHeader.value, false, IGNORE_RECEIVED_COUNT_CHECK); + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + + // It is expected to get QPACK_DECOMPRESSION_FAILED here since decoder is + // expected to be blocked due to missing entry with index 0 in the decoder table, + // and the default number of blocked streams (0) will be exceeded (1). + var lastHttp3Error = decodingCallback.lastHttp3Error.get(); + System.err.println("Last Http3Error: " + lastHttp3Error); + Assert.assertNull(lastHttp3Error); + Assert.assertNull(decodingCallback.lastThrowable.get()); + Assert.assertTrue(decodingCallback.completed.isDone()); + // Check that onDecoded was called for the test entry + var decodedHeader = decodingCallback.decodedHeaders.get(0); + Assert.assertEquals(decodedHeader, expectedHeader); + } + + @Test + public void awaitBlockedStreamsTest() throws Exception { + // Max number of blocked streams is not exceeded + // No blocked streams - with default SETTINGS_QPACK_BLOCKED_STREAMS value + // Default SETTINGS_QPACK_BLOCKED_STREAMS value (0) doesn't allow blocked streams + final int numberOfMaxAllowedBlockedStreams = 5; + final int numberOfHeaders = 4; + final int base = 2; + var encoderEh = new TestErrorHandler(); + var decoderEh = new TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair pair = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, + streamError, numberOfMaxAllowedBlockedStreams, numberOfHeaders); + + // Create list of headers to encode for each thread + List expectedHeaders = Collections.synchronizedList(new ArrayList<>()); + for (int headerId = 0; headerId < numberOfHeaders; headerId++) { + expectedHeaders.add(TestHeader.withId(headerId)); + } + + // Create virtual threads executor + var vtExecutor = Executors.newVirtualThreadPerTaskExecutor(); + List> decodingTaskResults = new ArrayList<>(); + + // Create 10 blocked tasks + for (int taskCount = 0; taskCount < numberOfMaxAllowedBlockedStreams; taskCount++) { + var decodingTask = new Callable() { + final EncoderDecoderConnector.EncoderDecoderPair ed = pair; + @Override + public TestDecodingCallback call() throws Exception { + var encoder = ed.encoder(); + var decoder = ed.decoder(); + // Create a decoding callback to check for completion and to log failures + TestDecodingCallback decodingCallback = new TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = + encoder.newEncodingContext(0, base, headerFrameWriter); + + for (var header : expectedHeaders) { + encoder.header(context, header.name, header.value, false, + IGNORE_RECEIVED_COUNT_CHECK); + headerFrameWriter.write(headersBb); + } + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + return decodingCallback; + } + }; + decodingTaskResults.add(vtExecutor.submit(decodingTask)); + } + + // Schedule the delayed update to the decoders dynamic table + var delayedExecutor = CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS, + vtExecutor); + AtomicLong updateDoneTimestamp = new AtomicLong(); + delayedExecutor.execute(() -> { + updateDoneTimestamp.set(System.nanoTime()); + prepopulateDynamicTable(pair.decoderTable(), numberOfHeaders); + }); + + // Await completion of all tasks + for (var decodingResultFuture : decodingTaskResults) { + decodingResultFuture.get().completed.get(); + } + // Acquire the timestamp + long updateDoneTimeStamp = updateDoneTimestamp.get(); + + System.err.println("All decoding tasks are done"); + System.err.println("Decoder table update timestamp: " + updateDoneTimeStamp); + // Check results of each decoding task + for (var decodingResultFuture : decodingTaskResults) { + var taskCallback = decodingResultFuture.get(); + Assert.assertNull(taskCallback.lastHttp3Error.get()); + Assert.assertNull(taskCallback.lastThrowable.get()); + long decodingTaskCompleted = taskCallback.completedTimestamp.get(); + System.err.println("Decoding task completion timestamp: " + decodingTaskCompleted); + Assert.assertTrue(decodingTaskCompleted >= updateDoneTimeStamp); + var decodedHeaders = taskCallback.decodedHeaders; + Assert.assertEquals(decodedHeaders, expectedHeaders); + } + } + + private static EncoderDecoderConnector.EncoderDecoderPair newPreconfiguredEncoderDecoder( + TestErrorHandler encoderEh, + TestErrorHandler decoderEh, + AtomicReference streamError, + long maxBlockedStreams, + int numberOfEntriesInEncoderDT) { + EncoderDecoderConnector conn = new EncoderDecoderConnector(); + var pair = conn.newEncoderDecoderPair( + e -> false, + encoderEh::qpackErrorHandler, + decoderEh::qpackErrorHandler, + streamError::set); + // Create settings frame with dynamic table capacity and number of blocked streams + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + // 4k should be enough for storing dynamic table entries added by 'prepopulateDynamicTable' + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DT_CAPACITY); + if (maxBlockedStreams > 0) { + // Set max number of blocked decoder streams if the provided value is positive, otherwise + // use the default RFC setting which is 0 + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS, maxBlockedStreams); + } + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder with constructed ConnectionSettings + pair.encoder().configure(settings); + pair.decoder().configure(settings); + pair.encoderTable().setCapacity(DT_CAPACITY); + pair.decoderTable().setCapacity(DT_CAPACITY); + + // Prepopulate encoder dynamic table with test entries. Decoder dynamic table will be pre-populated with + // a test-case specific code to reproduce blocked decoding scenario + prepopulateDynamicTable(pair.encoderTable(), numberOfEntriesInEncoderDT); + + return pair; + } + + private static void prepopulateDynamicTable(DynamicTable dynamicTable, int numEntries) { + for (int count = 0; count < numEntries; count++) { + var header = TestHeader.withId(count); + dynamicTable.insert(header.name(), header.value()); + } + } + + private static class TestDecodingCallback implements DecodingCallback { + + final List decodedHeaders = new CopyOnWriteArrayList<>(); + final CompletableFuture completed = new CompletableFuture<>(); + final AtomicLong completedTimestamp = new AtomicLong(); + + final AtomicReference lastThrowable = new AtomicReference<>(); + final AtomicReference lastHttp3Error = new AtomicReference<>(); + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + var nameValue = new TestHeader(name.toString(), value.toString()); + decodedHeaders.add(nameValue); + System.err.println("Decoding callback 'onDecoded': " + nameValue); + } + + @Override + public void onComplete() { + System.err.println("Decoding callback 'onComplete'"); + completedTimestamp.set(System.nanoTime()); + completed.complete(null); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + System.err.println("Decoding callback 'onError': " + http3Error); + lastThrowable.set(throwable); + lastHttp3Error.set(http3Error); + } + + @Override + public long streamId() { + return 0; + } + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + throw new RuntimeException("http3 error: " + http3Error, error); + } + } + + record TestHeader(String name, String value) { + public static TestHeader withId(int id) { + return new TestHeader(NAME + id, VALUE + id); + } + } + + private static final String NAME = "test"; + private static final String VALUE = "valueTest"; + private static final long DT_CAPACITY = 4096L; + private static final long IGNORE_RECEIVED_COUNT_CHECK = -1L; +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java new file mode 100644 index 00000000000..e231d0dded3 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java @@ -0,0 +1,165 @@ +/* + * 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. + * + * 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 + * @key randomness + * @library /test/lib + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL DecoderInstructionsReaderTest + */ + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.RepeatedTest; + +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DecoderInstructionsReaderTest { + + DecoderInstructionsReader decoderInstructionsReader; + private static final Random RANDOM = RandomFactory.getRandom(); + + @RepeatedTest(10) + public void acknowledgementTest() { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Stream ID (7+) | + // +---+---------------------------+ + + TestDecoderInstructionsCallback callback = new TestDecoderInstructionsCallback(); + decoderInstructionsReader = new DecoderInstructionsReader(callback, QPACK.getLogger()); + + long streamId = RANDOM.nextLong(0, IntegerReader.QPACK_MAX_INTEGER_VALUE); + IntegerWriter writer = new IntegerWriter(); + int bufferSize = requiredBufferSize(7, streamId); + var payload = 0b1000_0000; + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + writer.configure(streamId, 7, payload); + writer.write(byteBuffer); + byteBuffer.flip(); + + decoderInstructionsReader.read(byteBuffer); + assertEquals(streamId, callback.lastSectionAckStreamId.get()); + } + + @RepeatedTest(10) + public void cancellationTest() { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + + TestDecoderInstructionsCallback callback = new TestDecoderInstructionsCallback(); + decoderInstructionsReader = new DecoderInstructionsReader(callback, QPACK.getLogger()); + + long streamId = RANDOM.nextLong(0, IntegerReader.QPACK_MAX_INTEGER_VALUE); + IntegerWriter writer = new IntegerWriter(); + int bufferSize = requiredBufferSize(6, streamId); + var payload = 0b0100_0000; + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + writer.configure(streamId, 6, payload); + writer.write(byteBuffer); + byteBuffer.flip(); + + decoderInstructionsReader.read(byteBuffer); + assertEquals(streamId, callback.lastCancelStreamId.get()); + } + + @RepeatedTest(10) + public void incrementTest() { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | Increment (6+) | + // +---+---+-----------------------+ + + TestDecoderInstructionsCallback callback = new TestDecoderInstructionsCallback(); + decoderInstructionsReader = new DecoderInstructionsReader(callback, QPACK.getLogger()); + + long increaseCountInc = RANDOM.nextLong(0, IntegerReader.QPACK_MAX_INTEGER_VALUE); + IntegerWriter writer = new IntegerWriter(); + int bufferSize = requiredBufferSize(6, increaseCountInc); + var payload = 0b0000_0000; + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + writer.configure(increaseCountInc, 6, payload); + writer.write(byteBuffer); + byteBuffer.flip(); + + decoderInstructionsReader.read(byteBuffer); + assertEquals(increaseCountInc, callback.lastInsertCountInc.get()); + + } + + 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; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + private static class TestDecoderInstructionsCallback implements DecoderInstructionsReader.Callback { + final AtomicLong lastSectionAckStreamId = new AtomicLong(-1L); + final AtomicLong lastCancelStreamId = new AtomicLong(-1L); + final AtomicLong lastInsertCountInc = new AtomicLong(-1L); + + @Override + public void onSectionAck(long streamId) { + lastSectionAckStreamId.set(streamId); + } + + @Override + public void onStreamCancel(long streamId) { + lastCancelStreamId.set(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + lastInsertCountInc.set(increment); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java new file mode 100644 index 00000000000..275dec34927 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java @@ -0,0 +1,177 @@ +/* + * 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. + * + * 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 + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL DecoderInstructionsWriterTest + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.DecoderInstructionsWriter; +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class DecoderInstructionsWriterTest { + + @ParameterizedTest + @MethodSource("decoderInstructionsSource") + public void decoderInstructionsTest(DecoderInstruction instruction, long value) throws Exception { + testRunner(instruction, value); + } + + private static Stream decoderInstructionsSource() { + // "Section Acknowledgment" + Stream sectionAck = RANDOM.longs(10, + 0, IntegerReader.QPACK_MAX_INTEGER_VALUE) + .boxed() + .map(l -> Arguments.of(DecoderInstruction.SECTION_ACK, l)); + + // "Stream Cancellation" + Stream streamCancel = RANDOM.longs(10, + 0, IntegerReader.QPACK_MAX_INTEGER_VALUE) + .boxed() + .map(l -> Arguments.of(DecoderInstruction.STREAM_CANCEL, l)); + + // "Insert Count Increment" + Stream insertCountInc = RANDOM.longs(10, + 0, IntegerReader.QPACK_MAX_INTEGER_VALUE) + .boxed() + .map(l -> Arguments.of(DecoderInstruction.INSERT_COUNT_INC, l)); + + return Stream.concat(sectionAck, Stream.concat(streamCancel, insertCountInc)); + } + + + enum DecoderInstruction { + SECTION_ACK, + STREAM_CANCEL, + INSERT_COUNT_INC; + } + + private static void testRunner(DecoderInstruction instruction, long value) throws Exception { + var writer = new DecoderInstructionsWriter(); + int calculatedInstructionSize = configureWriter(writer, instruction, value); + var logger = QPACK.getLogger(); + var dynamicTable = new DynamicTable(logger); + + var buffers = new ArrayList(); + + boolean writeDone = false; + int writtenBytes = 0; + // Write instruction to a byte buffers of a random size + while (!writeDone) { + int allocSize = RANDOM.nextInt(1, 9); + var buffer = ByteBuffer.allocate(allocSize); + + writeDone = writer.write(buffer); + writtenBytes += buffer.position(); + buffer.flip(); + buffers.add(buffer); + } + // Check that instruction size calculated by the writer matches + // the number of written bytes + assertEquals(writtenBytes, calculatedInstructionSize); + + // Read back the data from byte buffers + var callback = new TestDecoderInstructionsCallback(); + var reader = new DecoderInstructionsReader(callback, logger); + for (var bb : buffers) { + reader.read(bb); + } + // Check that reader callback values match values supplied to the writer + long instructionValue = extractCallbackValue(instruction, callback); + assertEquals(value, instructionValue); + } + + private static long extractCallbackValue(DecoderInstruction instruction, + TestDecoderInstructionsCallback callback) { + return switch (instruction) { + case SECTION_ACK -> callback.lastSectionAckStreamId.get(); + case STREAM_CANCEL -> callback.lastCancelStreamId.get(); + case INSERT_COUNT_INC -> callback.lastInsertCountInc.get(); + }; + } + + + private static int configureWriter(DecoderInstructionsWriter writer, + DecoderInstruction instruction, + long instructionValue) { + return switch (instruction) { + case SECTION_ACK -> writer.configureForSectionAck(instructionValue); + case STREAM_CANCEL -> writer.configureForStreamCancel(instructionValue); + case INSERT_COUNT_INC -> writer.configureForInsertCountInc(instructionValue); + }; + } + + private static final Random RANDOM = RandomFactory.getRandom(); + + private static class TestDecoderInstructionsCallback implements DecoderInstructionsReader.Callback { + final AtomicLong lastSectionAckStreamId = new AtomicLong(-1L); + final AtomicLong lastCancelStreamId = new AtomicLong(-1L); + final AtomicLong lastInsertCountInc = new AtomicLong(-1L); + + @Override + public void onSectionAck(long streamId) { + lastSectionAckStreamId.set(streamId); + } + + @Override + public void onStreamCancel(long streamId) { + lastCancelStreamId.set(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + lastInsertCountInc.set(increment); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java b/test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java new file mode 100644 index 00000000000..4b715a46e08 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java @@ -0,0 +1,265 @@ +/* + * 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. + * + * 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 + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL + * DecoderSectionSizeLimitTest + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import jdk.test.lib.RandomFactory; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; + +public class DecoderSectionSizeLimitTest { + @Test(dataProvider = "headerSequences") + public void fieldSectionSizeLimitExceeded(List headersSequence, + long maxFieldSectionSize) { + + boolean decoderErrorExpected = + maxFieldSectionSize > 0 && maxFieldSectionSize < REQUIRED_FIELD_SECTION_SIZE; + + System.err.println("=".repeat(50)); + System.err.println("Max Field Section Size = " + maxFieldSectionSize); + System.err.println("Max Field Section Size is" + (decoderErrorExpected ? " not" : "") + + " enough to encode headers"); + + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + DecoderSectionSizeLimitTest.TestErrorHandler encoderErrorHandler = + new DecoderSectionSizeLimitTest.TestErrorHandler(); + DecoderSectionSizeLimitTest.TestErrorHandler decoderErrorHandler = + new DecoderSectionSizeLimitTest.TestErrorHandler(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> false, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // This test emulates a scenario with an Encoder that doesn't respect + // the SETTINGS_MAX_FIELD_SECTION_SIZE setting value while encoding the headers frame + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 512L); + settingsFrame.setParameter(SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, maxFieldSectionSize); + ConnectionSettings decoderConnectionSetting = ConnectionSettings.createFrom(settingsFrame); + + // Encoder imposes no limit on the field section size + settingsFrame.setParameter(SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, -1L); + ConnectionSettings encoderConnectionSetting = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder + encoder.configure(encoderConnectionSetting); + decoder.configure(decoderConnectionSetting); + + // Configure dynamic tables + configureDynamicTable(conn.encoderTable()); + configureDynamicTable(conn.decoderTable()); + + // Encode headers + // Create header frame writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, BASE, headerFrameWriter); + + ByteBuffer buffer = ByteBuffer.allocate(RANDOM.nextInt(1, 65)); + List buffers = new ArrayList<>(); + for (TestHeader header : headersSequence) { + // Configures encoder for writing the header name:value pair + encoder.header(context, header.name, header.value, + false, -1L); + + // Write the header + while (!headerFrameWriter.write(buffer)) { + buffer.flip(); + buffers.add(buffer); + buffer = ByteBuffer.allocate(RANDOM.nextInt(1, 65)); + } + } + buffer.flip(); + buffers.add(buffer); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + System.err.println("Number of generated header buffers:" + buffers.size()); + + // Decode header buffers and check if expected HTTP/3 error is reported + // via decoding callback + var decodingCallback = new TestDecodingCallback(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + for (int bufferIdx = 0; bufferIdx < buffers.size(); bufferIdx++) { + headerFrameReader.read(buffers.get(bufferIdx), + bufferIdx == buffers.size() - 1); + Http3Error decodingError = decodingCallback.lastHttp3Error.get(); + if (decodingError != null) { + System.err.printf("Decoding error observed during buffer #%d processing: %s throwable: %s%n", + bufferIdx, decodingError, decodingCallback.lastThrowable.get()); + if (decoderErrorExpected) { + Assert.assertEquals(decodingError, Http3Error.QPACK_DECOMPRESSION_FAILED); + return; + } else { + Assert.fail("No HTTP/3 error was expected"); + } + } else { + System.err.println("Buffer #" + bufferIdx + " readout completed without errors"); + } + } + if (decoderErrorExpected) { + Assert.fail("HTTP/3 error was expected but was not observed"); + } + } + + @DataProvider + public Object[][] headerSequences() { + List testCases = new ArrayList<>(); + for (var sequence : generateHeaderSequences()) { + // Decoding should complete without failure + testCases.add(new Object[]{sequence, -1L}); + // No failure since it is enough bytes specified in the SETTINGS_MAX_FIELD_SECTION_SIZE + // setting value + testCases.add(new Object[]{sequence, REQUIRED_FIELD_SECTION_SIZE}); + // Failure is expected - not enough bytes specified in the SETTINGS_MAX_FIELD_SECTION_SIZE + // setting value + testCases.add(new Object[]{sequence, REQUIRED_FIELD_SECTION_SIZE - 1}); + } + return testCases.toArray(Object[][]::new); + } + + private static List> generateHeaderSequences() { + List> headersSequences = new ArrayList<>(); + headersSequences.add(TEST_HEADERS); + // startIndex == 0 - the TEST_HEADERS sequence that is already + // added to the sequences list + for (int startIndex = 1; startIndex < TEST_HEADERS.size(); startIndex++) { + List firstPart = TEST_HEADERS.subList(startIndex, TEST_HEADERS.size()); + List secondPart = TEST_HEADERS.subList(0, startIndex); + List sequence = new ArrayList<>(); + sequence.addAll(firstPart); + sequence.addAll(secondPart); + headersSequences.add(sequence); + } + return headersSequences; + } + + record TestHeader(String name, String value, long size) { + public TestHeader(String name, String value) { + this(name, value, name.length() + value.length() + 32L); + } + } + + private static void configureDynamicTable(DynamicTable table) { + table.setCapacity(512L); + table.insert(NAME_IN_TABLE, VALUE_IN_TABLE); + table.insert(NAME_IN_TABLE_POSTBASE, VALUE_IN_TABLE_POSTBASE); + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + } + } + + private static class TestDecodingCallback implements DecodingCallback { + + final AtomicReference lastHttp3Error = new AtomicReference<>(); + final AtomicReference lastThrowable = new AtomicReference<>(); + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + lastHttp3Error.set(http3Error); + lastThrowable.set(throwable); + } + + @Override + public long streamId() { + return 0; + } + } + + private static final String NAME_IN_TABLE = "HEADER_NAME_FROM_TABLE"; + private static final String VALUE_IN_TABLE = "HEADER_VALUE_FROM_TABLE"; + private static final String NAME_IN_TABLE_POSTBASE = "HEADER_NAME_FROM_TABLE_POSTBASE"; + private static final String VALUE_IN_TABLE_POSTBASE = "HEADER_VALUE_FROM_TABLE_POSTBASE"; + private static final String NAME_NOT_IN_TABLE = "NAME_NOT_IN_TABLE"; + private static final String VALUE_NOT_IN_TABLE = "VALUE_NOT_IN_TABLE"; + + private static List TEST_HEADERS = List.of( + // Relative index + new TestHeader(NAME_IN_TABLE, VALUE_IN_TABLE), + // Relative name index + new TestHeader(NAME_IN_TABLE, VALUE_NOT_IN_TABLE), + // Post-base index + new TestHeader(NAME_IN_TABLE_POSTBASE, VALUE_IN_TABLE_POSTBASE), + // Post-base name index + new TestHeader(NAME_IN_TABLE_POSTBASE, VALUE_NOT_IN_TABLE), + // Literal + new TestHeader(NAME_NOT_IN_TABLE, VALUE_NOT_IN_TABLE) + ); + + private static long REQUIRED_FIELD_SECTION_SIZE = TEST_HEADERS.stream() + .mapToLong(TestHeader::size) + .sum(); + private static final long BASE = 1L; + private static final Random RANDOM = RandomFactory.getRandom(); +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderTest.java b/test/jdk/java/net/httpclient/qpack/DecoderTest.java new file mode 100644 index 00000000000..37f87bdda59 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderTest.java @@ -0,0 +1,392 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.qpack.writers.StringWriter; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.testng.Assert.*; + +/* + * @test + * @modules java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng/othervm DecoderTest + */ +public class DecoderTest { + + private final Random random = new Random(); + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter stringWriter = new StringWriter(); + + record DecoderWithReader(Decoder decoder, HeaderFrameReader reader) { + } + + private static DecoderWithReader newDecoderWithReader(DecodingCallback decodingCallback) throws IOException { + var decoder = new Decoder(DecoderTest::createDecoderStreams, DecoderTest::qpackErrorHandler); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + // Supply byte buffer with two bytes of zeroed Field Section Prefix + ByteBuffer prefix = ByteBuffer.allocate(2); + decoder.decodeHeader(prefix, true, headerFrameReader); + // Return record with decoder/reader tuple + return new DecoderWithReader(decoder, headerFrameReader); + } + + private static final int TEST_STR_MAX_LENGTH = 10; + + static QueuingStreamPair createDecoderStreams(Consumer receiver) { + return null; + } + + @DataProvider(name = "indexProvider") + public Object[][] indexProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(headerField -> List.of(tableIndex.getAndIncrement(), headerField)) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider(name = "nameReferenceProvider") + public Object[][] nameReferenceProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(h -> List.of(tableIndex.getAndIncrement(), h.name(), randomString())) + .map(List::toArray).toArray(Object[][]::new); + } + + @DataProvider(name = "literalProvider") + public Object[][] literalProvider() { + var output = new String[100][]; + for (int i = 0; i < 100; i++) { + output[i] = new String[]{ randomString(), randomString() }; + } + return output; + } + + @Test(dataProvider = "indexProvider") + public void testIndexedOnStaticTable(int index, HeaderField h) throws IOException { + var actual = writeIndex(index); + var callback = new TestingCallBack(index, h.name(), h.value()); + var dr = newDecoderWithReader(callback); + dr.decoder().decodeHeader(actual, true, dr.reader()); + } + + @Test(dataProvider = "nameReferenceProvider") + public void testLiteralWithNameReferenceOnStaticTable(int index, String name, String value) throws IOException { + boolean sensitive = random.nextBoolean(); + + var actual = writeNameRef(index, sensitive, value); + var callback = new TestingCallBack(index, sensitive, name, value); + var dr = newDecoderWithReader(callback); + dr.decoder().decodeHeader(actual, true, dr.reader()); + } + + @Test(dataProvider = "literalProvider") + public void testLiteralWithLiteralNameOnStaticTable(String name, String value) throws IOException { + boolean sensitive = random.nextBoolean(); + + var actual = writeLiteral(sensitive, name, value); + var callback = new TestingCallBack(sensitive, name, value); + var dr = newDecoderWithReader(callback); + dr.decoder().decodeHeader(actual, true, dr.reader()); + } + + @Test + public void stateCheckSingle() throws IOException { + boolean sensitive = random.nextBoolean(); + var name = "foo"; + var value = "bar"; + + var bb = writeLiteral(sensitive, name, value); + var callback = new TestingCallBack(sensitive, name, value); + + var dr = newDecoderWithReader(callback); + int len = bb.capacity(); + for (int i = 0; i < len; i++) { + var b = ByteBuffer.wrap(new byte[]{ bb.get() }); + dr.decoder().decodeHeader(b, (i == len - 1), dr.reader()); + } + } + + /* Test Methods */ + private void debug(ByteBuffer bb, String msg, boolean verbose) { + if (verbose) { + System.out.printf("DEBUG[%s]: pos=%d, limit=%d, remaining=%d\n", + msg, bb.position(), bb.limit(), bb.remaining()); + } + System.out.printf("DEBUG[%s]: ", msg); + for (byte b : bb.array()) { + System.out.printf("(%s,%d) ", Integer.toBinaryString(b & 0xFF), (int)(b & 0xFF)); + } + System.out.print("\n"); + } + + private ByteBuffer writeIndex(int index) { + int N = 6; + int payload = 0b1100_0000; // static table = true + var bb = ByteBuffer.allocate(2); + + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer writeNameRef(int index, boolean sensitive, String value) { + int N = 4; + int payload = 0b0101_0000; // static table = true + if (sensitive) + payload |= 0b0010_0000; + var bb = allocateNameRefBuffer(N, index, value); + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + boolean huffman = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffman) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffman); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer writeLiteral(boolean sensitive, String name, String value) { + int N = 3; + //boolean hasInSt = Sta; + int payload = 0b0010_0000; // static table = true + var bb = allocateLiteralBuffer(N, name, value); + + if (sensitive) + payload |= 0b0001_0000; + boolean huffmanName = QuickHuffman.isHuffmanBetterFor(name); + if (huffmanName) + payload |= 0b0000_1000; + stringWriter.configure(name, N, payload, huffmanName); + stringWriter.write(bb); + stringWriter.reset(); + + boolean huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffmanValue) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffmanValue); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer allocateIndexBuffer(int index) { + /* + * Note on Integer Representation used for storing the length of name and value strings. + * Taken from RFC 7541 Section 5.1 + * + * "An integer is represented in two parts: a prefix that fills the current octet and an + * optional list of octets that are used if the integer value does not fit within the + * prefix. The number of bits of the prefix (called N) is a parameter of the integer + * representation. If the integer value is small enough, i.e., strictly less than 2N-1, it + * is encoded within the N-bit prefix. + * + * ... + * + * Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2N-1, is + * encoded using a list of one or more octets. 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. + * The remaining bits of the octets are used to encode the decreased value." + * + * Use "null" for name, if name isn't being provided (i.e. for a nameRef); otherwise, buffer + * will be too large. + * + */ + int N = 6; // bits available in first byte + int size = 1; + index -= Math.pow(2, N) - 1; // number that you can store in first N bits + while (index >= 0) { + index -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateNameRefBuffer(int N, int index, CharSequence value) { + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = 1 + vlen; + + index -= Math.pow(2, N) - 1; + while (index >= 0) { + index -= 127; + size++; + } + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateLiteralBuffer(int N, CharSequence name, CharSequence value) { + int nlen = Math.min(QuickHuffman.lengthOf(name), name.length()); + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = nlen + vlen; + + nlen -= Math.pow(2, N) - 1; + size++; + while (nlen >= 0) { + nlen -= 127; + size++; + } + + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum."""; + + private String randomString() { + int lower = random.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + /** + * The empty string ("") is a valid value String in the static table and the random + * String returned cannot refer to a entry in the table. Therefore, we set the upper + * bound below to a minimum of 1. + */ + return LOREM.substring(lower, 1 + lower + random.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestingCallBack implements DecodingCallback { + final int index; + final boolean huffmanName, huffmanValue; + final boolean sensitive; + final String name, value; + + // Indexed + TestingCallBack(int index, String name, String value) { + this(index, false, name, value); + } + // Literal w/Literal Name + TestingCallBack(boolean sensitive, String name, String value) { + this(-1, sensitive, name, value); + } + // Literal w/Name Reference + TestingCallBack(int index, boolean sensitive, String name, String value) { + this.index = index; + this.sensitive = sensitive; + this.name = name; + this.value = value; + this.huffmanName = QuickHuffman.isHuffmanBetterFor(name); + this.huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + } + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + fail("should not be called"); + } + + @Override + public void onIndexed(long index, CharSequence name, CharSequence value) { + assertEquals(this.index, index); + assertEquals(this.name, name.toString()); + assertEquals(this.value, value.toString()); + } + + @Override + public void onLiteralWithNameReference(long index, CharSequence name, + CharSequence value, boolean huffmanValue, + boolean sensitive) { + assertEquals(this.index, index); + assertEquals(this.value, value.toString()); + assertEquals(this.huffmanValue, huffmanValue); + assertEquals(this.sensitive, sensitive); + } + + @Override + public void onLiteralWithLiteralName(CharSequence name, boolean huffmanName, + CharSequence value, boolean huffmanValue, + boolean sensitive) { + assertEquals(this.name, name.toString()); + assertEquals(this.huffmanName, huffmanName); + assertEquals(this.value, value.toString()); + assertEquals(this.huffmanValue, huffmanValue); + assertEquals(this.sensitive, sensitive); + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + fail(http3Error + "Decoding error:" + http3Error, throwable); + } + + @Override + public long streamId() { + return 0; + } + } + + private static void qpackErrorHandler(Throwable error, Http3Error http3Error) { + fail("QPACK error:" + http3Error, error); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java b/test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java new file mode 100644 index 00000000000..cdf584d1854 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java @@ -0,0 +1,404 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.*; + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA + * -Djdk.http.qpack.allowBlockingEncoding=true + * -Djdk.http.qpack.decoderBlockedStreams=4 + * DynamicTableFieldLineRepresentationTest + */ +public class DynamicTableFieldLineRepresentationTest { + + private static final Random RANDOM = RandomFactory.getRandom(); + private static final long DT_CAPACITY = 4096L; + + //4.5.2. Indexed Field Line + @Test + public void indexedFieldLineOnDynamicTable() throws IOException { + boolean sensitive = RANDOM.nextBoolean(); + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Configure settingsFrames and prepopulate table + configureConnector(conn,1); + + var name = conn.decoderTable().get(0).name(); + var value = conn.decoderTable().get(0).value(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack( name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 1, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastIndexedName,name); + } + + //4.5.3. Indexed Field Line with Post-Base Index + @Test + public void indexedFieldLineOnDynamicTablePostBase() throws IOException { + System.err.println("start indexedFieldLineOnDynamicTablePostBase"); + boolean sensitive = RANDOM.nextBoolean(); + + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create settings frame with dynamic table capacity and number of blocked streams + configureConnector(conn, 3); + + var name = conn.decoderTable().get(1).name(); + var value = conn.decoderTable().get(1).value(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack( name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 1, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastIndexedName,name); + assertEquals(callback.lastValue,value); + } + + // 4.5.4. Literal Field Line with Name Reference + // A literal field line with name reference representation encodes a field line + // where the field name matches the field name of an entry in the static table + // or the field name of an entry in the dynamic table with an absolute index + // less than the value of the Base. + @Test + public void literalFieldLineNameReference() throws IOException { + System.err.println("start literalFieldLineNameReference"); + boolean sensitive = RANDOM.nextBoolean(); + + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> false, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create settings frame with dynamic table capacity and number of blocked streams + configureConnector(conn, 3); + + var name = conn.decoderTable().get(1).name(); + var value = conn.decoderTable().get(2).value(); // don't want value to match + + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack(name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 3, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastReferenceName, name); + } + + //4.5.5. Literal Field Line with Post-Base Name Reference + @Test + public void literalFieldLineNameReferencePostBase() throws IOException { + System.err.println("start literalFieldLineNameReferencePostBase"); + boolean sensitive = RANDOM.nextBoolean(); + + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> false, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create settings frame with dynamic table capacity and number of blocked streams + configureConnector(conn,4); + + var name = conn.decoderTable().get(3).name(); + var value = conn.decoderTable().get(2).value(); // don't want value to match + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack(name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 2, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastReferenceName, name); + assertEquals(callback.lastValue, value); + } + + private void configureConnector(EncoderDecoderConnector.EncoderDecoderPair connector, int numberOfEntries){ + // Create settings frame with dynamic table capacity and number of blocked streams + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + // 4k should be enough for storing dynamic table entries added by 'prepopulateDynamicTable' + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DT_CAPACITY); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS,2); + + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder with constructed ConnectionSettings + connector.encoder().configure(settings); + connector.decoder().configure(settings); + connector.encoderTable().setCapacity(DT_CAPACITY); + connector.decoderTable().setCapacity(DT_CAPACITY); + + // add basic matching entries to both + prepopulateDynamicTable(connector.encoderTable(), numberOfEntries); + prepopulateDynamicTable(connector.decoderTable(), numberOfEntries); + } + + private static void prepopulateDynamicTable(DynamicTable dynamicTable, int numEntries) { + for (int count = 0; count < numEntries; count++) { + var header = TestHeader.withId(count); + dynamicTable.insert(header.name(), header.value()); + } + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + } + } + + private static class TestingDynamicCallBack implements DecodingCallback { + final String name, value; + String lastLiteralName = null; + String lastReferenceName = null; + String lastValue = null; + String lastIndexedName = null; + + TestingDynamicCallBack(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void onDecoded(CharSequence actualName, CharSequence value) { + fail("onDecoded should not be called"); + } + + @Override + public void onComplete() { + System.out.println("completed it"); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + fail("Decoding error: " + http3Error, throwable); + } + + @Override + public long streamId() { + return 0; + } + + @Override + public void onIndexed(long actualIndex, CharSequence actualName, CharSequence actualValue) { + System.out.println("Indexed called"); + assertEquals(actualName, name); + assertEquals(actualValue, value); + lastValue = value; + lastIndexedName = name; + } + + @Override + public void onLiteralWithNameReference(long index, + CharSequence actualName, + CharSequence actualValue, + boolean valueHuffman, + boolean hideIntermediary) { + System.out.println("Literal with name reference called"); + assertEquals(actualName.toString(), name); + assertEquals(actualValue.toString(), value); + lastReferenceName = name; + lastValue = value; + } + + @Override + public void onLiteralWithLiteralName(CharSequence actualName, boolean nameHuffman, + CharSequence actualValue, boolean valueHuffman, + boolean hideIntermediary) { + System.out.println("Literal with literal name called"); + assertEquals(actualName.toString(), name); + assertEquals(actualValue.toString(), value); + lastLiteralName = name; + lastValue = value; + } + } + + record TestHeader(String name, String value) { + public static BlockingDecodingTest.TestHeader withId(int id) { + return new BlockingDecodingTest.TestHeader(NAME + id, VALUE + id); + } + } + + private static final String NAME = "test"; + private static final String VALUE = "valueTest"; +} diff --git a/test/jdk/java/net/httpclient/qpack/DynamicTableTest.java b/test/jdk/java/net/httpclient/qpack/DynamicTableTest.java new file mode 100644 index 00000000000..80765e41787 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DynamicTableTest.java @@ -0,0 +1,366 @@ +/* + * 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. + * + * 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 + * @key randomness + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL DynamicTableTest + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.test.lib.RandomFactory; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.IntStream; + +public class DynamicTableTest { + + // Test for addition to the table and that indices are growing monotonically, + // and they can be used to retrieve previously added entries + @Test + public void monotonicIndexes() { + int tableMaxCapacityBytes = 2048; + int numberOfElementsToAdd = 1024; + int charsPerNumber = (int) Math.ceil(Math.log10(numberOfElementsToAdd)); + int oneElementSize = 32 + HEADER_NAME_PREFIX.length() + HEADER_VALUE_PREFIX.length() + charsPerNumber * 2; + + // Expected table capacity in elements + int maxElementsInTable = tableMaxCapacityBytes / oneElementSize; + + // Test element id counter + long lastAddedId; + var dynamicTable = new DynamicTable(QPACK.getLogger().subLogger("monotonicIndexes")); + dynamicTable.setMaxTableCapacity(2048); + dynamicTable.setCapacity(tableMaxCapacityBytes); + + for (lastAddedId = 0; lastAddedId < numberOfElementsToAdd; lastAddedId++) { + var name = generateHeaderString(lastAddedId, true, charsPerNumber); + var value = generateHeaderString(lastAddedId, false, charsPerNumber); + long addedId = dynamicTable.insert(name, value); + + // Check that dynamic table put gives back monotonically increasing indexes + Assert.assertEquals(addedId, lastAddedId); + + if (lastAddedId > maxElementsInTable) { + // Check that oldest element is available and not reclaimed + long oldestAliveId = lastAddedId - maxElementsInTable + 1; + dynamicTable.get(oldestAliveId); + + // Check that relative indexing can be used to get oldest and newest entry + dynamicTable.getRelative(maxElementsInTable - 1); + dynamicTable.getRelative(0); + + // Check that reverse lookup is working for a random index from not reclaimed region + long rid = RANDOM.nextLong(oldestAliveId, lastAddedId); + String rName = generateHeaderString(rid, true, charsPerNumber); + String rValue = generateHeaderString(rid, false, charsPerNumber); + + // The reverse lookup search result range is shifted by 1 to implement search result indexing: + // full match found in a table: searchResult = idx + 1 + // partial match (name) in a table: searchResult = -idx - 1 + // no match: 0 + long fullMatchSearchResult = dynamicTable.search(rName, rValue); + long onlyNameSearchResult = dynamicTable.search(rName, "notFoundInTable"); + long noMatchResult = dynamicTable.search(HEADER_NAME_PREFIX, HEADER_VALUE_PREFIX); + + Assert.assertEquals(fullMatchSearchResult - 1, rid); + Assert.assertEquals(-onlyNameSearchResult - 1, rid); + Assert.assertEquals(noMatchResult, 0); + } + } + } + + @Test(dataProvider = "randomTableResizeData") + public void randomTableResize(int initialSize, long tail, long head, int resizeTo) + throws Throwable { + HeaderField[] initial = generateHeadersArray(initialSize, tail, head); + resizeTestRunner(initial, tail, head, resizeTo); + } + + @DataProvider + public Object[][] randomTableResizeData() { + return IntStream.range(0, 1000) + .boxed() + .map(i -> newRandomTableConfiguration()) + .toArray(Object[][]::new); + } + + @Test + public void holderArrayLengthTest() { + // Test that holder array size for storing elements is increased according to demand on array + // elements, and that by default its length is set to 64 elements. + var dynamicTable = new DynamicTable(QPACK.getLogger().subLogger("tableResizeTests")); + + // Check that the initial array length is DynamicTable.INITIAL_HOLDER_ARRAY_SIZE + Assert.assertEquals(getElementsArrayLength(dynamicTable), + INITIAL_HOLDER_ARRAY_SIZE); + + // Update dynamic table capacity to maximum allowed value and check + // that holder array is not changed + dynamicTable.setMaxTableCapacity(IntegerReader.QPACK_MAX_INTEGER_VALUE); + dynamicTable.setCapacity(IntegerReader.QPACK_MAX_INTEGER_VALUE); + Assert.assertEquals(getElementsArrayLength(dynamicTable), + INITIAL_HOLDER_ARRAY_SIZE); + + // Add DynamicTable.INITIAL_HOLDER_ARRAY_SIZE + 1 element to the dynamic table + // and check that its length is increased 2 times + for (int i = 0; i <= INITIAL_HOLDER_ARRAY_SIZE; i++) { + dynamicTable.insert("name" + i, "value" + i); + } + Assert.assertEquals(getElementsArrayLength(dynamicTable), INITIAL_HOLDER_ARRAY_SIZE << 1); + } + + // Test for a simple resize that checks that unique indexes still reference the correct entry + @Test(dataProvider = "simpleTableResizeData") + public void simpleTableResize(HeaderField[] array, long tail, long head, int resizeTo) throws Throwable { + resizeTestRunner(array, tail, head, resizeTo); + } + + @DataProvider + public Object[][] simpleTableResizeData() { + return new Object[][]{ + tableResizeScenario1(), tableResizeScenario2(), + tableResizeScenario3(), tableResizeScenario4(), + tableResizeScenario5()}; + } + + private Object[] tableResizeScenario1() { + HeaderField[] elements = new HeaderField[8]; + elements[5] = new HeaderField("5", "5"); // Tail + elements[6] = new HeaderField("6", "6"); + elements[7] = new HeaderField("7", "7"); // Head + return new Object[]{elements, 21L, 24L, 4}; + } + + private Object[] tableResizeScenario2() { + HeaderField[] elements = new HeaderField[8]; + elements[2] = new HeaderField("2", "2"); // Tail + elements[3] = new HeaderField("3", "3"); + elements[4] = new HeaderField("4", "4"); + elements[5] = new HeaderField("5", "5"); // Head + return new Object[]{elements, 26L, 30L, 4}; + } + + private Object[] tableResizeScenario3() { + HeaderField[] elements = new HeaderField[8]; + elements[0] = new HeaderField("4", "4"); + elements[1] = new HeaderField("5", "5"); // Head + elements[6] = new HeaderField("2", "2"); // Tail + elements[7] = new HeaderField("3", "3"); + return new Object[]{elements, 30L, 34L, 64}; + } + + private Object[] tableResizeScenario4() { + HeaderField[] elements = new HeaderField[8]; + elements[0] = new HeaderField("4", "4"); + elements[1] = new HeaderField("5", "5"); // Head + elements[5] = new HeaderField("1", "1"); // Tail + elements[6] = new HeaderField("2", "2"); + elements[7] = new HeaderField("3", "3"); + return new Object[]{elements, 29L, 34L, 16}; + } + + private Object[] tableResizeScenario5() { + HeaderField[] elements = new HeaderField[64]; + elements[10] = new HeaderField("1", "1"); + return new Object[]{elements, 3977L, 3978L, 16}; + } + + private static void resizeTestRunner(HeaderField[] array, long tail, long head, int resizeTo) throws Throwable { + assert tail < head; + var dynamicTable = new DynamicTable(QPACK.getLogger().subLogger("tableResizeTests")); + dynamicTable.setMaxTableCapacity(2048); + dynamicTable.setCapacity(2048); + // Prepare dynamic table state for the resize operation + DT_ELEMENTS_VH.set(dynamicTable, array); + DT_HEAD_VH.set(dynamicTable, head); + DT_TAIL_VH.set(dynamicTable, tail); + + // Call resize + ReentrantReadWriteLock lock = (ReentrantReadWriteLock) DT_LOCK_VH.get(dynamicTable); + lock.writeLock().lock(); + HeaderField[] resizeResult; + try { + // Call DynamicTable.resize + DT_RESIZE_MH.invoke(dynamicTable, resizeTo); + // Acquire resize result + resizeResult = (HeaderField[]) DT_ELEMENTS_VH.get(dynamicTable); + } finally { + lock.writeLock().unlock(); + } + + // Check the resulting array by calculating the expected array + HeaderField[] expectedResult = calcResizeResult(array, tail, head, resizeTo); + + // Check the resulting of the resize operation + checkResizeResult(array, resizeResult, expectedResult); + } + + private static HeaderField[] generateHeadersArray(int size, long tail, long head) { + assert head > tail; + HeaderField[] res = new HeaderField[size]; + assert head > 0L; + int charsPerNumber = (int) (Math.log10(head) + 1); + for (long eid = tail; eid < head; eid++) { + int idx = (int) (eid % size); + res[idx] = new HeaderField(generateHeaderString(eid, true, charsPerNumber), + generateHeaderString(eid, false, charsPerNumber)); + } + return res; + } + + private static int getElementsArrayLength(DynamicTable dynamicTable) { + HeaderField[] array = (HeaderField[]) DT_ELEMENTS_VH.get(dynamicTable); + return array.length; + } + + private static Object[] newRandomTableConfiguration() { + boolean shrink = RANDOM.nextBoolean(); + int initialSize = pow2size(RANDOM.nextInt(2, 2048)); + int resizeTo = shrink ? pow2size(RANDOM.nextInt(1, initialSize)) : pow2size(RANDOM.nextInt(initialSize, 4096)); + int elementsCount = RANDOM.nextInt(0, Math.min(initialSize, resizeTo)); + long tail = RANDOM.nextLong(100000); + long head = tail + elementsCount + 1; + return new Object[]{initialSize, tail, head, resizeTo}; + } + + private static HeaderField[] calcResizeResult(HeaderField[] array, long tail, long head, int resizeTo) { + HeaderField[] result = new HeaderField[resizeTo]; + for (long p = tail; p < head; p++) { + int newIdx = (int) (p % resizeTo); + int oldIdx = (int) (p % array.length); + result[newIdx] = array[oldIdx]; + } + return result; + } + + private static void checkResizeResult(HeaderField[] initial, HeaderField[] resized, HeaderField[] expected) { + Assert.assertEquals(resized.length, expected.length); + for (int index = 0; index < expected.length; index++) { + if (!sameHeaderField(expected[index], resized[index])) { + System.err.println("Initial Array:" + Arrays.deepToString(initial)); + System.err.println("Resized Array:" + Arrays.deepToString(resized)); + System.err.println("Expected Array:" + Arrays.deepToString(expected)); + Assert.fail("DynamicTable.resize failed"); + } + } + } + + private static boolean sameHeaderField(HeaderField a, HeaderField b) { + // Check if one HeaderField is null and another is not null + if (a == null ^ b == null) { + return false; + } + // Given previous check, check if both HeaderField are null + if (a == null) { + return true; + } + // Both HFs are not null - will check name() and value() values + return a.name().equals(b.name()) && a.value().equals(b.value()); + } + + private static MethodHandle findDynamicTableResizeMH() { + try { + MethodType mt = MethodType.methodType(void.class, int.class); + return DT_LOOKUP.findVirtual(DynamicTable.class, "resize", mt); + } catch (Exception e) { + Assert.fail("Failed to initialize DynamicTable.resize MH", e); + return null; + } + } + + private static VarHandle findDynamicTableFieldVH(String fieldName, Class fieldType) { + try { + return DT_LOOKUP.findVarHandle(DynamicTable.class, fieldName, fieldType); + } catch (Exception e) { + Assert.fail("Failed to initialize DynamicTable private Lookup instance", e); + return null; + } + } + + private static T readDynamicTableStaticFieldValue(String fieldName, Class fieldType) { + try { + var vh = DT_LOOKUP.findStaticVarHandle(DynamicTable.class, fieldName, fieldType); + return (T) vh.get(); + } catch (Exception e) { + Assert.fail("Failed to read DynamicTable static field value", e); + return null; + } + } + + private static MethodHandles.Lookup initializeDtLookup() { + try { + return MethodHandles.privateLookupIn(DynamicTable.class, MethodHandles.lookup()); + } catch (IllegalAccessException e) { + Assert.fail("Failed to initialize DynamicTable private Lookup instance", e); + return null; + } + } + + + private static final MethodHandles.Lookup DT_LOOKUP; + private static final MethodHandle DT_RESIZE_MH; + private static final VarHandle DT_HEAD_VH; + private static final VarHandle DT_TAIL_VH; + private static final VarHandle DT_ELEMENTS_VH; + private static final VarHandle DT_LOCK_VH; + private static final int INITIAL_HOLDER_ARRAY_SIZE; + + static { + DT_LOOKUP = initializeDtLookup(); + DT_RESIZE_MH = findDynamicTableResizeMH(); + DT_HEAD_VH = findDynamicTableFieldVH("head", long.class); + DT_TAIL_VH = findDynamicTableFieldVH("tail", long.class); + DT_ELEMENTS_VH = findDynamicTableFieldVH("elements", HeaderField[].class); + DT_LOCK_VH = findDynamicTableFieldVH("lock", ReentrantReadWriteLock.class); + INITIAL_HOLDER_ARRAY_SIZE = readDynamicTableStaticFieldValue( + "INITIAL_HOLDER_ARRAY_LENGTH", int.class); + } + + private static String generateHeaderString(long id, boolean generateName, int charsPerNumber) { + return (generateName ? HEADER_NAME_PREFIX : HEADER_VALUE_PREFIX) + ("%0" + charsPerNumber + "d").formatted(id); + } + + private static int pow2size(int size) { + return 1 << (32 - Integer.numberOfLeadingZeros(size - 1)); + } + + private static final String HEADER_NAME_PREFIX = "HeaderName"; + private static final String HEADER_VALUE_PREFIX = "HeaderValue"; + private static final Random RANDOM = RandomFactory.getRandom(); +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java new file mode 100644 index 00000000000..418af9be2ad --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java @@ -0,0 +1,241 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicReference; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA + * EncoderDecoderConnectionTest + */ +public class EncoderDecoderConnectionTest { + + @Test + public void capacityUpdateTest() { + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, error::set); + + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Set encoder and decoder maximum dynamic table capacity + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 2048L); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + decoder.configure(settings); + encoder.configure(settings); + + // Encoder - update DT capacity + final long capacityToSet = 1024L; + encoder.setTableCapacity(capacityToSet); + + // Check that no errors observed + Assert.assertNull(encoderErrorHandler.error.get()); + Assert.assertNull(encoderErrorHandler.http3Error.get()); + Assert.assertNull(decoderErrorHandler.error.get()); + Assert.assertNull(decoderErrorHandler.http3Error.get()); + + // Check that encoder's table capacity is updated + Assert.assertEquals(conn.encoderTable().capacity(), capacityToSet); + // Since encoder/decoder streams are cross-wired we expect see dynamic + // table capacity updated for the decoder too + Assert.assertEquals(conn.decoderTable().capacity(), + conn.encoderTable().capacity()); + } + + @Test + public void entryInsertionTest() { + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Set encoder and decoder maximum dynamic table capacity + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 2048L); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + decoder.configure(settings); + encoder.configure(settings); + + // Update encoder and decoder DTs capacity - note that "Set Dynamic Table Capacity" + // is issued by the encoder that updates capacity on the decoder side + encoder.setTableCapacity(1024L); + + // Create table entry for insertion to the dynamic table + var entryToInsert = new TableEntry("test", "testValue"); + + // Create encoder instruction writer for generating "Insert with Literal Name" + // encoder instruction + var encoderInstructionWriter = new EncoderInstructionsWriter(); + + // Check that no errors observed + Assert.assertNull(encoderErrorHandler.error.get()); + Assert.assertNull(encoderErrorHandler.http3Error.get()); + Assert.assertNull(decoderErrorHandler.error.get()); + Assert.assertNull(decoderErrorHandler.http3Error.get()); + + // Issue the insert instruction on encoder stream + conn.encoderTable().insertWithEncoderStreamUpdate(entryToInsert, + encoderInstructionWriter, conn.encoderStreams(), + encoder.newEncodingContext(0, 0, new HeaderFrameWriter())); + var encoderHeader = conn.encoderTable().get(0); + var decoderHeader = conn.decoderTable().get(0); + Assert.assertEquals(encoderHeader.name(), decoderHeader.name()); + Assert.assertEquals(encoderHeader.value(), decoderHeader.value()); + } + + @Test + public void decoderErrorReportingTest() { + AtomicReference error = new AtomicReference<>(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(e -> false, + encoderErrorHandler::qpackErrorHandler, + decoderErrorHandler::qpackErrorHandler, + error::set); + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 2048L); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + conn.encoder().configure(settings); + conn.encoderTable().setCapacity(1024L); + conn.encoderTable().insertWithEncoderStreamUpdate( + new TableEntry("a", "b"), + new EncoderInstructionsWriter(), + conn.encoderStreams(), + conn.encoder().newEncodingContext(0, 0, new HeaderFrameWriter())); + + // QPACK_ENCODER_STREAM_ERROR is expected on the decoder side + // since the decoder dynamic table capacity was not updated + Assert.assertEquals(decoderErrorHandler.http3Error.get(), + Http3Error.QPACK_ENCODER_STREAM_ERROR); + + // It is expected that http3 error reported to + // the decoder error handler only + Assert.assertNull(encoderErrorHandler.http3Error.get()); + } + + @Test + public void overflowIntegerInInstructions() { + AtomicReference error = new AtomicReference<>(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(e -> false, + encoderErrorHandler::qpackErrorHandler, + decoderErrorHandler::qpackErrorHandler, + error::set); + + // Forge byte buffer with encoder instruction containing integer > + // QPACK_MAX_INTEGER_VALUE + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Capacity (5+) | + // +---+---+---+-------------------+ + var encoderInstBb = instructionWithOverflowInteger(5, 0b0010_0000); + conn.encoderStreams().submitData(encoderInstBb); + + // Send bad decoder instruction back to encoder + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + var buffer = instructionWithOverflowInteger(6, 0b0100_0000); + conn.decoderStreams().submitData(buffer); + + // Analyze errors for expected results + Throwable encoderError = encoderErrorHandler.error.get(); + Http3Error encoderHttp3Error = encoderErrorHandler.http3Error.get(); + Throwable decoderError = decoderErrorHandler.error.get(); + Http3Error decoderHttp3Error = decoderErrorHandler.http3Error.get(); + + System.err.println("Encoder Error: " + encoderError); + System.err.println("Encoder Http3 error: " + encoderHttp3Error); + System.err.println("Decoder Error: " + decoderError); + System.err.println("Decoder Http3 error: " + decoderHttp3Error); + + if (encoderError == null || !(encoderError instanceof IOException)) { + Assert.fail("Incorrect encoder error type", encoderError); + } + if (decoderError == null || !(decoderError instanceof IOException)) { + Assert.fail("Incorrect decoder error type", decoderError); + } + Assert.assertEquals(encoderHttp3Error, Http3Error.QPACK_DECODER_STREAM_ERROR); + Assert.assertEquals(decoderHttp3Error, Http3Error.QPACK_ENCODER_STREAM_ERROR); + } + + private static ByteBuffer instructionWithOverflowInteger(int N, int payload) { + var buffer = ByteBuffer.allocate(11); + int max = (2 << (N - 1)) - 1; + buffer.put((byte) (payload | max)); + for (int i = 0; i < 9; i++) { + buffer.put((byte) 128); + } + buffer.put((byte)10); + buffer.flip(); + return buffer; + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java new file mode 100644 index 00000000000..3cf185ff8a7 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java @@ -0,0 +1,509 @@ +/* + * 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. + * + * 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. + */ + +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; +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.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.InsertionPolicy; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.TerminationCause; +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.QuicStreamWriter; +import jdk.internal.net.quic.QuicTLSEngine; +import org.testng.Assert; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * Instance of this class provides a stubbed Quic Connection implementation that + * cross-wires encoder/decoder streams, and also provides access to + * encoder and decoder dynamic tables. + */ +public class EncoderDecoderConnector { + /** + * Constructs test connector instance capable of instantiating cross-wired encoder/decoder pair. + * It is achieved by implementing stubs for Quic connection and writer classes. + */ + public EncoderDecoderConnector() { + this(null, null); + } + + /** + * Constructs test connector instance capable of instantiating encoder/decoder pair. + * The encoder/decoder connections are not cross-wired. The provided byte buffer consumers + * are used by the underlying Quic connection instead. + * + * @param encoderBytesConsumer consumer of the encoder byte buffers + * @param decoderBytesConsumer consumer of the decoder byte buffers + */ + public EncoderDecoderConnector(Consumer encoderBytesConsumer, Consumer decoderBytesConsumer) { + encoderReceiverFuture = new CompletableFuture<>(); + decoderReceiverFuture = new CompletableFuture<>(); + encoderConnection = new TestQuicConnection(decoderReceiverFuture, encoderBytesConsumer); + decoderConnection = new TestQuicConnection(encoderReceiverFuture, decoderBytesConsumer); + } + + /** + * Create new encoder/decoder pair and establish Quic stub connection between them. + * + * @param encoderInsertionPolicy encoder insertion policy + * @param encoderErrorHandler encoder stream error handler + * @param decoderErrorHandler decoder stream error handler + * @param streamsErrorHandler streams error handler + * @return encoder/decoder pair + */ + public EncoderDecoderConnector.EncoderDecoderPair + newEncoderDecoderPair(InsertionPolicy encoderInsertionPolicy, + QPACKErrorHandler encoderErrorHandler, + QPACKErrorHandler decoderErrorHandler, + Consumer streamsErrorHandler) { + // One instance of this class supports only one encoder/decoder pair + if (connectionCreated) { + throw new IllegalStateException("Encoder/decoder pair was already instantiated"); + } + connectionCreated = true; + + // Create encoder + var encoder = new Encoder(encoderInsertionPolicy, (receiver) -> + createEncoderStreams(receiver, streamsErrorHandler), + encoderErrorHandler); + + // Create decoder + var decoder = new Decoder((receiver) -> + createDecoderStreams(receiver, streamsErrorHandler), + decoderErrorHandler); + // Extract encoder and decoder dynamic tables + DynamicTable encoderTable = dynamicTable(encoder); + DynamicTable decoderTable = dynamicTable(decoder); + return new EncoderDecoderConnector.EncoderDecoderPair(encoder, decoder, + encoderTable, decoderTable, encoderStreamPair, decoderStreamPair); + } + + /** + * Record describing {@linkplain EncoderDecoderConnector#EncoderDecoderConnector() cross-wired} + * OR {@link EncoderDecoderConnector#EncoderDecoderConnector(Consumer, Consumer) decoupled} + * encoder and decoder pair. + * The references for encoder and decoder dynamic tables also provided for testing purposes. + * + * @param encoder encoder + * @param decoder decoder + * @param encoderTable encoder's dynamic table + * @param decoderTable decoder's dynamic table + * @param encoderStreams encoder streams + */ + record EncoderDecoderPair(Encoder encoder, Decoder decoder, + DynamicTable encoderTable, + DynamicTable decoderTable, + QueuingStreamPair encoderStreams, + QueuingStreamPair decoderStreams) { + } + + + private CompletableFuture> decoderReceiverFuture; + private CompletableFuture> encoderReceiverFuture; + private final QuicConnection encoderConnection; + private final QuicConnection decoderConnection; + private volatile QueuingStreamPair encoderStreamPair; + private volatile QueuingStreamPair decoderStreamPair; + + private volatile boolean connectionCreated; + + private static DynamicTable dynamicTable(Encoder encoder) { + return (DynamicTable) ENCODER_DT_VH.get(encoder); + } + + private static DynamicTable dynamicTable(Decoder decoder) { + return (DynamicTable) DECODER_DT_VH.get(decoder); + } + + private static final MethodHandles.Lookup ENCODER_LOOKUP = + initializeLookup(Encoder.class); + private static final MethodHandles.Lookup DECODER_LOOKUP = + initializeLookup(Decoder.class); + private static final VarHandle ENCODER_DT_VH = findDynamicTableVH( + ENCODER_LOOKUP, Encoder.class); + private static final VarHandle DECODER_DT_VH = findDynamicTableVH( + DECODER_LOOKUP, Decoder.class); + + + private static MethodHandles.Lookup initializeLookup(Class clz) { + try { + return MethodHandles.privateLookupIn(clz, MethodHandles.lookup()); + } catch (IllegalAccessException e) { + Assert.fail("Failed to initialize private Lookup instance", e); + return null; + } + } + + private static VarHandle findDynamicTableVH( + final MethodHandles.Lookup lookup, Class recv) { + try { + return lookup.findVarHandle(recv, "dynamicTable", DynamicTable.class); + } catch (Exception e) { + Assert.fail("Failed to acquire dynamic table VarHandle instance", e); + return null; + } + } + + + QueuingStreamPair createEncoderStreams(Consumer receiver, + Consumer errorHandler) { + QueuingStreamPair streamPair = new QueuingStreamPair( + Http3Streams.StreamType.QPACK_ENCODER, + encoderConnection, + receiver, + TestErrorHandler.of(errorHandler), + Utils.getDebugLogger(() -> "test-encoder")); + encoderReceiverFuture.complete(receiver); + encoderStreamPair = streamPair; + return streamPair; + } + + QueuingStreamPair createDecoderStreams(Consumer receiver, + Consumer errorHandler) { + QueuingStreamPair streamPair = new QueuingStreamPair( + Http3Streams.StreamType.QPACK_DECODER, + decoderConnection, + receiver, + TestErrorHandler.of(errorHandler), + Utils.getDebugLogger(() -> "test-decoder")); + decoderReceiverFuture.complete(receiver); + decoderStreamPair = streamPair; + return streamPair; + } + + private class TestQuicConnection extends QuicConnection { + + public TestQuicConnection(CompletableFuture> receiverFuture, + Consumer bytesWriter) { + this.sender = new TestQuicSenderStream(receiverFuture, bytesWriter); + } + + final TestQuicSenderStream sender; + + @Override + public boolean isOpen() { + return true; + } + + @Override + public TerminationCause terminationCause() { + return null; + } + + @Override + public QuicTLSEngine getTLSEngine() { + return null; + } + + @Override + public InetSocketAddress peerAddress() { + return null; + } + + @Override + public SocketAddress localAddress() { + return null; + } + + @Override + public CompletableFuture startHandshake() { + return null; + } + + @Override + public CompletableFuture openNewLocalBidiStream( + Duration duration) { + return null; + } + + @Override + public CompletableFuture openNewLocalUniStream( + Duration duration) { + // This method is called to create two unidirectional streams: + // one for decoder, one for encoder + return MinimalFuture.completedFuture(sender); + } + + @Override + public void addRemoteStreamListener( + Predicate streamConsumer) { + } + + @Override + public boolean removeRemoteStreamListener( + Predicate streamConsumer) { + return false; + } + + @Override + public Stream quicStreams() { + return null; + } + + @Override + public CompletableFuture handshakeReachedPeer() { + return MinimalFuture.completedFuture(null); + } + + @Override + public CompletableFuture requestSendPing() { + return MinimalFuture.completedFuture(-1L); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return null; + } + + @Override + public String dbgTag() { + return null; + } + + @Override + public String logTag() { + return null; + } + } + + + private class TestQuicStreamWriter extends QuicStreamWriter { + final TestQuicSenderStream sender; + volatile boolean gotStreamType; + volatile Http3Streams.StreamType associatedStreamType; + + final CompletableFuture> receiverFuture; + final Consumer bytesWriter; + + TestQuicStreamWriter(SequentialScheduler scheduler, TestQuicSenderStream sender, + CompletableFuture> receiverFuture, + Consumer bytesWriter) { + super(scheduler); + this.sender = sender; + this.gotStreamType = false; + this.receiverFuture = receiverFuture; + this.bytesWriter = bytesWriter; + } + + private void write(ByteBuffer bb) { + if (bytesWriter == null) { + if (!gotStreamType) { + IntegerReader integerReader = new IntegerReader(); + integerReader.configure(8); + try { + integerReader.read(bb); + } catch (QPackException e) { + System.err.println("Can't read stream type byte"); + } + Http3Streams.StreamType type = Http3Streams.StreamType.ofCode((int) integerReader.get()).get(); + System.err.println("Stream opened with type=" + type); + gotStreamType = true; + associatedStreamType = type; + } else { + if (receiverFuture.isDone() && !receiverFuture.isCompletedExceptionally()) { + Consumer receiver = receiverFuture.getNow(null); + if (receiver != null) { + receiver.accept(bb); + } + } + } + } else { + bytesWriter.accept(bb); + } + } + + @Override + public QuicSenderStream.SendingStreamState sendingState() { + return null; + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) + throws IOException { + write(buffer); + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + write(buffer); + } + + @Override + public long credit() { + return Long.MAX_VALUE; + } + + @Override + public void reset(long errorCode) { + } + + @Override + public QuicSenderStream stream() { + return connected() ? sender : null; + } + + @Override + public boolean connected() { + return sender.writer == this; + } + } + + class TestQuicSenderStream implements QuicSenderStream { + private static AtomicLong ids = new AtomicLong(); + private final long id; + TestQuicStreamWriter writer; + Consumer bytesWriter; + final CompletableFuture> receiverFuture; + + TestQuicSenderStream(CompletableFuture> receiverFuture, + Consumer bytesWriter) { + id = ids.getAndIncrement() * 4 + type(); + this.receiverFuture = receiverFuture; + this.bytesWriter = bytesWriter; + } + + @Override + public SendingStreamState sendingState() { + return SendingStreamState.READY; + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return writer == null ? writer = new TestQuicStreamWriter( + scheduler, this, receiverFuture, bytesWriter) : writer; + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + } + + @Override + public void reset(long errorCode) { + } + + @Override + public long dataSent() { + return 0; + } + + @Override + public long streamId() { + return id; + } + + @Override + public StreamMode mode() { + return null; + } + + @Override + public boolean isClientInitiated() { + return true; + } + + @Override + public boolean isServerInitiated() { + return false; + } + + @Override + public boolean isBidirectional() { + return false; + } + + @Override + public boolean isLocalInitiated() { + return true; + } + + @Override + public boolean isRemoteInitiated() { + return false; + } + + @Override + public int type() { + return 0x02; + } + + @Override + public StreamState state() { + return SendingStreamState.READY; + } + + @Override + public long sndErrorCode() { + return -1; + } + + @Override + public boolean stopSendingReceived() { + return false; + } + } + + private static class TestErrorHandler + implements UniStreamPair.StreamErrorHandler { + final Consumer handler; + + private TestErrorHandler(Consumer handler) { + this.handler = handler; + } + + @Override + public void onError(QuicStream stream, UniStreamPair uniStreamPair, + Throwable throwable) { + handler.accept(throwable); + } + + public static TestErrorHandler of(Consumer handler) { + return new TestErrorHandler(handler); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java b/test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java new file mode 100644 index 00000000000..a1fa9745e05 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java @@ -0,0 +1,442 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.StaticTable; +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm EncoderDecoderTest + */ +public class EncoderDecoderTest { + private final Random random = new Random(); + + private static final int TEST_STR_MAX_LENGTH = 10; + + + private static void qpackErrorHandler(Throwable error, Http3Error http3Error) { + fail(http3Error + "QPACK error:" + http3Error, error); + } + + @DataProvider(name = "indexProvider") + public Object[][] indexProvider() { + AtomicLong index = new AtomicLong(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(headerField -> List.of(index.getAndIncrement(), headerField)) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider(name = "nameReferenceProvider") + public Object[][] nameReferenceProvider() { + AtomicLong tableIndex = new AtomicLong(); + Map> map = new HashMap<>(); + for (var headerField : StaticTable.HTTP3_HEADER_FIELDS) { + var name = headerField.name(); + var index = tableIndex.getAndIncrement(); + + if (!map.containsKey(name)) + map.put(name, new ArrayList<>()); + map.get(name).add(index); + } + return map.entrySet().stream() + .map(e -> List.of(e.getKey(), randomString(), e.getValue())) + .map(List::toArray).toArray(Object[][]::new); + } + + @DataProvider(name = "literalProvider") + public Object[][] literalProvider() { + var output = new String[100][]; + for (int i = 0; i < 100; i++) { + output[i] = new String[]{randomString(), randomString()}; + } + return output; + } + + private void assertNotFailed(AtomicReference errorRef) { + var error = errorRef.get(); + if (error != null) throw new AssertionError(error); + } + + @Test(dataProvider = "indexProvider") + public void encodeDecodeIndexedOnStaticTable(long index, HeaderField h) throws IOException { + var actual = allocateIndexTestBuffer(index); + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + AtomicReference writerStub = new AtomicReference<>(); + EncoderDecoderConnector connector = new EncoderDecoderConnector(writerStub::set, writerStub::set); + var conn = connector.newEncoderDecoderPair(e -> false, + EncoderDecoderTest::qpackErrorHandler, + EncoderDecoderTest::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + + // Set encoder maximum dynamic table capacity + conn.encoderTable().setMaxTableCapacity(256); + // Set dynamic table capacity that doesn't exceed the max capacity value + conn.encoderTable().setCapacity(256); + + var decoder = conn.decoder(); + + // Create header frame reader and writer + var callback = new TestingCallBack(index, h.name(), h.value()); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + var headerFrameWriter = encoder.newHeaderFrameWriter(); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, 0, headerFrameWriter); + + // Configures encoder for writing the header name:value pair + encoder.header(context, h.name(), h.value(), false); + + // Write the header + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + buffers.add(actual); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode generated prefix bytes and encoded headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertNotFailed(error); + } + + @Test(dataProvider = "nameReferenceProvider") + public void encodeDecodeLiteralWithNameRefOnStaticTable(String name, String value, List validIndices) throws IOException { + long index = Collections.max(validIndices); + boolean sensitive = random.nextBoolean(); + + var actual = allocateNameRefBuffer(index, value); + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + AtomicReference writerStub = new AtomicReference<>(); + + // Create encoder and decoder + EncoderDecoderConnector connector = new EncoderDecoderConnector(writerStub::set, writerStub::set); + var conn = connector.newEncoderDecoderPair(e -> false, + EncoderDecoderTest::qpackErrorHandler, + EncoderDecoderTest::qpackErrorHandler, + error::set); + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingCallBack(validIndices, name, value, sensitive); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, 0, headerFrameWriter); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive); + + // Write the header + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + buffers.add(actual); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertNotFailed(error); + } + + @Test(dataProvider = "literalProvider") + public void encodeDecodeLiteralWithLiteralNameOnStaticTable(String name, String value) throws IOException { + boolean sensitive = random.nextBoolean(); + List buffers = new ArrayList<>(); + var actual = allocateLiteralBuffer(name, value); + AtomicReference error = new AtomicReference<>(); + AtomicReference writerStub = new AtomicReference<>(); + + // Create encoder and decoder + EncoderDecoderConnector connector = new EncoderDecoderConnector(writerStub::set, writerStub::set); + var conn = connector.newEncoderDecoderPair(e -> false, + EncoderDecoderTest::qpackErrorHandler, + EncoderDecoderTest::qpackErrorHandler, + error::set); + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingCallBack(name, value, sensitive); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, 0, headerFrameWriter); + + // Configures encoder for writing the header name:value conn + encoder.header(context, name, value, sensitive); + // Write the header + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + buffers.add(actual); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertNotFailed(error); + } + + /* Test Methods */ + private void debug(ByteBuffer bb, String msg, boolean verbose) { + if (verbose) + System.out.printf("DEBUG[%s]: pos=%d, limit=%d, remaining=%d%n", + msg, bb.position(), bb.limit(), bb.remaining()); + System.out.printf("DEBUG[%s]: ", msg); + for (byte b : bb.array()) { + System.out.printf("(%s,%d) ", Integer.toBinaryString(b & 0xFF), b & 0xFF); + } + System.out.println(); + } + + private ByteBuffer allocateIndexTestBuffer(long index) { + /* + * Note on Integer Representation used for storing the length of name and value strings. + * Taken from RFC 7541 Section 5.1 + * + * "An integer is represented in two parts: a prefix that fills the current octet and an + * optional list of octets that are used if the integer value does not fit within the + * prefix. The number of bits of the prefix (called N) is a parameter of the integer + * representation. If the integer value is small enough, i.e., strictly less than 2N-1, it + * is encoded within the N-bit prefix. + * + * ... + * + * Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2N-1, is + * encoded using a list of one or more octets. 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. + * The remaining bits of the octets are used to encode the decreased value." + * + * Use "null" for name, if name isn't being provided (i.e. for a nameRef); otherwise, buffer + * will be too large. + * + */ + int N = 6; // bits available in first byte + int size = 1; + index -= Math.pow(2, N) - 1; // number that you can store in first N bits + while (index >= 0) { + index -= 127; + size++; + } + return ByteBuffer.allocate(size + 2); + } + + private ByteBuffer allocateNameRefBuffer(long index, CharSequence value) { + int N = 4; + return allocateNameRefBuffer(N, index, value); + } + + private ByteBuffer allocateNameRefBuffer(int N, long index, CharSequence value) { + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = 1 + vlen; + + index -= Math.pow(2, N) - 1; + while (index >= 0) { + index -= 127; + size++; + } + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size + 2); + } + + private ByteBuffer allocateLiteralBuffer(CharSequence name, CharSequence value) { + int N = 3; + return allocateLiteralBuffer(N, name, value); + } + + private ByteBuffer allocateLiteralBuffer(int N, CharSequence name, CharSequence value) { + int nlen = Math.min(QuickHuffman.lengthOf(name), name.length()); + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = nlen + vlen; + + nlen -= Math.pow(2, N) - 1; + size++; + while (nlen >= 0) { + nlen -= 127; + size++; + } + + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size + 2); + } + + static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum."""; + + private String randomString() { + int lower = random.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + /** + * The empty string ("") is a valid value String in the static table and the random + * String returned cannot refer to an entry in the table. Therefore, we set the upper + * bound below to a minimum of 1. + */ + return LOREM.substring(lower, 1 + lower + random.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestingCallBack implements DecodingCallback { + final long index; + final boolean huffmanName, huffmanValue; + final boolean sensitive; + final String name, value; + final List validIndices; + + // Indexed + TestingCallBack(long index, String name, String value) { + this(index, null, name, value, false); + } + // Literal w/Literal Name + TestingCallBack(String name, String value, boolean sensitive) { + this(-1L, null, name, value, sensitive); + } + // Literal w/Name Reference + TestingCallBack(List validIndices, String name, String value, boolean sensitive) { + this(-1L, validIndices, name, value, sensitive); + } + TestingCallBack(long index, List validIndices, String name, String value, boolean sensitive) { + this.index = index; + this.validIndices = validIndices; + this.huffmanName = QuickHuffman.isHuffmanBetterFor(name); + this.huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + this.sensitive = sensitive; + this.name = name; + this.value = value; + } + + @Override + public void onDecoded(CharSequence actualName, CharSequence value) { + fail("onDecoded should not be called"); + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + fail("Decoding error: " + http3Error, throwable); + } + + @Override + public long streamId() { + return 0; + } + + @Override + public void onIndexed(long actualIndex, CharSequence actualName, CharSequence actualValue) { + assertEquals(actualIndex, index); + assertEquals(actualName, name); + assertEquals(actualValue, value); + } + + @Override + public void onLiteralWithNameReference(long actualIndex, CharSequence actualName, + CharSequence actualValue, boolean huffmanValue, + boolean actualHideIntermediary) { + assertTrue(validIndices.contains(actualIndex)); + assertEquals(actualName.toString(), name); + assertEquals(actualValue.toString(), value); + assertEquals(huffmanValue, huffmanValue); + assertEquals(actualHideIntermediary, sensitive); + } + + @Override + public void onLiteralWithLiteralName(CharSequence actualName, boolean actualHuffmanName, + CharSequence actualValue, boolean actualHuffmanValue, + boolean actualHideIntermediary) { + assertEquals(actualName.toString(), name); + assertEquals(actualHuffmanName, huffmanName); + assertEquals(actualValue.toString(), value); + assertEquals(actualHuffmanValue, huffmanValue); + assertEquals(actualHideIntermediary, sensitive); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java new file mode 100644 index 00000000000..5c31762cdb3 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java @@ -0,0 +1,373 @@ +/* + * 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. + * + * 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 + * @key randomness + * @library /test/lib + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL EncoderInstructionsReaderTest + */ + +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import java.nio.ByteBuffer; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.writers.StringWriter; +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.RepeatedTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader.Callback; + +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +public class EncoderInstructionsReaderTest { + + EncoderInstructionsReader encoderInstructionsReader; + private static final Random RANDOM = RandomFactory.getRandom(); + + @RepeatedTest(5) + public void testCapacity() { + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Capacity (5+) | + // +---+---+---+-------------------+ + + // create logger and callback + QPACK.Logger logger = QPACK.getLogger().subLogger("testCapacity"); + TestCallback callback = new TestCallback(); + + // create a random value to be assigned as capacity + Long expectedCapacity = RANDOM.nextLong(IntegerReader.QPACK_MAX_INTEGER_VALUE); + + // create integerWriter, set expected size for the bytebuffer and write to it + IntegerWriter integerWriter = new IntegerWriter(); + int bufferSize = requiredBufferSize(5, expectedCapacity); + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + int payload = 0b0010_0000; + integerWriter.configure(expectedCapacity, 5, payload); + boolean result = integerWriter.write(byteBuffer); + + // assert that the writer finished and isn't expecting another bytebuffer + assert result; + + byteBuffer.flip(); + + // use EncoderInstructionReader and check it successfully reads the input + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + encoderInstructionsReader.read(byteBuffer, -1); + + long actualCapacity = callback.capacity.get(); + assertEquals(expectedCapacity, actualCapacity, "expected capacity differed from actual result"); + } + + @RepeatedTest(10) + public void testInsertWithNameReference() { + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Name Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + + QPACK.Logger logger = QPACK.getLogger() + .subLogger("testInsertWithNameReference"); + TestCallback callback = new TestCallback(); + + // Needs both an integer and String writer + IntegerWriter integerWriter = new IntegerWriter(); + StringWriter stringWriter = new StringWriter(); + boolean huffman = RANDOM.nextBoolean(); + boolean staticTable = RANDOM.nextBoolean(); + + int payload; + if (staticTable) { + payload = 0b1100_0000; + } else { + payload = 0b1000_0000; + } + + // get a random member of the dynamic table and create a random string to update it with + long index = RANDOM.nextLong(IntegerReader.QPACK_MAX_INTEGER_VALUE); + String value = randomString(); + + // calculate the size of the byteBuffer + int bufferSize = requiredBufferSize(6, index); + bufferSize += requiredBufferSize(7, value.length()); + + if (huffman) { + bufferSize += QuickHuffman.lengthOf(value); + } else { + bufferSize += value.length(); + } + + integerWriter.configure(index, 6, payload); + stringWriter.configure(value, huffman); + + boolean intWriterFinished = false; + boolean stringWriterFinished = false; + List byteBufferList = new ArrayList<>(); + + // Feed the writers with bytebuffers of random size until they total bufferSize, + // once each bytebuffer is full add it to byteBufferList to be read later + while (!stringWriterFinished) { + int randomSize = RANDOM.nextInt(0, bufferSize + 1); + bufferSize -= randomSize; + ByteBuffer byteBuffer = ByteBuffer.allocate(randomSize); + + if (!intWriterFinished) { + intWriterFinished = integerWriter.write(byteBuffer); + if (!intWriterFinished) { + // writer not finished, add bytebuffer to list of full bytebuffers + // then loop + byteBuffer.flip(); + byteBufferList.add(byteBuffer); + continue; + } + } + + // this stage should only be reached if the intWriter is finished + stringWriterFinished = stringWriter.write(byteBuffer); + byteBuffer.flip(); + byteBufferList.add(byteBuffer); + } + + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + for (var byteBuffer : byteBufferList) { + encoderInstructionsReader.read(byteBuffer, -1); + } + + assertEquals(index, callback.indexInsert.nameIndex); + assertEquals(value, callback.indexInsert.value); + assertEquals(staticTable, callback.indexInsert.staticTable); + } + + @RepeatedTest(10) + public void testInsertWithLiteralName() { + + // 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) | + // +-------------------------------+ + + QPACK.Logger logger = QPACK.getLogger() + .subLogger("testInsertWithLiteralName"); + TestCallback callback = new TestCallback(); + + StringWriter stringWriter = new StringWriter(); + boolean huffman = RANDOM.nextBoolean(); + + int payload; + if (huffman) { + payload = 0b0110_0000; // static table = true + } else { + payload = 0b0100_0000; // static table = false + } + + //generate name and value then calculate the size required for the bytebuffer + String name = randomString(); + String value = randomString(); + + int bufferSize = requiredBufferSize(5, name.length()); + bufferSize += requiredBufferSize(7, value.length()); + bufferSize += value.length() + name.length(); + + List byteBuffers = new ArrayList<>(); + + // configure the stringWriter to take name + stringWriter.configure(name, 5, payload, huffman); + + boolean firstStringWriterFinished = false; + boolean secondStringWriterFinished = false; + + // Feed the writers with bytebuffers of random size until they total bufferSize, + // once each bytebuffer is full add it to byteBufferList to be read later + while (!secondStringWriterFinished) { + int randomSize = RANDOM.nextInt(0, bufferSize + 1); + bufferSize -= randomSize; + + ByteBuffer byteBuffer = ByteBuffer.allocate(randomSize); + + if (!firstStringWriterFinished) { + firstStringWriterFinished = stringWriter.write(byteBuffer); + + if (!firstStringWriterFinished) { + // writer not finished, add bytebuffer to array of full bytebuffers + // then loop + byteBuffer.flip(); + byteBuffers.add(byteBuffer); + continue; + } else { + // if the name has been written then reset the stringWriter + // so that it can be reused for value + stringWriter.reset(); + stringWriter.configure(value, huffman); + } + } + + // this stage should only be reached if the name has already been written + secondStringWriterFinished = stringWriter.write(byteBuffer); + byteBuffer.flip(); + byteBuffers.add(byteBuffer); + } + + System.err.println(name + " Attempting to insert value: " + value); + + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + + for (var byteBuffer : byteBuffers) { + encoderInstructionsReader.read(byteBuffer, -1); + } + + assertEquals(name, callback.lastInsert.name); + assertEquals(value, callback.lastInsert.value); + } + + @RepeatedTest(5) + public void testDuplicate() { + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | Index (5+) | + // +---+---+---+-------------------+ + // + + QPACK.Logger logger = QPACK.getLogger() + .subLogger("testDuplicate"); + TestCallback callback = new TestCallback(); + + long index = RANDOM.nextLong(0, DT_NAMES.size()); + + IntegerWriter integerWriter = new IntegerWriter(); + int bufferSize = requiredBufferSize(5, index); + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + int payload = 0b0000_0000; + integerWriter.configure(index, 5, payload); + boolean result = integerWriter.write(byteBuffer); + assert result; + + byteBuffer.flip(); + + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + encoderInstructionsReader.read(byteBuffer, -1); + + assertEquals(index, callback.duplicate.get()); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + 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; + } + + private static final int TEST_STR_MAX_LENGTH = 20; + private static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum.""" + .replaceAll(" ", "") + .replaceAll("\\W", ""); + + private static final List DT_NAMES = generateTableNames(40); + + private static List generateTableNames(int count) { + return IntStream.range(0, count) + .boxed() + .map(i -> randomString()) + .toList(); + } + + private static String randomString() { + int lower = RANDOM.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + return LOREM.substring(lower, 1 + lower + RANDOM.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestCallback implements Callback { + + record LiteralInsert(String name, String value) { + } + + record IndexedInsert(boolean staticTable, Long nameIndex, String value) { + } + + final AtomicLong capacity = new AtomicLong(-1L); + final AtomicLong duplicate = new AtomicLong(-1L); + LiteralInsert lastInsert; + + IndexedInsert indexInsert; + + @Override + public void onCapacityUpdate(long capacity) { + this.capacity.set(capacity); + } + + @Override + public void onInsert(String name, String value) { + lastInsert = new LiteralInsert(name, value); + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + indexInsert = new IndexedInsert(indexInStaticTable, nameIndex, valueString); + } + + @Override + public void onDuplicate(long duplicateValue) { + this.duplicate.set(duplicateValue); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java new file mode 100644 index 00000000000..fd1fc41d8f7 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java @@ -0,0 +1,343 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader.Callback; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.test.lib.RandomFactory; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; +import static org.junit.jupiter.api.Assertions.*; + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL EncoderInstructionsWriterTest + */ + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class EncoderInstructionsWriterTest { + + @RepeatedTest(10) + public void tableCapacityInstructionTest() throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("tableCapacityInstructionTest"); + var dynamicTable = new DynamicTable(logger); + // Generate random capacity value + long capacity = RANDOM.nextLong(IntegerReader.QPACK_MAX_INTEGER_VALUE); + logger.log(System.Logger.Level.TRACE, "Capacity value = " + capacity); + + // Initial dynamic table capacity - required to check for + // writer changing the capacity once the instruction is written. + long initialTableCapacity = capacity == 1234L ? 1235L : 1234L; + + // Create and configure encoder instruction writer for writing the table + // capacity update instruction + var encoderInstructionsWriter = new EncoderInstructionsWriter(); + int calculatedInstructionSize = + encoderInstructionsWriter.configureForTableCapacityUpdate(capacity); + + // Set max capacity to maximum possible value + dynamicTable.setMaxTableCapacity(IntegerReader.QPACK_MAX_INTEGER_VALUE); + + // Create dynamic table with initial capacity + dynamicTable.setCapacity(initialTableCapacity); + + // Perform write operation and then read the capacity update instruction + var callback = new TestEncoderInstructionsCallback(dynamicTable); + + int bytesWritten = writeThenReadInstruction(encoderInstructionsWriter, + callback, -1, dynamicTable, + (dt) -> dt.capacity() == initialTableCapacity, + logger); + + // We expect here to get a callback with the capacity value supplied + // to the instruction writer + assertEquals(capacity, callback.capacityFromCallback.get()); + + // We don't expect dynamic table capacity to be updated by the encoder + // instruction reader + assertNotEquals(capacity, dynamicTable.capacity()); + + // Check if size calculated by the EncoderInstructionsWriter matches + // the number of bytes written to the byte buffers + assertEquals(calculatedInstructionSize, bytesWritten); + } + + @ParameterizedTest + @MethodSource("nameReferenceInsertSource") + public void insertWithNameReferenceInstructionTest(boolean referencingStatic, + long nameIndex, int byteBufferSize) + throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("insertWithNameReferenceInstructionTest"); + var dynamicTable = dynamicTable(logger); + + // generate random value String + String value = randomString(); + TableEntry entry = new TableEntry(referencingStatic, nameIndex, "", value, NAME); + + // Create and configure encoder instruction writer for writing + // the "Insert With Name Reference" instruction + var writer = new EncoderInstructionsWriter(); + int calculatedInstructionSize = writer.configureForEntryInsertion(entry); + + // Perform write operation and then read back the insert entry instruction + var callback = new TestEncoderInstructionsCallback(dynamicTable); + int bytesWritten = writeThenReadInstruction(writer, callback, byteBufferSize, + dynamicTable, (dt) -> true, logger); + + // Check that reader callback values match values supplied to the writer + assertEquals(nameIndex, callback.lastNameInsert.index()); + assertEquals(value, callback.lastNameInsert.value()); + assertEquals(referencingStatic, callback.lastNameInsert.isStaticTable()); + + // Check if size calculated by the EncoderInstructionsWriter matches + // the number of bytes written to the byte buffers + assertEquals(calculatedInstructionSize, bytesWritten); + } + + private static Stream nameReferenceInsertSource() { + Stream staticTableCases = + RANDOM.longs(10, 0, + StaticTable.HTTP3_HEADER_FIELDS.size()) + .boxed() + .map(index -> Arguments.of(true, index, + RANDOM.nextInt(1, 65))); + Stream dynamicTableCases = + RANDOM.longs(10, 0, + DT_NAMES.size()) + .boxed() + .map(index -> Arguments.of(false, index, + RANDOM.nextInt(1, 65))); + return Stream.concat(staticTableCases, dynamicTableCases); + } + + @RepeatedTest(10) + public void insertWithLiteralInstructionTest() throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("insertWithLiteralInstructionTest"); + var dynamicTable = dynamicTable(logger); + + // Generate random strings for name:value entry + String name = randomString(); + String value = randomString(); + var tableEntry = new TableEntry(name, value); + // Create and configure encoder instruction writer for writing the "Insert With Literal Name" + // instruction + var writer = new EncoderInstructionsWriter(); + int calculatedInstructionSize = writer.configureForEntryInsertion(tableEntry); + + var callback = new TestEncoderInstructionsCallback(dynamicTable); + int writtenBytes = writeThenReadInstruction(writer, callback, -1, + dynamicTable, (dt) -> true, logger); + + // Check that reader callback values match values supplied to the writer + assertEquals(name, callback.lastLiteralInsert.name()); + assertEquals(value, callback.lastLiteralInsert.value()); + // Check if size calculated by the EncoderInstructionsWriter matches the number of + // bytes written to the byte buffers + assertEquals(calculatedInstructionSize, writtenBytes); + } + + @RepeatedTest(10) + public void duplicateInstructionTest() throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("duplicateInstructionTest"); + var dynamicTable = dynamicTable(logger); + // Absolute id to duplicate + // size() - 1 is used to not duplicate the last entry, otherwise the + // DynamicTable.duplicate(relativeIndex) checks below will fail + long idToDuplicate = RANDOM.nextLong(0, DT_NAMES.size() - 1); + // Create and configure encoder instruction writer for writing the "Duplicate" + // instruction + var writer = new EncoderInstructionsWriter(); + // insert count - 1 is the head element index + long relativeIndex = dynamicTable.insertCount() - 1 - idToDuplicate; + int calculatedInstructionSize = writer.configureForEntryDuplication(relativeIndex); + var callback = new TestEncoderInstructionsCallback(dynamicTable); + int writtenBytes = writeThenReadInstruction(writer, callback, -1, + dynamicTable, (dt) -> true, logger); + HeaderField original = dynamicTable.get(idToDuplicate); + HeaderField head = dynamicTable.getRelative(0); + + // Check that reader callback values match values supplied to the writer + assertEquals(relativeIndex, callback.duplicateIdFromCallback.get()); + // Check that DynamicTable.duplicate(relativeIndex) properly recreates + // the referenced entry + assertEquals(head.name(), original.name()); + assertEquals(head.value(), original.value()); + // Check if size calculated by the EncoderInstructionsWriter matches the number + // of bytes written to the byte buffers + assertEquals(calculatedInstructionSize, writtenBytes); + + } + + // Test runner method that writes an instruction with the supplied encoder + // instructions writer pre-configured for writing of a specific instruction. + private static int writeThenReadInstruction( + EncoderInstructionsWriter writer, TestEncoderInstructionsCallback callback, + int bufferSize, DynamicTable dynamicTable, + Function partialWriteCheck, + QPACK.Logger logger) throws Exception { + var buffers = new ArrayList(); + boolean writeDone = false; + int writtenBytes = 0; + // Write instruction to the list of byte buffers + while (!writeDone) { + int allocSize = bufferSize == -1 ? RANDOM.nextInt(1, 65) : bufferSize; + var buffer = ByteBuffer.allocate(allocSize); + writeDone = writer.write(buffer); + writtenBytes += buffer.position(); + if (!writeDone && !partialWriteCheck.apply(dynamicTable)) { + Assert.fail("Wrong dynamic table state after partial write"); + } + buffer.flip(); + buffers.add(buffer); + } + + // Read back the data from byte buffers + var encoderInstructionReader = new EncoderInstructionsReader(callback, logger); + + // Read out an instruction and return the callback instance + for (var bb : buffers) { + encoderInstructionReader.read(bb, -1); + } + return writtenBytes; + } + + private static final Random RANDOM = RandomFactory.getRandom(); + private static final int TEST_STR_MAX_LENGTH = 20; + private static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum.""" + .replaceAll(" ", "") + .replaceAll("\\W", ""); + + private static final List DT_NAMES = generateTableNames(40); + + private static List generateTableNames(int count) { + return IntStream.range(0, count) + .boxed() + .map(i -> randomString()) + .toList(); + } + + private static DynamicTable dynamicTable(QPACK.Logger logger) { + var dt = new DynamicTable(logger.subLogger("dynamicTable")); + dt.setMaxTableCapacity(4096); + dt.setCapacity(4096); + for (var name : DT_NAMES) { + dt.insert(name, randomString()); + } + return dt; + } + + private static String randomString() { + int lower = RANDOM.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + return LOREM.substring(lower, 1 + lower + RANDOM.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestEncoderInstructionsCallback implements Callback { + + final DynamicTable dynamicTable; + public TestEncoderInstructionsCallback(DynamicTable dynamicTable) { + this.dynamicTable = dynamicTable; + } + + record LiteralInsert(String name, String value) { + } + + record IndexedNameInsert(boolean isStaticTable, long index, String value) { + } + + final AtomicLong capacityFromCallback = new AtomicLong(-1L); + final AtomicLong duplicateIdFromCallback = new AtomicLong(-1L); + LiteralInsert lastLiteralInsert; + IndexedNameInsert lastNameInsert; + + @Override + public void onCapacityUpdate(long capacity) { + capacityFromCallback.set(capacity); + } + + @Override + public void onInsert(String name, String value) { + lastLiteralInsert = new LiteralInsert(name, value); + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + lastNameInsert = new IndexedNameInsert(indexInStaticTable, nameIndex, valueString); + } + + @Override + public void onDuplicate(long l) { + dynamicTable.duplicate(l); + duplicateIdFromCallback.set(l); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderTest.java b/test/jdk/java/net/httpclient/qpack/EncoderTest.java new file mode 100644 index 00000000000..a80a0899420 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderTest.java @@ -0,0 +1,670 @@ +/* + * 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. + * + * 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. + */ +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.hpack.QuickHuffman; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.writers.StringWriter; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.TerminationCause; +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.QuicSenderStream.SendingStreamState; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.quic.QuicTLSEngine; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.testng.Assert.*; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng/othervm EncoderTest + */ +public class EncoderTest { + private final Random random = new Random(); + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter stringWriter = new StringWriter(); + private static final int TEST_STR_MAX_LENGTH = 10; + + @DataProvider(name = "indexProvider") + public Object[][] indexProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(headerField -> List.of(tableIndex.getAndIncrement(), headerField)) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider + public Object[][] staticNameReferenceProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + Map> map = new HashMap<>(); + for (var headerField : StaticTable.HTTP3_HEADER_FIELDS) { + var name = headerField.name(); + var index = tableIndex.getAndIncrement(); + if (!map.containsKey(name)) + map.put(name, new ArrayList<>()); + map.get(name).add(index); + } + return map.entrySet().stream() + .map(e -> List.of(e.getKey(), randomString(), e.getValue())) + .map(List::toArray).toArray(Object[][]::new); + } + + @DataProvider + public Object[][] literalsProvider() { + var output = new String[100][]; + for (int i = 0; i < 100; i++) { + output[i] = new String[]{ randomString(), randomString() }; + } + return output; + } + + private static class TestErrorHandler implements + UniStreamPair.StreamErrorHandler { + final Consumer handler; + private TestErrorHandler(Consumer handler) { + this.handler = handler; + } + @Override + public void onError(QuicStream stream, UniStreamPair uniStreamPair, Throwable throwable) { + handler.accept(throwable); + } + public static TestErrorHandler of(Consumer handler) { + return new TestErrorHandler(handler); + } + } + + QueuingStreamPair createEncoderStreams(Consumer receiver, + Consumer errorHandler, + TestQuicConnection quicConnection) { + return new QueuingStreamPair(StreamType.QPACK_ENCODER, + quicConnection, + receiver, + TestErrorHandler.of(errorHandler), + Utils.getDebugLogger(() -> "quic-encoder-test") + ); + } + + private void assertNotFailed(AtomicReference errorRef) { + var error = errorRef.get(); + if (error != null) throw new AssertionError(error); + } + + private static void qpackErrorHandler(Throwable error, Http3Error http3Error) { + fail(http3Error + "QPACK error:" + http3Error, error); + } + + @Test(dataProvider = "indexProvider") + public void testFieldLineWriterWithStaticIndex(int index, HeaderField h) { + var actual = allocateIndexBuffer(index); + var expected = writeIndex(index); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> false, + (receiver) -> createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + + encoder.header(context, h.name(), h.value(), false); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + assertEquals(actual, expected, debug(h.name(), h.value(), actual, expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "staticNameReferenceProvider") + public void testInsertWithStaticTableNameReference(String name, String value, List validIndices) { + int index = Collections.max(validIndices); + + var actual = allocateInsertNameRefBuffer(index, value); + var expected = writeInsertNameRef(value, validIndices); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> true, + (receiver) -> createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + configureDynamicTableSize(encoder); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, false); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + TestQuicStreamWriter quicStreamWriter = quicConnection.sender.writer; + + assertTrue(expected.contains(quicStreamWriter.get()), debug(name, value, quicStreamWriter.get(), expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "staticNameReferenceProvider") + public void testFieldLineWithStaticTableNameReference(String name, String value, List validIndices) { + int index = Collections.max(validIndices); + boolean sensitive = random.nextBoolean(); + + var actual = allocateNameRefBuffer(index, value); + var expected = writeNameRef(sensitive, value, validIndices); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> false, (receiver) -> + createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, sensitive); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + assertTrue(expected.contains(actual), debug(name, value, actual, expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "literalsProvider") + public void testInsertWithLiterals(String name, String value) { + var expected = writeInsertLiteral(name, value); + var actual = allocateInsertLiteralBuffer(name, value); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> true, (receiver) -> + createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + configureDynamicTableSize(encoder); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, false); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + TestQuicStreamWriter quicStreamWriter = quicConnection.sender.writer; + assertEquals(quicStreamWriter.get(), expected, debug(name, value, quicStreamWriter.get(), expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "literalsProvider") + public void testFieldLineEncodingWithLiterals(String name, String value) { + boolean sensitive = random.nextBoolean(); + + var expected = writeLiteral(sensitive, name, value); + var actual = allocateLiteralBuffer(name, value); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> false, (receiver) -> + createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, sensitive); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + assertEquals(actual, expected, debug(name, value, actual, expected)); + assertNotFailed(error); + } + + // Test cases which test insertion of entries to the dynamic need to have + // dynamic table with non-zero capacity + private static void configureDynamicTableSize(Encoder encoder) { + // Set encoder maximum dynamic table capacity + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 256); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + encoder.configure(settings); + // Set dynamic table capacity that doesn't exceed the max capacity value + encoder.setTableCapacity(256); + } + + /* Test Methods */ + private class TestQuicStreamWriter extends QuicStreamWriter { + volatile ByteBuffer b = null; + final TestQuicSenderStream sender; + + TestQuicStreamWriter(SequentialScheduler scheduler, TestQuicSenderStream sender) { + super(scheduler); + this.sender = sender; + } + + private void write(ByteBuffer bb) { + b = bb; + } + public ByteBuffer get() { + if (b == null) { + fail("TestQuicStreamWriter buffer is null"); + } + return b; + } + @Override + public SendingStreamState sendingState() { return null;} + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + write(buffer); + } + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + write(buffer); + } + @Override + public long credit() { return Long.MAX_VALUE;} + @Override + public void reset(long errorCode) {} + @Override + public QuicSenderStream stream() { + return connected() ? sender : null; + } + @Override + public boolean connected() { + return sender.writer == this; + } + } + + private class TestQuicConnection extends QuicConnection { + final TestQuicSenderStream sender = new TestQuicSenderStream(); + @Override + public boolean isOpen() {return true;} + @Override + public TerminationCause terminationCause() {return null;} + @Override + public QuicTLSEngine getTLSEngine() {return null;} + @Override + public InetSocketAddress peerAddress() {return null;} + @Override + public SocketAddress localAddress() {return null;} + @Override + public CompletableFuture startHandshake() {return null;} + @Override + public CompletableFuture openNewLocalBidiStream(Duration duration) { + return null; + } + @Override + public CompletableFuture openNewLocalUniStream(Duration duration) { + return MinimalFuture.completedFuture(sender); + } + @Override + public void addRemoteStreamListener(Predicate streamConsumer) { + } + @Override + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + return false; + } + @Override + public Stream quicStreams() { + return null; + } + @Override + public CompletableFuture handshakeReachedPeer() { + return MinimalFuture.completedFuture(null); + } + @Override + public CompletableFuture requestSendPing() { + return MinimalFuture.completedFuture(-1L); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return null; + } + + @Override + public String dbgTag() { return null; } + + @Override + public String logTag() { + return null; + } + } + + class TestQuicSenderStream implements QuicSenderStream { + private static AtomicLong ids = new AtomicLong(); + private final long id; + TestQuicStreamWriter writer; + TestQuicSenderStream() { + id = ids.getAndIncrement() * 4 + type(); + } + @Override + public SendingStreamState sendingState() { return SendingStreamState.READY; } + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return writer = new TestQuicStreamWriter(scheduler, this); + } + @Override + public void disconnectWriter(QuicStreamWriter writer) { } + @Override + public void reset(long errorCode) { } + @Override + public long dataSent() { return 0; } + @Override + public long streamId() { return id; } + @Override + public StreamMode mode() { return null; } + @Override + public boolean isClientInitiated() { return true; } + @Override + public boolean isServerInitiated() { return false; } + @Override + public boolean isBidirectional() { return false; } + @Override + public boolean isLocalInitiated() { return true; } + @Override + public boolean isRemoteInitiated() { return false; } + @Override + public int type() { return 0x02; } + @Override + public StreamState state() { return SendingStreamState.READY; } + + @Override + public long sndErrorCode() { return -1; } + @Override + public boolean stopSendingReceived() { return false; } + } + + private String debug(String name, String value, ByteBuffer actual, ByteBuffer expected) { + return debug(name, value, actual, List.of(expected)); + } + + private String debug(String name, String value, ByteBuffer actual, List expected) { + var output = new StringBuilder(); + output.append("\n\nBUFFER CONTENTS\n"); + output.append("----------------\n"); + output.append("DEBUG[NAME]: %s\nDEBUG[VALUE]: %s\n".formatted(name, value)); + output.append("DEBUG[ACTUAL]: "); + for (byte b : actual.array()) { + output.append("(%s,%d) ".formatted(Integer.toBinaryString(b & 0xFF), (int)(b & 0xFF))); + } + output.append("\n"); + + output.append("DEBUG[EXPECTED]: "); + for (var bb : expected) { + for (byte b : bb.array()) { + output.append("(%s,%d) ".formatted(Integer.toBinaryString(b & 0xFF), (int) (b & 0xFF))); + } + output.append("\n"); + } + return output.toString(); + } + + private ByteBuffer writeIndex(int index) { + int N = 6; + int payload = 0b1100_0000; // use static table = true; + var bb = ByteBuffer.allocate(2); + + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + bb.flip(); + return bb; + } + + private List writeNameRef(boolean sensitive, String value, List validIndices) { + int N = 4; + int payload = 0b0101_0000; // static table = true + return writeNameRef(N, payload, sensitive, value, validIndices); + } + + private List writeInsertNameRef(String value, List validIndices) { + int N = 6; + int payload = 0b1100_0000; // static table = true + return writeNameRef(N, payload, false, value, validIndices); + } + + private List writeNameRef(int N, int payload, boolean sensitive, String value, List validIndices) { + // Each Header name may have several valid indices associated with it. + List output = new ArrayList<>(); + for (int index : validIndices) { + if (sensitive) + payload |= 0b0010_0000; + var bb = allocateNameRefBuffer(N, index, value); + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + boolean huffman = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffman) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffman); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + output.add(bb); + } + + return output; + } + + private ByteBuffer writeInsertLiteral(String name, String value) { + int N = 5; + int payload = 0b0100_0000; + boolean huffmanName = QuickHuffman.isHuffmanBetterFor(name); + if (huffmanName) + payload |= 0b0010_0000; + return writeLiteral(N, payload, name, value); + } + + private ByteBuffer writeLiteral(boolean sensitive, String name, String value) { + int N = 3; + int payload = 0b0010_0000; // static table = true + if (sensitive) + payload |= 0b0001_0000; + + if (QuickHuffman.isHuffmanBetterFor(name)) + payload |= 0b0000_1000; + return writeLiteral(N, payload, name, value); + } + + private ByteBuffer writeLiteral(int N, int payload, String name, String value) { + var bb = allocateLiteralBuffer(N, name, value); + + boolean huffmanName = QuickHuffman.isHuffmanBetterFor(name); + stringWriter.configure(name, N, payload, huffmanName); + stringWriter.write(bb); + stringWriter.reset(); + + boolean huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffmanValue) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffmanValue); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer allocateIndexBuffer(int index) { + /* + * Note on Integer Representation used for storing the length of name and value strings. + * Taken from RFC 7541 Section 5.1 + * + * "An integer is represented in two parts: a prefix that fills the current octet and an + * optional list of octets that are used if the integer value does not fit within the + * prefix. The number of bits of the prefix (called N) is a parameter of the integer + * representation. If the integer value is small enough, i.e., strictly less than 2N-1, it + * is encoded within the N-bit prefix. + * + * ... + * + * Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2N-1, is + * encoded using a list of one or more octets. 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. + * The remaining bits of the octets are used to encode the decreased value." + * + * Use "null" for name, if name isn't being provided (i.e. for a nameRef); otherwise, buffer + * will be too large. + * + */ + int N = 6; // bits available in first byte + int size = 1; + index -= Math.pow(2, N) - 1; // number that you can store in first N bits + while (index >= 0) { + index -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateInsertNameRefBuffer(int index, CharSequence value) { + int N = 6; + return allocateNameRefBuffer(N, index, value); + } + + private ByteBuffer allocateNameRefBuffer(int index, CharSequence value) { + int N = 4; + return allocateNameRefBuffer(N, index, value); + } + + private ByteBuffer allocateNameRefBuffer(int N, int index, CharSequence value) { + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = 1 + vlen; + + index -= Math.pow(2, N) - 1; + while (index >= 0) { + index -= 127; + size++; + } + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateInsertLiteralBuffer(CharSequence name, CharSequence value) { + int N = 5; + return allocateLiteralBuffer(N, name, value); + } + + private ByteBuffer allocateLiteralBuffer(CharSequence name, CharSequence value) { + int N = 3; + return allocateLiteralBuffer(N, name, value); + } + + private ByteBuffer allocateLiteralBuffer(int N, CharSequence name, CharSequence value) { + int nlen = Math.min(QuickHuffman.lengthOf(name), name.length()); + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = nlen + vlen; + + nlen -= Math.pow(2, N) - 1; + size++; + while (nlen >= 0) { + nlen -= 127; + size++; + } + + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum."""; + + private String randomString() { + int lower = random.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + /** + * The empty string ("") is a valid value String in the static table and the random + * String returned cannot refer to a entry in the table. Therefore, we set the upper + * bound below to a minimum of 1. + */ + return LOREM.substring(lower, 1 + lower + random.nextInt(TEST_STR_MAX_LENGTH)); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java b/test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java new file mode 100644 index 00000000000..c295ec5d01d --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java @@ -0,0 +1,190 @@ +/* + * 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. + * + * 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 + * @summary test dynamic table entry eviction scenarios + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA EntriesEvictionTest + */ + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder.SectionReference; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPACK.Logger; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class EntriesEvictionTest { + + @Test(dataProvider = "evictionScenarios") + public void evictionInsertionTest(TestHeader headerToAdd, + SectionReference sectionReference, + long insertedId, + long largestEvictedId) { + Logger logger = QPACK.getLogger().subLogger("evictionInsertionTest"); + DynamicTable dynamicTable = new DynamicTable(logger); + + dynamicTable.setMaxTableCapacity(TABLE_CAPACITY); + dynamicTable.setCapacity(TABLE_CAPACITY); + + for (TestHeader header : TEST_HEADERS) { + dynamicTable.insert(header.name, header.value); + } + + // Insert last entry + long id = dynamicTable.insert(headerToAdd.name, headerToAdd.value, sectionReference); + + Assert.assertEquals(id, insertedId); + + if (largestEvictedId != -1) { + // Check that evicted entry with the largest absolute index + // is not accessible + Assert.assertThrows(() -> dynamicTable.get(largestEvictedId)); + // Check that an entry after that can be acquired with its + // absolute index + dynamicTable.get(largestEvictedId + 1); + } + + if (insertedId != -1) { + HeaderField insertedField = dynamicTable.get(insertedId); + Assert.assertEquals(insertedField, + new HeaderField(headerToAdd.name(), headerToAdd.value())); + } + } + + @DataProvider + public static Object[][] evictionScenarios() { + + // Header that requires only one entry to be evicted + String oneSizedValue = HEADER_PREXIX + String.format("%03d", HEADERS_COUNT); + TestHeader headerToAddWithOneEviction = TestHeader.newHeader( + oneSizedValue, oneSizedValue); + + // Header that requires two entries to be evicted + // "e" is repeated 16 times to compensate 32 bytes - 16 in header name, + // another 16 in header value + String doubleSizedHeaderValue = (HEADER_PREXIX + "Dbl").repeat(2) + + "e".repeat(16); + TestHeader headerToAddWithTwoEvictions = TestHeader.newHeader( + doubleSizedHeaderValue, doubleSizedHeaderValue); + + // Construct header with size equals to the dynamic table capacity + // / 2 since the string used two times - for headers name and value + String hugeStrPart1 = TEST_HEADERS.stream().map(TestHeader::name) + .collect(Collectors.joining()); + String hugeStrToCompensate32PerElement = "a".repeat(32 * (HEADERS_COUNT - 1) / 2); + String hugeStr = hugeStrPart1 + hugeStrToCompensate32PerElement; + + TestHeader hugeEntryWithAllEviction = TestHeader.newHeader(hugeStr, hugeStr); + + // Header with size 2 bytes bigger than the dynamic table capacity + TestHeader hugeEntryExceedsCapacity = TestHeader.newHeader( + hugeStr + "H", hugeStr + "H"); + + return new Object[][]{ + // Evict one to have space for a new entry + {headerToAddWithOneEviction, SectionReference.noReferences(), + HEADERS_COUNT, 0}, + + // Evict all entries to have space for a new entry + {hugeEntryWithAllEviction, SectionReference.noReferences(), + HEADERS_COUNT, HEADERS_COUNT - 1}, + + // Not enough capacity for a new entry even if all entries are evicted + {hugeEntryExceedsCapacity, SectionReference.noReferences(), + -1, -1}, + + // Entry with size == capacity and there are section references preventing + // eviction of all entries + {hugeEntryWithAllEviction, new SectionReference(0, 1), + -1, -1}, + + // Element with 0 absolute id is not referenced and therefore can be evicted + {headerToAddWithOneEviction, new SectionReference(1, 2), + HEADERS_COUNT, 0}, + + // Elements with 0 and 1 ids are not referenced and should be + // evicted to insert double-sized entry + {headerToAddWithTwoEvictions, new SectionReference(2, 3), + HEADERS_COUNT, 1}, + + // Element with 1 id cannot be evicted since it is + // referenced + {headerToAddWithTwoEvictions, new SectionReference(1, 3), + -1, -1} + }; + } + + record TestHeader(String name, String value, long size) { + public static TestHeader newHeader(String name, String value) { + return new TestHeader(name, value, 32L + name.length() + value.length()); + } + + @Override + public String toString() { + return name + ":" + value + "[" + size + "]"; + } + } + + // Number of headers to insert before running an eviction scenario + private static final int HEADERS_COUNT = 3; + // Test header prefix + private static final String HEADER_PREXIX = "HeaderPrefix"; + // List of headers to insert before running an eviction scenario + private static final List TEST_HEADERS; + // Table capacity required by test scenarios + private static final long TABLE_CAPACITY; + + static { + List testHeaders = new ArrayList<>(); + long capacity = 0; + + // List of headers to prepopulate dynamic table before running + // test cases + for (int i = 0; i < HEADERS_COUNT; i++) { + String headerStr = HEADER_PREXIX + String.format("%03d", i); + var header = TestHeader.newHeader(headerStr, headerStr); + capacity += header.size(); + testHeaders.add(header); + } + TEST_HEADERS = testHeaders; + TABLE_CAPACITY = capacity; + } +} diff --git a/test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java b/test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java new file mode 100644 index 00000000000..e9a7a86e975 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java @@ -0,0 +1,133 @@ +/* + * 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. + * + * 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 + * @modules java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng FieldSectionPrefixTest + */ + + +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.QPACK; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.FieldLineSectionPrefixWriter; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicLong; + +public class FieldSectionPrefixTest { + + private static final long DT_CAPACITY = 220L; + private static final long MAX_ENTRIES = DT_CAPACITY / 32L; + + @Test(dataProvider = "encodingCases") + public void encodingTest(long base, long requiredInsertCount, + byte expectedRic, byte expectedBase) { + var fieldSectionPrefix = new FieldSectionPrefix(requiredInsertCount, base); + FieldLineSectionPrefixWriter writer = new FieldLineSectionPrefixWriter(); + int bytesNeeded = writer.configure(fieldSectionPrefix, MAX_ENTRIES); + var byteBuffer = ByteBuffer.allocate(bytesNeeded); + writer.write(byteBuffer); + byteBuffer.flip(); + Assert.assertEquals(byteBuffer.get(0), expectedRic); + Assert.assertEquals(byteBuffer.get(1), expectedBase); + } + + @DataProvider(name = "encodingCases") + public Object[][] encodingCases() { + var cases = new ArrayList(); + // Simple with 0 values + cases.add(new Object[]{0L, 0L, (byte) 0x0, (byte) 0x0}); + // Based on RFC-9204: "B.2. Dynamic Table example" + cases.add(new Object[]{0L, 2L, (byte) 0x3, (byte) 0x81}); + // Based on RFC-9204: "Duplicate Instruction, Stream Cancellation" + cases.add(new Object[]{4L, 4L, (byte) 0x5, (byte) 0x0}); + return cases.toArray(Object[][]::new); + } + + @Test(dataProvider = "decodingCases") + public void decodingTest(long expectedRIC, long expectedBase, byte... bytes) throws IOException { + var logger = QPACK.getLogger().subLogger("decodingTest"); + var dt = new DynamicTable(logger, false); + dt.setMaxTableCapacity(DT_CAPACITY); + dt.setCapacity(DT_CAPACITY); + var callback = new DecodingCallback() { + @Override + public void onDecoded(CharSequence name, CharSequence value) { + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + throw new RuntimeException("Error during Field Line Section Prefix decoding - " + + http3Error + ": " + throwable.getMessage()); + } + + @Override + public long streamId() { + return 0; + } + }; + AtomicLong blockedStreamsCounter = new AtomicLong(); + // maxBlockStreams = 1 is needed for tests with Required Insert Count > 0 - otherwise test + // fails with "QPACK_DECOMPRESSION_FAILED: too many blocked streams" + HeaderFrameReader reader = new HeaderFrameReader(dt, callback, blockedStreamsCounter, + 1, -1, logger); + var bb = ByteBuffer.wrap(bytes); + reader.read(bb, false); + var fsp = reader.decodedSectionPrefix(); + + System.err.println("Required Insert Count:" + fsp.requiredInsertCount()); + System.err.println("Base:" + fsp.base()); + Assert.assertEquals(fsp.requiredInsertCount(), expectedRIC); + Assert.assertEquals(fsp.base(), expectedBase); + + } + + @DataProvider(name = "decodingCases") + public Object[][] decodingCases() { + var cases = new ArrayList(); + cases.add(new Object[]{0L, 0L, (byte) 0x0, (byte) 0x0}); + cases.add(new Object[]{4L, 4L, (byte) 0x5, (byte) 0x0}); + cases.add(new Object[]{2L, 0L, (byte) 0x3, (byte) 0x81}); + return cases.toArray(Object[][]::new); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java b/test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java new file mode 100644 index 00000000000..6f21549f497 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java @@ -0,0 +1,84 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.stream.IntStream; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=INFO + * IntegerReaderMaxValuesTest + */ +public class IntegerReaderMaxValuesTest { + @DataProvider + public Object[][] nValues() { + return IntStream.range(1, 8) + .boxed() + .map(N -> new Object[]{N}) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "nValues") + public void maxIntegerWriteRead(int N) { + IntegerWriter writer = new IntegerWriter(); + writer.configure(IntegerReader.QPACK_MAX_INTEGER_VALUE, N, 0); + ByteBuffer buffer = ByteBuffer.allocate(1024); + writer.write(buffer); + IntegerReader reader = new IntegerReader(); + reader.configure(N); + buffer.flip(); + reader.read(buffer); + long result = reader.get(); + Assert.assertEquals(result, IntegerReader.QPACK_MAX_INTEGER_VALUE); + } + + @Test(dataProvider = "nValues", expectedExceptions = QPackException.class) + public void overflowInteger(int N) { + // Construct buffer with overflowed integer + ByteBuffer overflowBuffer = ByteBuffer.allocate(11); + + overflowBuffer.put((byte) ((2 << (N - 1)) - 1)); + for (int i = 0; i < 9; i++) { + overflowBuffer.put((byte) 128); + } + overflowBuffer.put((byte) 10); + overflowBuffer.flip(); + // Read the buffer with IntegerReader + IntegerReader reader = new IntegerReader(); + reader.configure(N); + reader.read(overflowBuffer); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java b/test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java new file mode 100644 index 00000000000..662d0c264d8 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java @@ -0,0 +1,182 @@ +/* + * 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. + * + * 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 + * @modules java.net.http/jdk.internal.net.http.qpack + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL StaticTableFieldsTest + */ + +import jdk.internal.net.http.qpack.StaticTable; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.testng.Assert.assertEquals; + +public class StaticTableFieldsTest { + + @BeforeTest + public void setUp() { + // Populate expected table as defined by RFC + expectedTable = new ArrayList<>(); + String[] arr = staticTableFields.split("\n"); + for (String s : arr) { + s = s.replaceAll("( )+", " "); + // index + int endOfIndex = s.indexOf(" "); + var i = Integer.parseInt(s.substring(0, endOfIndex)); + // name + int endOfName = s.indexOf(" ", endOfIndex + 1); + var n = s.substring(endOfIndex + 1, endOfName).trim(); + // value + var v = s.substring(endOfName + 1).strip(); + expectedTable.add(new TableLine(i, n, v)); + } + // Populate actual static table currently being used by QPACK + actualTable = new ArrayList<>(); + for (int i = 0; i < StaticTable.HTTP3_HEADER_FIELDS.size(); i++) { + var n = StaticTable.HTTP3_HEADER_FIELDS.get(i).name(); + var v = StaticTable.HTTP3_HEADER_FIELDS.get(i).value(); + actualTable.add(new TableLine(i, n, v)); + } + } + + @Test + public void testStaticTable() { + assertEquals(actualTable.size(), expectedTable.size()); + for (int i = 0; i < expectedTable.size(); i++) { + assertEquals(actualTable.get(i).name(), expectedTable.get(i).name()); + assertEquals(actualTable.get(i).value(), expectedTable.get(i).value()); + } + } + + // Copy-Paste of static table from RFC 9204 for QPACK Appendix A + // https://www.rfc-editor.org/rfc/rfc9204.html#name-static-table-2 + String staticTableFields = """ + 0 :authority \s + 1 :path / + 2 age 0 + 3 content-disposition \s + 4 content-length 0 + 5 cookie \s + 6 date \s + 7 etag \s + 8 if-modified-since \s + 9 if-none-match \s + 10 last-modified \s + 11 link \s + 12 location \s + 13 referer \s + 14 set-cookie \s + 15 :method CONNECT + 16 :method DELETE + 17 :method GET + 18 :method HEAD + 19 :method OPTIONS + 20 :method POST + 21 :method PUT + 22 :scheme http + 23 :scheme https + 24 :status 103 + 25 :status 200 + 26 :status 304 + 27 :status 404 + 28 :status 503 + 29 accept */* + 30 accept application/dns-message + 31 accept-encoding gzip, deflate, br + 32 accept-ranges bytes + 33 access-control-allow-headers cache-control + 34 access-control-allow-headers content-type + 35 access-control-allow-origin * + 36 cache-control max-age=0 + 37 cache-control max-age=2592000 + 38 cache-control max-age=604800 + 39 cache-control no-cache + 40 cache-control no-store + 41 cache-control public, max-age=31536000 + 42 content-encoding br + 43 content-encoding gzip + 44 content-type application/dns-message + 45 content-type application/javascript + 46 content-type application/json + 47 content-type application/x-www-form-urlencoded + 48 content-type image/gif + 49 content-type image/jpeg + 50 content-type image/png + 51 content-type text/css + 52 content-type text/html; charset=utf-8 + 53 content-type text/plain + 54 content-type text/plain;charset=utf-8 + 55 range bytes=0- + 56 strict-transport-security max-age=31536000 + 57 strict-transport-security max-age=31536000; includesubdomains + 58 strict-transport-security max-age=31536000; includesubdomains; preload + 59 vary accept-encoding + 60 vary origin + 61 x-content-type-options nosniff + 62 x-xss-protection 1; mode=block + 63 :status 100 + 64 :status 204 + 65 :status 206 + 66 :status 302 + 67 :status 400 + 68 :status 403 + 69 :status 421 + 70 :status 425 + 71 :status 500 + 72 accept-language \s + 73 access-control-allow-credentials FALSE + 74 access-control-allow-credentials TRUE + 75 access-control-allow-headers * + 76 access-control-allow-methods get + 77 access-control-allow-methods get, post, options + 78 access-control-allow-methods options + 79 access-control-expose-headers content-length + 80 access-control-request-headers content-type + 81 access-control-request-method get + 82 access-control-request-method post + 83 alt-svc clear + 84 authorization \s + 85 content-security-policy script-src 'none'; object-src 'none'; base-uri 'none' + 86 early-data 1 + 87 expect-ct \s + 88 forwarded \s + 89 if-range \s + 90 origin \s + 91 purpose prefetch + 92 server \s + 93 timing-allow-origin * + 94 upgrade-insecure-requests 1 + 95 user-agent \s + 96 x-forwarded-for \s + 97 x-frame-options deny + 98 x-frame-options sameorigin + """; + + private List actualTable, expectedTable; + private record TableLine(int index, String name, String value) { } +} diff --git a/test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java b/test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java new file mode 100644 index 00000000000..ade50d2843b --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java @@ -0,0 +1,482 @@ +/* + * 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. + * + * 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. + */ + + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.readers.StringReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.writers.StringWriter; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.http.qpack.allowBlockingEncoding=true + * StringLengthLimitsTest + */ +public class StringLengthLimitsTest { + + @DataProvider + Object[][] stringReaderLimitsData() { + return new Object[][]{ + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH, false, false}, + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH - 1, false, true}, + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH / 4, true, false}, + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH / 4 - 1, true, true} + }; + } + + @Test(dataProvider = "stringReaderLimitsData") + public void stringReaderLimits(int length, int limit, boolean huffmanBit, + boolean exceptionExpected) throws IOException { + IntegerWriter intWriter = new IntegerWriter(); + intWriter.configure(length, 7, huffmanBit ? STRING_READER_HUFFMAN_PAYLOAD : STRING_READER_PAYLOAD); + + var byteBuffer = ByteBuffer.allocate(2); + if (!intWriter.write(byteBuffer)) { + Assert.fail("Error with test buffer preparations"); + } + byteBuffer.flip(); + + StringReader stringReader = new StringReader(); + StringBuilder unusedOutput = new StringBuilder(); + if (exceptionExpected) { + QPackException exception = Assert.expectThrows(QPackException.class, + () -> stringReader.read(byteBuffer, unusedOutput, limit)); + Throwable cause = exception.getCause(); + Assert.assertNotNull(cause); + Assert.assertTrue(cause instanceof ProtocolException); + System.err.println("Got expected ProtocolException: " + cause); + } else { + boolean done = stringReader.read(byteBuffer, unusedOutput, limit); + Assert.assertFalse(done, "read done"); + } + } + + @DataProvider + Object[][] encoderInstructionLimitsData() { + int maxEntrySize = ENCODER_INSTRUCTIONS_DT_CAPACITY - 32; + return new Object[][]{ + // "Insert with Literal Name" instruction tests + // No Huffman, incomplete instruction, enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize, + false, DO_NOT_GENERATE_PART, false, true}, + // No Huffman, incomplete instruction, not enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize + 1, + false, DO_NOT_GENERATE_PART, false, false}, + // No Huffman, full instruction, enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 2, + false, maxEntrySize / 2, false, true}, + // No Huffman, full instruction, not enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 2, + false, 1 + maxEntrySize / 2, false, false}, + // Huffman (name + value), full instruction, enough space + // in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 4 / 2, + true, maxEntrySize / 4 / 2, true, true}, + // Huffman (value only), full instruction, not enough space + // in the DT. + // +16 term is added to make sure huffman estimate exceeds the limit + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 2, + false, 2 * maxEntrySize + 16, true, false}, + + // "Insert with Name Reference" instruction tests + // Enough space in the DT for the value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize, false, true}, + // Not enough space in the DT for the value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize + 1, false, false}, + // Enough space in the DT for the Huffman encoded value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize * 4, true, true}, + // Not enough space in the DT for the Huffman encoded value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize * 4 + 4, true, false} + }; + } + + @Test(dataProvider = "encoderInstructionLimitsData") + public void encoderInstructionLimits(EncoderInstruction instruction, + int nameLength, boolean nameHuffman, + int valueLength, boolean valueHuffman, + boolean successExpected) { + // Encoder/decoder pair with instruction that has string and with length > limit + var connector = new EncoderDecoderConnector(); + AtomicReference observedError = new AtomicReference<>(); + QPACK.QPACKErrorHandler errorHandler = (throwable, error) -> { + System.err.println("QPACK error observed: " + error); + observedError.set(throwable); + }; + var streamError = new AtomicReference(); + var pair = connector.newEncoderDecoderPair((_) -> true, errorHandler, errorHandler, streamError::set); + + // Configure dynamic tables + var decoderDT = pair.decoderTable(); + var encoderDT = pair.encoderTable(); + encoderDT.setMaxTableCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + encoderDT.setCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + decoderDT.setMaxTableCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + decoderDT.setCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + + // Generate buffers with encoder instruction bytes + var instructionBuffers = generateInstructionBuffers(instruction, + nameLength, nameHuffman, valueLength, valueHuffman); + for (var buffer : instructionBuffers) { + // Submit encoder instruction with test instructions which + // could be incomplete + pair.encoderStreams().submitData(buffer); + } + Throwable error = observedError.get(); + if (successExpected && error != null) { + Assert.fail("Unexpected error", error); + } else if (error == null && !successExpected) { + Assert.fail("Expected error"); + } + } + + /* + * Generate a list of instruction buffers. + * First buffer contains a name part (index or String), + * Second buffer contains a value part (index or String). + * If instruction type is INSERT_NAME_REFERENCE - + * the nameLength and nameHuffman are ignored. + */ + private static List generateInstructionBuffers( + EncoderInstruction instruction, + int nameLength, boolean nameHuffman, + int valueLength, boolean valueHuffman) { + IntegerWriter intWriter = new IntegerWriter(); + StringWriter stringWriter = new StringWriter(); + List instructionBuffers = new ArrayList<>(); + int valuePartPayload = valueHuffman ? + STRING_READER_HUFFMAN_PAYLOAD : STRING_READER_PAYLOAD; + // Configure writers for an instruction + switch (instruction) { + case INSERT_LITERAL_NAME: + int namePartPayload = nameHuffman ? + INSERT_INSTRUCTION_LITERAL_NAME_HUFFMAN_PAYLOAD : + INSERT_INSTRUCTION_LITERAL_NAME_PAYLOAD; + if (valueLength != DO_NOT_GENERATE_PART) { + // Generate data for the name part + var namePartBB = ByteBuffer.allocate(nameLength + 1); + stringWriter.configure("T".repeat(nameLength), 5, namePartPayload, valueHuffman); + boolean nameDone = stringWriter.write(namePartBB); + assert nameDone; + namePartBB.flip(); + instructionBuffers.add(namePartBB); + // Generate data for the value part + var valuePartBB = generatePartialString(7, valuePartPayload, valueLength); + instructionBuffers.add(valuePartBB); + } else { + // Generate data for the name part only + var namePartBB = generatePartialString(5, namePartPayload, nameLength); + instructionBuffers.add(namePartBB); + } + break; + case INSERT_NAME_REFERENCE: + var nameIndexPart = ByteBuffer.allocate(1); + // Write some static table name id + // Referencing static table entry with id = 16, ie ":method" + // nameLength and nameHuffman are ignored + intWriter.configure(16, 6, INSERT_INSTRUCTION_WITH_NAME_REFERENCE_PAYLOAD); + boolean nameIndexDone = intWriter.write(nameIndexPart); + assert nameIndexDone; + nameIndexPart.flip(); + intWriter.reset(); + // Write value part with specified length and huffman encoding + // Generate data for the value part + var valueLengthPart = + generatePartialString(7, valuePartPayload, valueLength); + // Add both parts to the list of forged instruction buffers + instructionBuffers.add(nameIndexPart); + instructionBuffers.add(valueLengthPart); + break; + } + return instructionBuffers; + } + + @DataProvider + Object[][] fieldLineLimitsData() { + return new Object[][]{ + // Post-Base Index + {-1, -1, ENTRY_NAME, ENTRY_VALUE, true, true}, + // Relative Index + {-1, -1, ENTRY_NAME, ENTRY_VALUE, false, true}, + // Post-Base Name Index + {-1, -1, ENTRY_NAME, "X".repeat(ENTRY_VALUE.length()), true, true}, + // Relative Name Index + {-1, -1, ENTRY_NAME, "X".repeat(ENTRY_VALUE.length()), false, true}, + // Post-Base Index, limit is exceeded + {-1, -1, BIG_ENTRY_NAME, BIG_ENTRY_VALUE, true, false}, + // Relative Index, limit is exceeded + {-1, -1, BIG_ENTRY_NAME, BIG_ENTRY_VALUE, false, false}, + // Post-Base Name Index, limit is exceeded + {-1, -1, BIG_ENTRY_NAME, ENTRY_VALUE, true, false}, + // Relative Name Index, limit is exceeded + {-1, -1, ENTRY_NAME, BIG_ENTRY_VALUE, false, false}, + // Name and Value are literals, limit is not exceeded + {ENTRY_NAME.length(), ENTRY_VALUE.length(), null, null, false, true}, + // Name and Value are literals, limit is exceeded in name part + {ENTRY_NAME.length() + ENTRY_VALUE.length() + 1, 0, null, null, false, false}, + // Name and Value are literals, limit is exceeded in value part + {1, ENTRY_NAME.length() + ENTRY_VALUE.length(), null, null, false, false} + + }; + } + + @Test(dataProvider = "fieldLineLimitsData") + public void fieldLineLimits(int nameLength, int valueLength, + String name, String value, + boolean isPostBase, boolean successExpected) throws IOException { + // QPACK writers for test data generations + IntegerWriter intWriter = new IntegerWriter(); + StringWriter stringWriter = new StringWriter(); + + // QPACK error handlers, stream error capture and decoding callback + var encoderError = new AtomicReference(); + var decoderError = new AtomicReference(); + var streamError = new AtomicReference(); + var decodingCallbackError = new AtomicReference(); + + QPACK.QPACKErrorHandler encoderErrorHandler = (throwable, error) -> { + System.err.println("Encoder error observed: " + error); + encoderError.set(throwable); + }; + + QPACK.QPACKErrorHandler decoderErrorHandler = (throwable, error) -> { + System.err.println("Decoder error observed: " + error); + decoderError.set(throwable); + }; + + var decodingCallback = new FieldLineDecodingCallback(decodingCallbackError); + + // Create encoder/decoder pair + var conn = new EncoderDecoderConnector(); + var pair = conn.newEncoderDecoderPair( + // Disallow entries insertion. + // The dynamic table is pre-populated with needed entries + // before the test execution + _ -> false, + encoderErrorHandler, + decoderErrorHandler, + streamError::set); + var encoder = pair.encoder(); + var decoder = pair.decoder(); + + // Set MAX_HEADER_SIZE limit on a decoder side + // Create settings frame with MAX_FIELD_SECTION_SIZE + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, MAX_FIELD_SECTION_SIZE_SETTING_VALUE); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, FIELD_LINES_DT_CAPACITY); + pair.decoder().configure(ConnectionSettings.createFrom(settingsFrame)); + + // Configure tables + configureTablesForFieldLinesTest(pair); + + // Encode the section prefix + long RIC = pair.encoderTable().insertCount(); + long base = isPostBase ? 0 : RIC; + HeaderFrameWriter writer = encoder.newHeaderFrameWriter(); + HeaderFrameReader reader = decoder.newHeaderFrameReader(decodingCallback); + var encodingContext = encoder.newEncodingContext(123, base, writer); + List buffers = new ArrayList<>(); + + if (nameLength == -1 && valueLength == -1) { + // Test configuration for all indexed field line wire formats + // (indexed name and indexed entry) + var headersBuffer = ByteBuffer.allocate(1024); + // knownReceivedCount == InsertCount to allow DT reference encodings, + // since there is one entry in the test dynamic table + encoder.header(encodingContext, name, value, false, RIC); + writer.write(headersBuffer); + headersBuffer.flip(); + buffers.add(headersBuffer); + } else { + if (nameLength > MAX_FIELD_SECTION_SIZE_SETTING_VALUE - DynamicTable.ENTRY_SIZE) { + // if nameLength > limit - only need to generate name part + // We only write partial name part of the "Literal Field Line with Literal + // Name" instruction with String length value + var nameLengthBB = + generatePartialString(3, FIELD_LINE_NAME_VALUE_LITERALS_PAYLOAD, nameLength); + buffers.add(nameLengthBB); + } else if (nameLength + valueLength > + MAX_FIELD_SECTION_SIZE_SETTING_VALUE - DynamicTable.ENTRY_SIZE) { + // if nameLength + valueLength > limit - + // the whole instruction needs to be generated with basic writers + var fieldLineBB = ByteBuffer.allocate(1024); + stringWriter.configure("Z".repeat(nameLength), 3, + FIELD_LINE_NAME_VALUE_LITERALS_PAYLOAD, false); + intWriter.configure(valueLength, 7, 0); + stringWriter.write(fieldLineBB); + intWriter.write(fieldLineBB); + fieldLineBB.flip(); + buffers.add(fieldLineBB); + } else { + // name + value doesn't exceed MAX_FIELD_SECTION_SIZE + var headersBuffer = ByteBuffer.allocate(1024); + // We use 'X' and 'Z' letters to prevent encoder from + // huffman encoding. + encoder.header(encodingContext, + "X".repeat(nameLength), "Z".repeat(valueLength), + false, RIC); + writer.write(headersBuffer); + headersBuffer.flip(); + buffers.add(headersBuffer); + } + } + // Generate field lines section prefix + encoder.generateFieldLineSectionPrefix(encodingContext, buffers); + assert buffers.size() == 2; + + // Decode generated header buffers + decoder.decodeHeader(buffers.get(0), false, reader); + decoder.decodeHeader(buffers.get(1), true, reader); + + // Check if any error is observed and it meets the test expectations + var error = decodingCallbackError.get(); + System.err.println("Decoding callback error: " + error); + if (successExpected && error != null) { + Assert.fail("Unexpected error", error); + } else if (error == null && !successExpected) { + Assert.fail("Error expected"); + } + } + + private static void configureTablesForFieldLinesTest( + EncoderDecoderConnector.EncoderDecoderPair pair) { + // Encoder + pair.encoderTable().setMaxTableCapacity(StringLengthLimitsTest.FIELD_LINES_DT_CAPACITY); + pair.encoderTable().setCapacity(StringLengthLimitsTest.FIELD_LINES_DT_CAPACITY); + + // Decoder max table capacity is set via the settings frame + pair.decoderTable().setCapacity(StringLengthLimitsTest.FIELD_LINES_DT_CAPACITY); + + // Insert test entry to both tables + pair.decoderTable().insert(ENTRY_NAME, ENTRY_VALUE); + pair.encoderTable().insert(ENTRY_NAME, ENTRY_VALUE); + pair.decoderTable().insert(BIG_ENTRY_NAME, BIG_ENTRY_VALUE); + pair.encoderTable().insert(BIG_ENTRY_NAME, BIG_ENTRY_VALUE); + } + + // Encoder instructions under test + public enum EncoderInstruction { + INSERT_LITERAL_NAME, + INSERT_NAME_REFERENCE, + } + + // Decoding callback used by Field Line Representation tests + private static class FieldLineDecodingCallback implements DecodingCallback { + private final AtomicReference decodingError; + + public FieldLineDecodingCallback(AtomicReference decodingCallbackError) { + this.decodingError = decodingCallbackError; + } + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + decodingError.set(throwable); + } + + @Override + public long streamId() { + return 0; + } + } + + // Utility method to generate partial QPack string with length part only + private static ByteBuffer generatePartialString(int N, int payload, int length) { + IntegerWriter intWriter = new IntegerWriter(); + var partialStringBB = ByteBuffer.allocate( + IntegerWriter.requiredBufferSize(N, length)); + intWriter.configure(length, N, payload); + boolean done = intWriter.write(partialStringBB); + assert done; + partialStringBB.flip(); + return partialStringBB; + } + + // Constants for StringReader tests + private static final int STRING_READER_HUFFMAN_PAYLOAD = 0b1000_0000; + private static final int STRING_READER_PAYLOAD = 0b0000_0000; + private static final int STRING_READER_STRING_LENGTH = 32; + + // Constants for Encoder Instructions Reader tests + private static final int ENCODER_INSTRUCTIONS_DT_CAPACITY = 64; + // This constant is used to instruct test data generator not to generate + // value or name (if name is referenced by the DT index) part of the + // decoder instruction + private static final int DO_NOT_GENERATE_PART = -1; + // Encoder instruction payloads used to forge instruction buffers + private static final int INSERT_INSTRUCTION_LITERAL_NAME_HUFFMAN_PAYLOAD = 0b0110_0000; + private static final int INSERT_INSTRUCTION_LITERAL_NAME_PAYLOAD = 0b0100_0000; + private static final int INSERT_INSTRUCTION_WITH_NAME_REFERENCE_PAYLOAD = 0b1100_0000; + private static final int FIELD_LINE_NAME_VALUE_LITERALS_PAYLOAD = 0b0010_0000; + + // Constants for Field Line Representation tests + // Table capacity big enough for insertion of all entries + private static final long FIELD_LINES_DT_CAPACITY = 1024; + private static final String ENTRY_NAME = "FullEntryName"; + private static final String ENTRY_VALUE = "FullEntryValue"; + private static final String BIG_ENTRY_NAME = "FullEntryName_Big"; + private static final String BIG_ENTRY_VALUE = "FullEntryValue_Big"; + private static final long MAX_FIELD_SECTION_SIZE_SETTING_VALUE = + ENTRY_NAME.length() + ENTRY_VALUE.length() + DynamicTable.ENTRY_SIZE; + +} diff --git a/test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java b/test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java new file mode 100644 index 00000000000..4ed489ca6dd --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java @@ -0,0 +1,196 @@ +/* + * 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. + * + * 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 + * @modules java.net.http/jdk.internal.net.http.qpack + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=INFO TablesIndexerTest + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.qpack.TablesIndexer; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType; + +public class TablesIndexerTest { + + @DataProvider(name = "indicesLookupData") + public Object[][] indicesData() { + List tcs = new ArrayList<>(); + + long index = 0; + for (HeaderField f : StaticTable.HTTP3_HEADER_FIELDS) { + // Full name:value match + tcs.add(new Object[]{ + f.name(), f.value(), f.value(), + Set.of(index), EntryType.NAME_VALUE + }); + // Static and Dynamic tables contain only name match - we expect static + // NAME only entry to be returned + tcs.add(new Object[]{ + f.name(), "NotInStatic", "InDynamic", + acceptableIndicesForName(f.name()), EntryType.NAME}); + index++; + } + return tcs.toArray(Object[][]::new); + } + + @Test(dataProvider = "indicesLookupData") + public void checkIndicesLookup(String name, String value, + String dynamicTableValue, + Set indices, EntryType type) { + // We construct dynamic table with the same name value to check that + // static entry is returned first + DynamicTable dt = new DynamicTable(QPACK.getLogger().subLogger( + "checkStaticIndicesLookup")); + dt.setMaxTableCapacity(256); + dt.setCapacity(256); + dt.insert(name, dynamicTableValue); + + // Construct TablesIndexer + TablesIndexer tablesIndexer = new TablesIndexer(STATIC_TABLE, dt); + + // Use TablesIndexer to locate TableEntry + TableEntry tableEntry = tablesIndexer.entryOf(name, value, + IGNORE_RECEIVED_COUNT_CHECK); + + // TableEntry should be for static table only + Assert.assertTrue(tableEntry.isStaticTable()); + + // If value is not equal to dynamicTableValue, the full name:dynamicTableValue + // should be found in the dynamic table with index 0 + if (!value.equals(dynamicTableValue)) { + TableEntry dtEntry = tablesIndexer.entryOf(name, dynamicTableValue, + IGNORE_RECEIVED_COUNT_CHECK); + Assert.assertFalse(dtEntry.isStaticTable()); + Assert.assertEquals(dtEntry.type(), EntryType.NAME_VALUE); + Assert.assertEquals(dtEntry.index(), 0L); + } + + // Check that found index is contained in a set and returned indices match + Assert.assertTrue(indices.contains(tableEntry.index())); + + // Check that entry type matches + Assert.assertEquals(tableEntry.type(), type); + + var headerField = STATIC_TABLE.get(tableEntry.index()); + // Check that name and/or value matches the one that can be acquired by + // using looked-up index + if (tableEntry.type() == EntryType.NAME) { + Assert.assertEquals(headerField.name(), name); + // If only name entry is found huffmanName should be set to false + Assert.assertFalse(tableEntry.huffmanName()); + } else if (tableEntry.type() == EntryType.NAME_VALUE) { + Assert.assertEquals(headerField.name(), name); + Assert.assertEquals(headerField.value(), value); + // If "name:value" match is found huffmanName and huffmanValue should + // be set to false + Assert.assertFalse(tableEntry.huffmanName()); + Assert.assertFalse(tableEntry.huffmanValue()); + + } else { + Assert.fail("Unexpected TableEntry type returned:" + tableEntry); + } + } + + @Test(dataProvider = "unacknowledgedEntriesLookupData") + public void unacknowledgedEntryLookup(String headerName, String headerValue, + boolean staticEntryExpected, + EntryType expectedType) { + // Construct dynamic table with pre-populated entries + DynamicTable dynamicTable = dynamicTableForUnackedEntriesTest(); + // Construct TablesIndexer + TablesIndexer tablesIndexer = new TablesIndexer(STATIC_TABLE, dynamicTable); + // Search for an entry in the dynamic and the static tables + var entry = tablesIndexer.entryOf(headerName, headerValue, TEST_KNOWN_RECEIVED_COUNT); + // Check that entry references expected table + Assert.assertEquals(entry.isStaticTable(), staticEntryExpected); + // And the type of found entry matches expectations + Assert.assertEquals(entry.type(), expectedType); + } + + @DataProvider + public Object[][] unacknowledgedEntriesLookupData() { + List data = new ArrayList<>(); + data.add(new Object[]{USER_AGENT_ST_NAME, "not-in-dynamic", true, EntryType.NAME}); + data.add(new Object[]{USER_AGENT_ST_NAME, USER_AGENT_DT_VALUE, false, EntryType.NAME_VALUE}); + data.add(new Object[]{TEST_ACKED_ENTRY, "not-in-dynamic", false, EntryType.NAME}); + data.add(new Object[]{TEST_ACKED_ENTRY, TEST_ACKED_ENTRY, false, EntryType.NAME_VALUE}); + data.add(new Object[]{CONTENT_TYPE_ST_NAME, "what/ever", true, EntryType.NAME}); + data.add(new Object[]{TEST_UNACKED_ENTRY, TEST_UNACKED_ENTRY, false, EntryType.NEITHER}); + return data.toArray(Object[][]::new); + } + + private static DynamicTable dynamicTableForUnackedEntriesTest() { + DynamicTable dt = new DynamicTable(QPACK.getLogger() + .subLogger("unacknowledgedEntryLookup")); + dt.setMaxTableCapacity(1024); + dt.setCapacity(1024); + // Acknowledged entry with name available in static and dynamic table + dt.insert(USER_AGENT_ST_NAME, USER_AGENT_DT_VALUE); // 0 + // Acknowledged entry with name available in dynamic table only + dt.insert(TEST_ACKED_ENTRY, TEST_ACKED_ENTRY); // 1 + // Unacknowledged entry with name available in static table + dt.insert(CONTENT_TYPE_ST_NAME, "what/ever"); // 2 + // Unacknowledged entry with name available in dynamic table only + dt.insert(TEST_UNACKED_ENTRY, TEST_UNACKED_ENTRY); // 3 + return dt; + } + + private static final long IGNORE_RECEIVED_COUNT_CHECK = -1L; + private static final long TEST_KNOWN_RECEIVED_COUNT = 2L; + + private static final String USER_AGENT_ST_NAME = "user-agent"; + private static final String USER_AGENT_DT_VALUE = "qpack-test-client"; + private static final String CONTENT_TYPE_ST_NAME = "content-type"; + private static final String TEST_ACKED_ENTRY = "test-acked-entry"; + private static final String TEST_UNACKED_ENTRY = "test-unacked-entry"; + + private final static StaticTable STATIC_TABLE = StaticTable.HTTP3; + + private Set acceptableIndicesForName(String name) { + AtomicLong enumerator = new AtomicLong(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(f -> new NameEnumeration(enumerator.getAndIncrement(), f.name())) + .filter(ne -> name.equals(ne.name())) + .mapToLong(NameEnumeration::id) + .boxed() + .collect(Collectors.toUnmodifiableSet()); + } + + record NameEnumeration(long id, String name) { + } +} diff --git a/test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java b/test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java new file mode 100644 index 00000000000..7b621e2d69d --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java @@ -0,0 +1,243 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertNotEquals; + +/* + * @test + * @summary check that unacknowledged header is not inserted + * twice to the dynamic table + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA UnacknowledgedInsertionTest + */ +public class UnacknowledgedInsertionTest { + + @Test(dataProvider = "duplicateEntryInsertions") + public void unacknowledgedDoubleInsertion(long knownReceiveCount, List expectedHeadersEncodingType) throws Exception { + // When knownReceiveCount is set to -1 the Encoder.knownReceiveCount() + // value is used to encode headers - otherwise the provided value is used + var encoderEh = new UnacknowledgedInsertionTest.TestErrorHandler(); + var decoderEh = new UnacknowledgedInsertionTest.TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair ed = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, streamError); + + + var encoder = ed.encoder(); + var decoder = ed.decoder(); + // Create a decoding callback to check for completion and to log failures + UnacknowledgedInsertionTest.TestDecodingCallback decodingCallback = + new UnacknowledgedInsertionTest.TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = encoder.newEncodingContext(0, 0, + headerFrameWriter); + + String name = "name"; + String value = "value"; + + for (int i = 0; i < 3; i++) { + long krcToUse = knownReceiveCount == -1L ? encoder.knownReceivedCount() : knownReceiveCount; + if (i == 1) { + encoder.header(context, name, "nameMatchOnly", false, krcToUse); + } else { + encoder.header(context, name, value, false, krcToUse); + } + headerFrameWriter.write(headersBb); + } + + // Only two entries are expected to be inserted to the dynamic table + Assert.assertEquals(ed.decoderTable().insertCount(), 2); + + // Check that headers byte buffer is not empty + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Use decoder to process generated byte buffers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + + var actualHeaderEncodingTypes = decodingCallback.decodedHeaders + .stream() + .map(DecodedHeader::encodingType) + .toList(); + Assert.assertEquals(actualHeaderEncodingTypes, expectedHeadersEncodingType); + } + + + @DataProvider(name = "duplicateEntryInsertions") + private Object[][] duplicateEntryInsertionsData() { + return new Object[][]{ + {0, List.of(EncodingType.LITERAL, EncodingType.LITERAL, EncodingType.LITERAL)}, + {-1, List.of(EncodingType.LITERAL, EncodingType.NAME_REF, EncodingType.INDEXED)}, + }; + } + + private static EncoderDecoderConnector.EncoderDecoderPair newPreconfiguredEncoderDecoder( + UnacknowledgedInsertionTest.TestErrorHandler encoderEh, + UnacknowledgedInsertionTest.TestErrorHandler decoderEh, + AtomicReference streamError) { + EncoderDecoderConnector conn = new EncoderDecoderConnector(); + var pair = conn.newEncoderDecoderPair( + e -> true, + encoderEh::qpackErrorHandler, + decoderEh::qpackErrorHandler, + streamError::set); + // Create settings frame with dynamic table capacity and number of blocked streams + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + // 4k should be enough for storing dynamic table entries added by 'prepopulateDynamicTable' + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DT_CAPACITY); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder with constructed ConnectionSettings + pair.encoder().configure(settings); + pair.decoder().configure(settings); + pair.encoderTable().setCapacity(DT_CAPACITY); + pair.decoderTable().setCapacity(DT_CAPACITY); + + return pair; + } + + private static class TestDecodingCallback implements DecodingCallback { + + final List decodedHeaders = new CopyOnWriteArrayList<>(); + final CompletableFuture completed = new CompletableFuture<>(); + final AtomicLong completedTimestamp = new AtomicLong(); + + final AtomicReference lastThrowable = new AtomicReference<>(); + final AtomicReference lastHttp3Error = new AtomicReference<>(); + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + Assert.fail("onDecoded not expected to be called"); + } + + @Override + public void onLiteralWithLiteralName(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman, + boolean hideIntermediary) { + var header = new DecodedHeader(name.toString(), value.toString(), EncodingType.LITERAL); + decodedHeaders.add(header); + System.err.println("Decoding callback 'onLiteralWithLiteralName': " + header); + } + + @Override + public void onLiteralWithNameReference(long index, + CharSequence name, + CharSequence value, + boolean valueHuffman, + boolean hideIntermediary) { + var header = new DecodedHeader(name.toString(), value.toString(), EncodingType.NAME_REF); + decodedHeaders.add(header); + System.err.println("Decoding callback 'onLiteralWithNameReference': " + header); + + } + + @Override + public void onIndexed(long index, CharSequence name, CharSequence value) { + var header = new DecodedHeader(name.toString(), value.toString(), EncodingType.INDEXED); + decodedHeaders.add(header); + System.err.println("Decoding callback 'onIndexed': " + header); + } + + + @Override + public void onComplete() { + System.err.println("Decoding callback 'onComplete'"); + completedTimestamp.set(System.nanoTime()); + completed.complete(null); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + System.err.println("Decoding callback 'onError': " + http3Error); + lastThrowable.set(throwable); + lastHttp3Error.set(http3Error); + } + + @Override + public long streamId() { + return 0; + } + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + throw new RuntimeException("http3 error: " + http3Error, error); + } + } + + private record DecodedHeader(String name, String value, EncodingType encodingType) { + } + + enum EncodingType { + LITERAL, + NAME_REF, + INDEXED + } + + private static final long DT_CAPACITY = 4096L; +} diff --git a/test/jdk/java/net/httpclient/quic/AckElicitingTest.java b/test/jdk/java/net/httpclient/quic/AckElicitingTest.java new file mode 100644 index 00000000000..47fdb935598 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/AckElicitingTest.java @@ -0,0 +1,729 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckRange; +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.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +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.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.crypto.AEADBadTagException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import static jdk.internal.net.http.quic.frames.QuicFrame.*; +import static jdk.internal.net.http.quic.frames.ConnectionCloseFrame.CONNECTION_CLOSE_VARIANT; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * @test + * @summary tests the logic to decide whether a packet or + * a frame is ACK-eliciting. + * @library /test/lib + * @run testng AckElicitingTest + * @run testng/othervm -Dseed=-7997973196290088038 AckElicitingTest + */ +public class AckElicitingTest { + + static final Random RANDOM = RandomFactory.getRandom(); + + private static class DummyQuicTLSEngine implements QuicTLSEngine { + @Override + public HandshakeState getHandshakeState() { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean isTLSHandshakeComplete() { + return true; + } + + @Override + public KeySpace getCurrentSendKeySpace() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean keysAvailable(KeySpace keySpace) { + return true; + } + + @Override + public void discardKeys(KeySpace keySpace) { + // no-op + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + throw new AssertionError("should not come here!"); + } + + @Override + public void restartHandshake() throws IOException { + throw new AssertionError("should not come here!"); + } + + @Override + public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) { + throw new AssertionError("should not come here!"); + } + + @Override + public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { } + + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return 0; + } + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) { + return ByteBuffer.allocate(5); + } + + @Override + public int getAuthTagSize() { + return 0; + } + + @Override + public void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException { + // this dummy QUIC TLS engine doesn't do any encryption. + // we just copy over the raw packet payload into the output buffer + output.put(packetPayload); + } + + @Override + public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) { + packet.position(packet.position() + headerLength); + output.put(packet); + } + @Override + public void signRetryPacket(QuicVersion quicVersion, + ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) { + throw new AssertionError("should not come here!"); + } + @Override + public void verifyRetryPacket(QuicVersion quicVersion, + ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException { + throw new AssertionError("should not come here!"); + } + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) { + throw new AssertionError("should not come here!"); + } + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) { + throw new AssertionError("should not come here!"); + } + @Override + public Runnable getDelegatedTask() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryMarkHandshakeDone() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryReceiveHandshakeDone() { + throw new AssertionError("should not come here!"); + } + + @Override + public Set getSupportedQuicVersions() { + return Set.of(QuicVersion.QUIC_V1); + } + + @Override + public void setUseClientMode(boolean mode) { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean getUseClientMode() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLParameters getSSLParameters() { + throw new AssertionError("should not come here!"); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + throw new AssertionError("should not come here!"); + } + + @Override + public String getApplicationProtocol() { + return null; + } + + @Override + public SSLSession getSession() { throw new AssertionError("should not come here!"); } + + @Override + public SSLSession getHandshakeSession() { throw new AssertionError("should not come here!"); } + + @Override + public void versionNegotiated(QuicVersion quicVersion) { + // no-op + } + + @Override + public void setOneRttContext(QuicOneRttContext ctx) { + // no-op + } + } + private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine(); + private static abstract class TestCodingContext implements CodingContext { + TestCodingContext() { } + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new AssertionError("should not come here!"); + } + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new AssertionError("should not come here!"); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicTLSEngine getTLSEngine() { + return TLS_ENGINE; + } + } + + static final int CIDLEN = RANDOM.nextInt(5, QuicConnectionId.MAX_CONNECTION_ID_LENGTH + 1); + + private static final TestCodingContext CONTEXT = new TestCodingContext() { + + @Override + public long largestProcessedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public long largestAckedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public int connectionIdLength() { + return CIDLEN; + } + + @Override + public QuicConnectionId originalServerConnId() { + return null; + } + }; + + /** + * A record to store all the input of a given test case. + * @param type a concrete frame type or packet type + * @param describer a function to describe the {@code obj} instance + * for tracing/diagnosis purposes + * @param ackEliciting the function we want to test. This is either + * {@link QuicFrame#isAckEliciting() + * QuicFrame::isAckEliciting} or + * {@link QuicPacket#isAckEliciting() + * QuicPacket::isAckEliciting} + * @param obj the instance on which to call the {@code ackEliciting} + * function. + * @param expected the expected result of calling + * {@code obj.ackEliciting()} + * @param A concrete subclass of {@link QuicFrame} or {@link QuicPacket} + */ + static record TestCase(Class type, + Function describer, + Predicate ackEliciting, + T obj, + boolean expected) { + + @Override + public String toString() { + // shorter & better toString than the default + return "%s(%s)" + .formatted(type.getSimpleName(), describer.apply(obj)); + } + + private static String describeFrame(QuicFrame frame) { + long type = frame.getTypeField(); + return HexFormat.of().toHexDigits((byte)type); + } + + /** + * Creates an instance of {@code TestCase} for a concrete frame type + * @param type the concrete frame class + * @param frame the concrete instance + * @param expected whether {@link QuicFrame#isAckEliciting()} + * should return true for that instance. + * @param a concrete subclass of {@code QuicFrame} + * @return a new instance of {@code TestCase} + */ + public static TestCase + of(Class type, T frame, boolean expected) { + return new TestCase(type, TestCase::describeFrame, + QuicFrame::isAckEliciting, frame, expected); + } + + /** + * Creates an instance of {@code TestCase} for a concrete frame type + * @param frame the concrete frame instance + * @param expected whether {@link QuicFrame#isAckEliciting()} + * should return true for that instance. + * @param a concrete subclass of {@code QuicFrame} + * @return a new instance of {@code TestCase} + */ + public static TestCase of(T frame, boolean expected) { + return new TestCase((Class)frame.getClass(), + TestCase::describeFrame, + QuicFrame::isAckEliciting, + frame, expected); + } + + /** + * Creates an instance of {@code TestCase} for a concrete packet type + * @param packet the concrete packet instance + * @param expected whether {@link QuicPacket#isAckEliciting()} + * should return true for that instance. + * @param a concrete subclass of {@code QuicPacket} + * @return a new instance of {@code TestCase} + */ + public static TestCase of(T packet, boolean expected) { + return new TestCase((Class)packet.getClass(), + (p) -> p.frames().stream() + .map(Object::getClass) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")), + QuicPacket::isAckEliciting, + packet, expected); + } + } + + // convenient alias to shorten lines in data providers + private static TestCase of(T frame, boolean ackEliciting) { + return TestCase.of(frame, ackEliciting); + } + + // convenient alias to shorten lines in data providers + private static TestCase of(T packet, boolean ackEliciting) { + return TestCase.of(packet, ackEliciting); + } + + /** + * Create a new instance of the given frame type, populated with + * dummy values. + * @param frameClass the frame type + * @param a concrete subclass of {@code QuicFrame} + * @return a new instance of the given concrete class. + */ + T newFrame(Class frameClass) { + var frameType = QuicFrame.frameTypeOf(frameClass); + if (frameType == CONNECTION_CLOSE) { + if (RANDOM.nextBoolean()) { + frameType = CONNECTION_CLOSE_VARIANT; + } + } + long streamId = 4; + long largestAcknowledge = 3; + long ackDelay = 20; + long gap = 0; + long range = largestAcknowledge; + long offset = 0; + boolean fin = false; + int length = 10; + long errorCode = 1; + long finalSize = 10; + int size = 3; + long maxData = 10; + boolean maxStreamsBidi = true; + long maxStreams = 100; + long maxStreamData = 10; + long sequenceNumber = 4; + long retirePriorTo = 3; + String reason = "none"; + long errorFrameType = ACK; + int pathChallengeLen = PathChallengeFrame.LENGTH; + int pathResponseLen = PathResponseFrame.LENGTH; + var frame = switch (frameType) { + case ACK -> new AckFrame(largestAcknowledge, ackDelay, List.of(new AckRange(gap, range))); + case STREAM -> new StreamFrame(streamId, offset, length, fin, ByteBuffer.allocate(length)); + case RESET_STREAM -> new ResetStreamFrame(streamId, errorCode, finalSize); + case PADDING -> new PaddingFrame(size); + case PING -> new PingFrame(); + case STOP_SENDING -> new StopSendingFrame(streamId, errorCode); + case CRYPTO -> new CryptoFrame(offset, length, ByteBuffer.allocate(length)); + case NEW_TOKEN -> new NewTokenFrame(ByteBuffer.allocate(length)); + case DATA_BLOCKED -> new DataBlockedFrame(maxData); + case MAX_DATA -> new MaxDataFrame(maxData); + case MAX_STREAMS -> new MaxStreamsFrame(maxStreamsBidi, maxStreams); + case MAX_STREAM_DATA -> new MaxStreamDataFrame(streamId, maxStreamData); + case STREAM_DATA_BLOCKED -> new StreamDataBlockedFrame(streamId, maxStreamData); + case STREAMS_BLOCKED -> new StreamsBlockedFrame(maxStreamsBidi, maxStreams); + case NEW_CONNECTION_ID -> new NewConnectionIDFrame(sequenceNumber, retirePriorTo, + ByteBuffer.allocate(length), ByteBuffer.allocate(16)); + case RETIRE_CONNECTION_ID -> new RetireConnectionIDFrame(sequenceNumber); + case PATH_CHALLENGE -> new PathChallengeFrame(ByteBuffer.allocate(pathChallengeLen)); + case PATH_RESPONSE -> new PathResponseFrame(ByteBuffer.allocate(pathResponseLen)); + case CONNECTION_CLOSE -> new ConnectionCloseFrame(errorCode, errorFrameType, reason); + case CONNECTION_CLOSE_VARIANT -> new ConnectionCloseFrame(errorCode, reason); + case HANDSHAKE_DONE -> new HandshakeDoneFrame(); + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + return frameClass.cast(frame); + } + + /** + * Creates a list of {@code TestCase} to test all possible concrete + * subclasses of {@code QuicFrame}. + * + * @return a list of {@code TestCase} to test all possible concrete + * subclasses of {@code QuicFrame} + */ + public List> createFramesTests() { + List> frames = new ArrayList<>(); + frames.add(of(newFrame(AckFrame.class), false)); + frames.add(of(newFrame(ConnectionCloseFrame.class), false)); + frames.add(of(newFrame(PaddingFrame.class), false)); + + for (var frameType : QuicFrame.class.getPermittedSubclasses()) { + if (frameType == AckFrame.class) continue; + if (frameType == ConnectionCloseFrame.class) continue; + if (frameType == PaddingFrame.class) continue; + Class quicFrameClass = (Class)frameType; + frames.add(of(newFrame(quicFrameClass), true)); + } + + return List.copyOf(frames); + } + + /** + * Creates a {@code QuicPacket} containing the given list of frames. + * @param frames a list of frames + * @return a new instance of {@code QuicPacket} + */ + QuicPacket createPacket(List frames) { + PacketType[] values = PacketType.values(); + int index = PacketType.NONE.ordinal(); + while (index == PacketType.NONE.ordinal()) { + index = RANDOM.nextInt(0, values.length); + } + PacketType packetType = values[index]; + QuicPacketEncoder encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1); + byte[] scid = new byte[CIDLEN]; + RANDOM.nextBytes(scid); + byte[] dcid = new byte[CIDLEN]; + RANDOM.nextBytes(dcid); + QuicConnectionId source = new PeerConnectionId(scid); + QuicConnectionId dest = new PeerConnectionId(dcid); + long largestAckedPacket = CONTEXT.largestAckedPN(packetType); + QuicPacket packet = switch (packetType) { + case NONE -> throw new AssertionError("should not come here"); + // TODO: add more packet types + default -> encoder.newInitialPacket(source, dest, null, largestAckedPacket + 1 , + largestAckedPacket, frames, CONTEXT); + }; + return packet; + + } + + /** + * Creates a random instance of {@code QuicPacket} containing a + * pseudo random list of concrete {@link QuicFrame} instances. + * @param ackEliciting whether the returned packet should be + * ack eliciting. + * @return + */ + QuicPacket createPacket(boolean ackEliciting) { + List frames = new ArrayList<>(); + int mincount = ackEliciting ? 1 : 0; + int ackCount = RANDOM.nextInt(mincount, 5); + int nackCount = RANDOM.nextInt(0, 10); + + // TODO: maybe refactor this to make sure the frame + // we use are compatible with the packet type. + List> noAckFrames = List.of(AckFrame.class, + PaddingFrame.class, ConnectionCloseFrame.class); + for (int i=0; i < nackCount ; i++) { + frames.add(newFrame(noAckFrames.get(i % noAckFrames.size()))); + } + if (ackEliciting) { + // TODO: maybe refactor this to make sure the frame + // we use are compatible with the packet type. + Class[] frameClasses = QuicFrame.class.getPermittedSubclasses(); + for (int i=0; i < ackCount; i++) { + Class selected; + do { + int fx = RANDOM.nextInt(0, frameClasses.length); + selected = frameClasses[fx]; + } while (noAckFrames.contains(selected)); + frames.add(newFrame((Class) selected)); + } + } + if (!ackEliciting || RANDOM.nextBoolean()) { + // if !ackEliciting we always shuffle. + // Otherwise, we only shuffle half the time. + Collections.shuffle(frames, RANDOM); + } + return createPacket(mergeConsecutivePaddingFrames(frames)); + } + + private List mergeConsecutivePaddingFrames(List frames) { + var iterator = frames.listIterator(); + QuicFrame previous = null; + + while (iterator.hasNext()) { + var frame = iterator.next(); + if (previous instanceof PaddingFrame prevPad + && frame instanceof PaddingFrame nextPad) { + int previousIndex = iterator.previousIndex(); + QuicFrame merged = new PaddingFrame(prevPad.size() + nextPad.size()); + frames.set(previousIndex, merged); + iterator.remove(); + } else { + previous = frame; + } + } + return frames; + } + + /** + * Creates a list of {@code TestCase} to test random instances of + * {@code QuicPacket} containing random instances of {@link QuicFrame} + * @return a list of {@code TestCase} to test random instances of + * {@code QuicPacket} containing random instances of {@link QuicFrame} + */ + public List> createPacketsTests() { + List> packets = new ArrayList<>(); + packets.add(of(createPacket(List.of(newFrame(AckFrame.class))), false)); + packets.add(of(createPacket(List.of(newFrame(ConnectionCloseFrame.class))), false)); + packets.add(of(createPacket(List.of(newFrame(PaddingFrame.class))), false)); + var frames = new ArrayList<>(List.of( + newFrame(PaddingFrame.class), + newFrame(AckFrame.class), + newFrame(ConnectionCloseFrame.class))); + Collections.shuffle(frames, RANDOM); + packets.add(of(createPacket(List.copyOf(frames)), false)); + + int maxPackets = RANDOM.nextInt(5, 11); + for (int i = 0; i < maxPackets ; i++) { + packets.add(of(createPacket(true), true)); + } + return List.copyOf(packets); + } + + + /** + * A provider of test case to test + * {@link QuicFrame#isAckEliciting()}. + * @return test case to test + * {@link QuicFrame#isAckEliciting()} + */ + @DataProvider(name = "frames") + public Object[][] framesDataProvider() { + return createFramesTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + /** + * A provider of test case to test + * {@link QuicPacket#isAckEliciting()}. + * @return test case to test + * {@link QuicPacket#isAckEliciting()} + */ + @DataProvider(name = "packets") + public Object[][] packetsDataProvider() { + return createPacketsTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + /** + * Verifies the behavior of {@link QuicFrame#isAckEliciting()} + * with the given test case inputs. + * @param test the test inputs + * @param a concrete subclass of QuicFrame + */ + @Test(dataProvider = "frames") + public void testFrames(TestCase test) { + testAckEliciting(test.type(), + test.describer(), + test.ackEliciting(), + test.obj(), + test.expected()); + } + + /** + * Verifies the behavior of {@link QuicPacket#isAckEliciting()} + * with the given test case inputs. + * @param test the test inputs + * @param a concrete subclass of QuickPacket + */ + @Test(dataProvider = "packets") + public void testPackets(TestCase test) { + testAckEliciting(test.type(), + test.describer(), + test.ackEliciting(), + test.obj(), + test.expected()); + } + + + /** + * Asserts that {@code ackEliciting.test(obj) == expected}. + * @param type the concrete type of {@code obj} + * @param describer a function to describe {@code obj} + * @param ackEliciting the function being tested + * @param obj the instance on which to call the function being tested + * @param expected the expected result of {@code ackEliciting.test(obj)} + * @param the concrete class being tested + */ + private void testAckEliciting(Class type, + Function describer, + Predicate ackEliciting, + T obj, + boolean expected) { + System.out.printf("%ntestAckEliciting: %s(%s) - expecting %s%n", + type.getSimpleName(), + describer.apply(obj), + expected); + assertEquals(ackEliciting.test(obj), expected, describer.apply(obj)); + if (obj instanceof QuicFrame frame) { + checkFrame(frame); + } else if (obj instanceof QuicPacket packet) { + checkPacket(packet); + } + } + + // This is not a full-fledged test for frame encoding/decoding. + // Just a smoke test to verify that the ACK-eliciting property + // survives encoding/decoding + private void checkFrame(QuicFrame frame) { + int size = frame.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + System.out.println("Checking frame: " + frame.getClass()); + try { + frame.encode(buffer); + buffer.flip(); + var decoded = QuicFrame.decode(buffer); + checkFrame(decoded, frame); + } catch (QuicTransportException x) { + throw new AssertionError(frame.getClass().getName(), x); + } + } + + // This is not a full-fledged test for frame equality: + // And we still need a proper decoding/encoding test for frames + private void checkFrame(QuicFrame decoded, QuicFrame expected) { + System.out.printf("Comparing frames: %s with %s%n", + decoded.getClass().getSimpleName(), + expected.getClass().getSimpleName()); + assertEquals(decoded.getClass(), expected.getClass()); + assertEquals(decoded.size(), expected.size()); + assertEquals(decoded.getTypeField(), expected.getTypeField()); + assertEquals(decoded.isAckEliciting(), expected.isAckEliciting()); + } + + // This is not a full-fledged test for packet encoding/decoding. + // Just a smoke test to verify that the ACK-eliciting property + // survives encoding/decoding + private void checkPacket(QuicPacket packet) { + int size = packet.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + System.out.println("Checking packet: " + packet.getClass()); + try { + var encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1); + var decoder = QuicPacketDecoder.of(QuicVersion.QUIC_V1); + encoder.encode(packet, buffer, CONTEXT); + buffer.flip(); + var decoded = decoder.decode(buffer, CONTEXT); + assertEquals(decoded.size(), packet.size()); + assertEquals(decoded.packetType(), packet.packetType()); + assertEquals(decoded.payloadSize(), packet.payloadSize()); + assertEquals(decoded.isAckEliciting(), packet.isAckEliciting()); + var frames = packet.frames(); + var decodedFrames = decoded.frames(); + assertEquals(decodedFrames.size(), frames.size()); + } catch (Exception x) { + throw new AssertionError(packet.getClass().getName(), x); + } + + } +} diff --git a/test/jdk/java/net/httpclient/quic/AckFrameTest.java b/test/jdk/java/net/httpclient/quic/AckFrameTest.java new file mode 100644 index 00000000000..129394f126c --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/AckFrameTest.java @@ -0,0 +1,387 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckFrameBuilder; +import jdk.internal.net.http.quic.frames.AckFrame.AckRange; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.LongPredicate; +import java.util.stream.LongStream; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; + +/** + * @test + * @summary tests the logic to build an AckFrame + * @library /test/lib + * @run testng AckFrameTest + */ +public class AckFrameTest { + + static final Random RANDOM = RandomFactory.getRandom(); + + private static abstract class TestCodingContext implements CodingContext { + TestCodingContext() { } + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new AssertionError("should not come here!"); + } + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new AssertionError("should not come here!"); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicTLSEngine getTLSEngine() { + throw new AssertionError("should not come here!"); + } + } + + static final int CIDLEN = RANDOM.nextInt(5, QuicConnectionId.MAX_CONNECTION_ID_LENGTH + 1); + + private static final TestCodingContext CONTEXT = new TestCodingContext() { + + @Override + public long largestProcessedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public long largestAckedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public int connectionIdLength() { + return CIDLEN; + } + + @Override + public QuicConnectionId originalServerConnId() { + return null; + } + }; + + public static record Acknowledged(long first, long last) { + public boolean contains(long packet) { + return first <= packet && last >= packet; + } + public static List of(long... numbers) { + if (numbers == null || numbers.length == 0) return List.of(); + if (numbers.length%2 != 0) throw new IllegalArgumentException(); + List res = new ArrayList<>(numbers.length/2); + for (int i = 0; i < numbers.length; i += 2) { + res.add(new Acknowledged(numbers[i], numbers[i+1])); + } + return List.copyOf(res); + } + } + public static record Packet(long packetNumber) { + static List ofAcks(List acks) { + return packets(acks); + } + static List of(long... numbers) { + return LongStream.of(numbers).mapToObj(Packet::new).toList(); + } + } + + public static record TestCase(List acks, List packets, boolean shuffled) { + public TestCase(List acks) { + this(acks, Packet.ofAcks(acks), false); + } + public TestCase shuffle() { + List shuffled = new ArrayList<>(); + shuffled.addAll(packets); + Collections.shuffle(shuffled, RANDOM); + return new TestCase(acks, List.copyOf(shuffled), true); + } + } + + List generateTests() { + List tests = new ArrayList<>(); + List simples = List.of( + new TestCase(List.of(new Acknowledged(5,5))), + new TestCase(List.of(new Acknowledged(5,7))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,9))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,7))), + new TestCase(List.of(new Acknowledged(3,3), new Acknowledged(5,7))) + ); + tests.addAll(simples); + List specials = List.of( + new TestCase(Acknowledged.of(5,5,7,7), Packet.of(5,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(5,7,6), true), + new TestCase(Acknowledged.of(6,7), Packet.of(6,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(6,7,5), true), + new TestCase(Acknowledged.of(5,7), Packet.of(5,6,7), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(5, 7, 8), true), + new TestCase(Acknowledged.of(5,5,8,8), Packet.of(8, 5), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(8, 5, 7), true), + new TestCase(Acknowledged.of(3,5,7,9), Packet.of(8,5,7,4,9,3), true), + new TestCase(Acknowledged.of(27,27,31,31), + Packet.of(27, 31), true), + new TestCase(Acknowledged.of(27,27,29,29,31,31), + Packet.of(27, 31, 29), true), + new TestCase(Acknowledged.of(3,5,7,7,9,9,22,22,27,27,29,29,31,31), + Packet.of(4,22,27,31,9,29,7,5,3), true) + ); + tests.addAll(specials); + for (int i=0; i < 5; i++) { + List acks = generateAcks(); + List packets = packets(acks); + TestCase test = new TestCase(acks, List.copyOf(packets), false); + tests.add(test); + for (int j = 0; j < 5; j++) { + tests.add(test.shuffle()); + } + } + return tests; + } + + List generateAcks() { + int count = RANDOM.nextInt(3, 10); + List acks = new ArrayList<>(count); + long prev = -1; + for (int i=0; i packets(List acks) { + List res = new ArrayList<>(); + for (Acknowledged ack : acks) { + for (long i = ack.first() ; i<= ack.last() ; i++) { + var packet = new Packet(i); + assert !res.contains(packet); + res.add(packet); + } + } + return res; + } + + @DataProvider(name = "tests") + public Object[][] tests() { + return generateTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "tests") + public void testAckFrames(TestCase testCase) { + AckFrameBuilder builder = new AckFrameBuilder(); + List acks = testCase.acks; + List packets = testCase.packets; + long largest = packets.stream() + .mapToLong(Packet::packetNumber) + .max().getAsLong(); + System.out.printf("%ntestAckFrames(%s, %s)%n", acks, testCase.shuffled); + builder.ackDelay(250); + packets.stream().mapToLong(Packet::packetNumber).forEach(builder::addAck); + AckFrame frame = builder.build(); + System.out.printf(" -> %s%n", frame); + checkFrame(frame, testCase, packets, frame); + checkAcknowledging(builder::isAcknowledging, testCase, packets); + + AckFrameBuilder dup = new AckFrameBuilder(frame); + assertEquals(frame, dup.build()); + assertEquals(frame, builder.build()); + checkAcknowledging(dup::isAcknowledging, testCase, packets); + + packets.stream().mapToLong(Packet::packetNumber).forEach(builder::addAck); + checkFrame(builder.build(), testCase, packets, frame); + checkAcknowledging(builder::isAcknowledging, testCase, packets); + + packets.stream().mapToLong(Packet::packetNumber).forEach(dup::addAck); + checkFrame(dup.build(), testCase, packets, frame); + checkAcknowledging(dup::isAcknowledging, testCase, packets); + + AckFrameBuilder dupdup = new AckFrameBuilder(); + dupdup.ackDelay(250); + List dups = new ArrayList<>(packets); + dups.addAll(packets); + dups.addAll(packets); + Collections.shuffle(dups, RANDOM); + dups.stream().mapToLong(Packet::packetNumber).forEach(dupdup::addAck); + checkFrame(dupdup.build(), testCase, dups, frame); + checkAcknowledging(dupdup::isAcknowledging, testCase, packets); + + } + + private void checkFrame(AckFrame frame, TestCase testCase, List packets, AckFrame reference) { + long largest = testCase.packets.stream() + .mapToLong(Packet::packetNumber) + .max().getAsLong(); + assertEquals(frame.largestAcknowledged(), largest); + checkAcknowledging(frame::isAcknowledging, testCase, packets); + for (var ack : testCase.acks) { + checkRangeAcknowledged(frame, ack.first, ack.last); + } + assertEquals(frame, reference); + int size = frame.size(); + ByteBuffer buffer = ByteBuffer.allocate(size + 10); + buffer.position(5); + buffer.limit(size + 5); + try { + frame.encode(buffer); + assertEquals(buffer.position(), buffer.limit()); + buffer.position(5); + buffer.limit(buffer.capacity()); + var decoded = QuicFrame.decode(buffer); + assertEquals(buffer.position(), size + 5); + assertEquals(decoded, frame); + assertEquals(decoded, reference); + } catch (Exception e) { + throw new AssertionError("Can't encode or decode frame: " + frame, e); + } + } + + private void checkRangeAcknowledged(AckFrame frame, long first, long last) { + assertTrue(frame.isRangeAcknowledged(first, last), + "range [%s, %s] should be acked".formatted(first, last)); + if (first > 0) { + if (!frame.isAcknowledging(first - 1)) { + assertFalse(frame.isRangeAcknowledged(first -1, last), + "range [%s, %s] should not be acked".formatted(first -1, last)); + } else { + assertTrue(frame.isRangeAcknowledged(first - 1, last), + "range [%s, %s] should be acked".formatted(first - 1, last)); + if (frame.isAcknowledging(last + 1)) { + assertTrue(frame.isRangeAcknowledged(first -1, last + 1), + "range [%s, %s] should be acked".formatted(first -1, last+1)); + } + } + } + if (!frame.isAcknowledging(last + 1)) { + assertFalse(frame.isRangeAcknowledged(first, last + 1), + "range [%s, %s] should not be acked".formatted(first, last + 1)); + } else { + assertTrue(frame.isRangeAcknowledged(first, last+1), + "range [%s, %s] should be acked".formatted(first, last + 1)); + } + if (last - 1 >= first) { + assertTrue(frame.isRangeAcknowledged(first + 1, last), + "range [%s, %s] should be acked".formatted(first + 1, last)); + assertTrue(frame.isRangeAcknowledged(first, last - 1), + "range [%s, %s] should be acked".formatted(first, last - 1)); + } + if (last - 2 >= first) { + assertTrue(frame.isRangeAcknowledged(first + 1, last - 1), + "range [%s, %s] should be acked".formatted(first + 1, last - 1)); + } + } + + private void checkAcknowledging(LongPredicate isAckPredicate, + TestCase testCase, + List packets) { + long largest = testCase.packets.stream() + .mapToLong(Packet::packetNumber) + .max().getAsLong(); + for (long i = largest + 10; i >= 0; i--) { + long pn = i; + boolean expected = testCase.acks.stream().anyMatch((a) -> a.contains(pn)); + boolean isAcknowledging = isAckPredicate.test(pn); + if (isAcknowledging != expected && testCase.shuffled) { + System.out.printf(" -> %s%n", packets); + } + assertEquals(isAcknowledging, expected, String.valueOf(pn)); + } + for (var p : testCase.packets) { + boolean isAcknowledging = isAckPredicate.test(p.packetNumber); + if (!isAcknowledging && testCase.shuffled) { + System.out.printf(" -> %s%n", packets); + } + assertEquals(isAcknowledging, true, p.toString()); + } + } + + @Test + public void simpleTest() { + AckFrame frame = new AckFrame(1, 0, List.of(new AckRange(0,0))); + System.out.println("simpleTest: " + frame); + assertTrue(frame.isAcknowledging(1), "1 should be acked"); + assertFalse(frame.isAcknowledging(0), "0 should not be acked"); + assertFalse(frame.isAcknowledging(2), "2 should not be acked"); + assertEquals(frame.smallestAcknowledged(), 1); + assertEquals(frame.largestAcknowledged(), 1); + assertEquals(frame.acknowledged().toArray(), new long[] {1L}); + assertTrue(frame.isRangeAcknowledged(1,1), "[1,1] should be acked"); + assertFalse(frame.isRangeAcknowledged(0, 1), "[0,1] should not be acked"); + assertFalse(frame.isRangeAcknowledged(1, 2), "[1,2] should not be acked"); + assertFalse(frame.isRangeAcknowledged(0, 2), "[0,2] should not be acked"); + + frame = new AckFrame(1, 0, List.of(new AckRange(0,1))); + System.out.println("simpleTest: " + frame); + assertTrue(frame.isAcknowledging(1), "1 should be acked"); + assertTrue(frame.isAcknowledging(0), "0 should be acked"); + assertFalse(frame.isAcknowledging(2), "2 should not be acked"); + assertEquals(frame.smallestAcknowledged(), 0); + assertEquals(frame.largestAcknowledged(), 1); + assertEquals(frame.acknowledged().toArray(), new long[] {1L, 0L}); + assertTrue(frame.isRangeAcknowledged(0,0), "[0,0] should be acked"); + assertTrue(frame.isRangeAcknowledged(1,1), "[1,1] should be acked"); + assertTrue(frame.isRangeAcknowledged(0, 1), "[0,1] should be acked"); + assertFalse(frame.isRangeAcknowledged(1, 2), "[1,2] should not be acked"); + assertFalse(frame.isRangeAcknowledged(0, 2), "[0,2] should not be acked"); + + frame = new AckFrame(10, 0, List.of(new AckRange(0,3), new AckRange(2, 3))); + System.out.println("simpleTest: " + frame); + assertTrue(frame.isAcknowledging(10), "10 should be acked"); + assertTrue(frame.isAcknowledging(0), "0 should be acked"); + assertTrue(frame.isRangeAcknowledged(0, 3), "[0,3] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[7,10] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[0,2] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[1,3] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[1,2] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[7,9] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[8,10] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[8,9] should be acked"); + assertFalse(frame.isRangeAcknowledged(0, 10), "[0,10] should not be acked"); + assertFalse(frame.isRangeAcknowledged(4, 6), "[4,6] should not be acked"); + assertFalse(frame.isRangeAcknowledged(4, 6), "[3,7] should not be acked"); + assertFalse(frame.isRangeAcknowledged(4, 6), "[2,8] should not be acked"); + } + } diff --git a/test/jdk/java/net/httpclient/quic/BuffersReaderTest.java b/test/jdk/java/net/httpclient/quic/BuffersReaderTest.java new file mode 100644 index 00000000000..8d03f4265b3 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/BuffersReaderTest.java @@ -0,0 +1,520 @@ +/* + * 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. + * + * 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. + */ + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.*; + + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.quic + * @run junit/othervm BuffersReaderTest + * @summary Tests various BuffersReader methods + * work as expected. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class BuffersReaderTest { + static final Class IAE = IllegalArgumentException.class; + + static final Random RAND = RandomFactory.getRandom(); + static final int GENERATED = 2; + + // describes a byte buffer at a given global offset + record BB(long globalOffset, int position, int length, int capacity) {} + record Simple(long position, long index, int expected) {} + + record TestCase(List bbs, List simples) {} + + + // describes a BuffersReader configuration composed of 5 bytes buffer + // added with various position, limit, and capacity (limit = position + length) + List specialCases = List.of(new TestCase(List.of( + new BB(0, 10, 10, 30), + new BB(10, 5, 10, 20), + new BB(20, 15, 10, 40), + new BB(30, 0, 10, 20), + new BB(40, 5, 10, 20)), + List.of(new Simple(11, 50, 40)) + )); + + + private List tests() { + int generated = 2; + List allcases = new ArrayList<>(specialCases.size() + GENERATED); + allcases.addAll(specialCases); + for (int i = 0; i < GENERATED; i++) { + allcases.add(new TestCase(generateBBs(), List.of())); + } + return allcases; + } + + private List generateBBs() { + var bbscount = RAND.nextInt(1, 11); + List bbs = new ArrayList<>(bbscount); + long globalOffset = 0; + for (int i = 0; i < bbscount; i++) { + int length = RAND.nextInt(1, 11); + int offset = RAND.nextInt(0,3); + int tail = RAND.nextInt(0,3); + bbs.add(new BB(globalOffset, offset, length, offset + length + tail)); + globalOffset += length; + } + return List.copyOf(bbs); + } + + + @Test + public void testGet() { + test("hello world".getBytes(StandardCharsets.US_ASCII), 2, 10); + } + + @Test + public void testGetPos6() { + test("May the road rise up to meet you".getBytes(StandardCharsets.US_ASCII), 6, 23); + } + + @Test + public void testGetPos0() { + test("May the wind always be at your back".getBytes(StandardCharsets.US_ASCII), 0, 29); + } + + public void test(byte[] values, int position, int limit) { + ByteBuffer bb = ByteBuffer.wrap(values); + bb.position(position); + bb.limit(limit); + + ListBuffersReader br = BuffersReader.list(bb); + assertEquals(br.position(), position); + assertEquals(br.limit(), limit); + for (int i = position; i < limit; i++) { + int j = limit - (i - position) - 1; + System.err.printf("%ntesting(v[i:%s]=%s, v[j:%s]=%s)%n", + i, values[i], j, values[j]); + assertEquals(br.position(), i); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", i, i); + assertEquals((char)br.get(i), (char)values[i]); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", j, j); + assertEquals((char)br.get(j), (char)values[j]); + assertEquals(br.position(), i); + System.err.printf("assertEquals((char)br.get(), (char)values[%s])%n", i); + assertEquals((char)br.get(), (char)values[i]); + assertEquals(br.position(), i+1); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", i, i); + assertEquals((char)br.get(i), (char)values[i]); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", j, j); + assertEquals((char)br.get(j), (char)values[j]); + } + assertEquals(br.position(), br.limit()); + br.release(); + assertEquals(br.position(), 0); + assertEquals(br.limit(), 0); + bb.position(0); + bb.limit(bb.capacity()); + int start = 0; + limit = bb.limit(); + br.add(bb); + + final int N = 3; + for (int i = 1 ; i < N; i++) { + ByteBuffer bbb = ByteBuffer.allocate(bb.limit() + 4); + bbb.put((byte)-1); + bbb.put((byte)-2); + bbb.put(bb.slice()); + bbb.put((byte)-3); + bbb.put((byte)-4); + bbb.position(2); + bbb.limit(2 + bb.limit()); + br.add(bbb); + } + + long read = br.read(); + for (int i = start; i < N*limit; i++) { + var vi = values[i%limit]; + var j = N*limit - i - 1; + var vj = values[j%limit]; + System.err.printf("%ndouble testing(v[i:%s]=%s, v[j:%s]=%s) position: %s%n", + i, vi, j, vj, br.position()); + assertEquals(br.get(i), vi); + assertEquals(br.get(j), vj); + assertEquals(br.get(), vi); + assertEquals(br.get(i), vi); + assertEquals(br.get(j), vj); + } + assertEquals(br.position(), N * values.length); + assertEquals(br.read() - read, N * values.length - start); + + if (N > 2) { + System.err.printf("testing getAndRelease()%n"); + br.position(values.length + position); + assertEquals(br.position(), values.length + position); + assertEquals(br.read() - read, values.length + position - start); + var bbl = br.getAndRelease(values.length); + assertEquals(bbl.size(), (position == 0 ? 1 : 2)); + // We expect bbl.getFirst() to be the second byte buffer, which will + // have an offset of 2. The position in that byte buffer + // should therefore be position + 2, since we moved the + // position of the buffers reader to values.length + + // position before calling getAndRelease. + assertEquals(position + 2, bbl.getFirst().position()); + int rstart = (int) bbl.getFirst().position(); + ListBuffersReader br2 = BuffersReader.list(bbl); + System.err.printf("position=%s, bbl[0].position=%s%n", position, rstart); + // br2 initial position should reflect the initial position + // of the first buffer in the bbl list. + assertEquals(br2.position(), rstart); + try { + br2.position(rstart - 1); + throw new AssertionError("Expected IllegalArgumentException not thrown"); + } catch (IllegalArgumentException iae) { + System.err.printf("Got expected exception" + + " trying to move before initial position: %s%n", iae); + } + assertEquals(br2.limit(), values.length + rstart); + for (int i = 0; i < values.length; i++) { + assertEquals(br2.get(), values[(i + position) % values.length]); + } + } + } + + // Encode the given length and then decodes it and compares + // the results, asserting various invariants along the way. + @Test + public void testEncodeDecodeVL() { + testEncodeDecodeVL(4611686018427387903L, 3); + } + + public void testEncodeDecodeVL(long length, int expectedPrefix) { + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(10)) { + System.err.printf("Encode/Decode %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too short, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer shorter = ByteBuffer.allocate(actualSize - 1 + offset); + shorter.position(offset); + ByteBuffer shorterref = ByteBuffer.allocate(actualSize - 1 + offset); + shorterref.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(offset); + shortSlice.limit(offset + actualSize - 1); + ListBuffersReader br = BuffersReader.list(shortSlice); + var actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), offset); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + assertEquals(br.position(), offset); + assertEquals(br.limit(), offset + actualSize - 1); + br.release(); + + // decode with the exact buffer + br = BuffersReader.list(exact); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(exact.position(), offset + actualSize); + assertFalse(exact.hasRemaining()); + assertEquals(br.position(), offset + actualSize); + assertFalse(br.hasRemaining()); + br.release(); + assertEquals(br.read(), actualSize); + assertFalse(br.hasRemaining()); + + + // decode with the longer buffer + long read = br.read(); + assertEquals(br.limit(), 0); + assertEquals(br.position(), 0); + br.add(longer); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.remaining(), 10); + assertEquals(br.position(), offset + actualSize); + assertEquals(br.remaining(), 10); + br.release(); + assertEquals(br.read() - read, actualSize); + assertEquals(br.remaining(), 10); + } + } + + @ParameterizedTest + @MethodSource("tests") + void testAbsolutes(TestCase testCase) { + + List bbs = testCase.bbs(); + // Add byte buffers that match the description in bbs to the BuffersReader. + // The byte buffer bytes that should never be read are set to -1, this way + // if a get returns -1 we know it's peeking outside the expected range. + // bytes at any valid readable position are set to (position - start) % 128 + var reader = BuffersReader.list(); + int val = 0; + for (var bb : bbs) { + var b = ByteBuffer.allocate(bb.capacity); + for (int i=0; i reader.get()); + System.err.printf("Got expected BufferUnderflowException for %s: %s%n", reader.position(), bue); + + if (!testCase.simples.isEmpty()) { + System.err.println("\n*** Simple tests\n"); + } + for (var simple : testCase.simples) { + System.err.printf("get(%s) with position=%s, expect %s%n", + simple.index, simple.position, simple.expected); + long p0 = reader.position(); + reader.position(simple.position); + assertEquals(reader.get(simple.index), simple.expected); + reader.position(p0); + assertEquals(reader.position(), reader.limit()); + } + + System.err.println("\n*** Testing BuffersReader::get(long)\n"); + for (long i=0; i < limit; i++) { + final long pos = i; + if (pos < start) { + var ioobe = assertThrows(IndexOutOfBoundsException.class, () -> reader.get(pos)); + System.err.printf("Got expected IndexOutOfBoundsException for %s: %s%n", pos, ioobe); + } else { + assertEquals(reader.get(pos), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + } + } + System.err.println("\n*** Testing BuffersReader::position(long)\n"); + for (long i=0; i <= limit; i++) { + final long pos = limit-i; + final long rpos = i; + if (pos < start) { + try { + var iae = assertThrows(IAE, () -> reader.position(pos)); + System.err.printf("Got expected IllegalArgumentException for %s: %s%n", pos, iae); + } catch (AssertionError error) { + System.err.printf(error.getMessage() + " for start: %s, index: %s, limit: %s", + start, pos, limit); + throw error; + } + } else { + System.err.printf("> reader.position(%s -> %s)%n", reader.position(), pos); + reader.position(pos); + if (pos < limit) { + try { + assertEquals(reader.get(), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + System.err.printf("> reader.position is now %s%n", reader.position()); + assertEquals(reader.read(), pos - start + 1); + } catch (RuntimeException x) { + System.err.println("get failed at index " + pos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + } + if (rpos >= start && rpos < limit) { + try { + System.err.printf("get(%s) with position=%s, expect %s%n", + rpos, reader.position(), (rpos - start) % 128); + assertEquals(reader.get(rpos), (rpos - start) % 128, + "get failed at index " + rpos + " " + + "(start: " + start + ", limit: " + limit + ")"); + } catch (RuntimeException x) { + System.err.println("get failed at index " + rpos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + assertEquals(reader.read(), reader.position() - start); + if (rpos < start) { + var iae = assertThrows(IAE, () -> reader.position(rpos)); + System.err.printf("Got expected IllegalArgumentException for %s: %s%n", rpos, iae); + } else { + System.err.printf("< reader.position(%s -> %s)%n", reader.position(), rpos); + reader.position(rpos); + if (rpos < limit) { + try { + assertEquals(reader.get(), (rpos - start) % 128, + "get failed at index " + rpos + " " + + "(start: " + start + ", limit: " + limit + ")"); + assertEquals(reader.read(), rpos - start + 1); + System.err.printf("< reader.position is now %s%n", reader.position()); + } catch (RuntimeException x) { + System.err.println("get failed at index " + rpos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + } + if (pos >= start && pos < limit) { + try { + System.err.printf("get(%s) with position=%s, expect %s%n", + pos, reader.position(), (pos - start) % 128); + assertEquals(reader.get(pos), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + } catch (RuntimeException x) { + System.err.println("get failed at index " + pos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + assertEquals(reader.read(), reader.position() - start); + } + + System.err.println("\n*** Testing BuffersReader::position(rand1) and get(rand2)\n"); + List positions = LongStream.range(0, limit+1).mapToObj(Long::valueOf) + .collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(positions, RAND); + List indices = LongStream.range(0, limit+1).mapToObj(Long::valueOf) + .collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(indices, RAND); + for (int i = 0; i <= limit; i++) { + long pos = positions.get(i); + long index = indices.get(i); + System.err.printf("position(%s) -> get() -> get(%s)%n", pos, index); + if (pos < start) { + try { + var iae = assertThrows(IAE, () -> reader.position(pos)); + System.err.printf("Got expected IllegalArgumentException for %s: %s%n", pos, iae); + } catch (AssertionError error) { + System.err.printf(error.getMessage() + " for start: %s, index: %s, limit: %s", + start, pos, limit); + throw error; + } + } else { + System.err.printf("> reader.position(%s -> %s)%n", reader.position(), pos); + reader.position(pos); + if (pos < limit) { + try { + assertEquals(reader.get(), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + System.err.printf("> reader.position is now %s%n", reader.position()); + assertEquals(reader.read(), pos - start + 1); + } catch (RuntimeException x) { + System.err.println("get failed at index " + pos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + } + if (index < start || index >= limit) { + var ioobe = assertThrows(IndexOutOfBoundsException.class, () -> reader.get(index)); + System.err.printf("Got expected IndexOutOfBoundsException for %s: %s%n", index, ioobe); + } else { + assertEquals(reader.get(index), (index - start) % 128, + "get failed at index " + index + " " + + "(start: " + start + ", limit: " + limit + ")"); + } + } + + } +} diff --git a/test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java b/test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java new file mode 100644 index 00000000000..5064bbd5cb3 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java @@ -0,0 +1,325 @@ +/* + * 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. + * + * 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. + */ + +import java.nio.ByteBuffer; +import java.util.List; + +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jtreg.SkippedException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.quic + * @run testng/othervm BuffersReaderVLTest + * @summary Tests to check quic/util methods encode/decodeVariableLength methods + * work as expected. + */ +public class BuffersReaderVLTest { + static final Class IAE = IllegalArgumentException.class; + + @DataProvider(name = "decode invariants") + public Object[][] decodeInvariants() { + return new Object[][] + { + { new byte[]{7}, 7, 1 }, // 00 + { new byte[]{65, 11}, 267, 2 }, // 01 + { new byte[]{-65, 11, 22, 33}, 1057691169, 4 }, // 10 + { new byte[]{-1, 11, 22, 33, 44, 55, 66, 77}, 4542748980864827981L, 8 }, // 11 + { new byte[]{-1, -11, -22, -33, -44, -55, -66, -77}, 4608848040752168627L, 8 }, + { new byte[]{}, -1, 0 }, + { new byte[]{-65}, -1, 0 }, + }; + } + @DataProvider(name = "prefix invariants") + public Object[][] prefixInvariants() { + return new Object[][] + { + { Long.MAX_VALUE, 0, IAE }, + { 4611686018427387903L+1, 0, IAE }, + { 4611686018427387903L, 3, null }, + { 4611686018427387903L-1, 3, null }, + { 1073741823+1, 3, null }, + { 1073741823, 2, null }, // (length > (1L << 30)-1) + { 1073741823-1, 2, null }, + { 16383+1, 2, null }, + { 16383, 1, null }, // (length > (1L << 14)-1 + { 16383-1, 1, null }, + { 63+1, 1, null }, + { 63 , 0, null }, // (length > (1L << 6)-1 + { 63-1, 0, null }, + { 100, 1, null }, + { 10, 0, null }, + { 1, 0, null }, + { 0, 0, null }, // (length >= 0) + { -1, 0, IAE }, + { -10, 0, IAE }, + { -100, 0, IAE }, + { Long.MIN_VALUE, 0, IAE }, + { -4611686018427387903L-1, 0, IAE }, + { -4611686018427387903L, 0, IAE }, + { -4611686018427387903L+1, 0, IAE }, + { -1073741823-1, 0, IAE }, + { -1073741823, 0, IAE }, // (length > (1L << 30)-1) + { -1073741823+1, 0, IAE }, + { -16383-1, 0, IAE }, + { -16383, 0, IAE }, // (length > (1L << 14)-1 + { -16383+1, 0, IAE }, + { -63-1, 0, IAE }, + { -63 , 0, IAE }, // (length > (1L << 6)-1 + { -63+1, 0, IAE }, + }; + } + + @Test(dataProvider = "decode invariants") + public void testDecode(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + BuffersReader br = BuffersReader.list(bb); + var actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, expectedPosition); + assertEquals(br.position(), expectedPosition); + br.release(); + assertEquals(br.read(), expectedPosition); + } + + @Test(dataProvider = "decode invariants") + public void testPeek(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + BuffersReader br = BuffersReader.list(bb); + var actualLength = VariableLengthEncoder.peekEncodedValue(br, 0); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, 0); + assertEquals(br.position(), 0); + br.release(); + assertEquals(br.read(), 0); + } + + // Encode the given length and then decodes it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodeDecode(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + } else { + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Decode %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too short, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer shorter = ByteBuffer.allocate(actualSize - 1 + offset); + shorter.position(offset); + ByteBuffer shorterref = ByteBuffer.allocate(actualSize - 1 + offset); + shorterref.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer too short + expectThrows(IAE, () -> VariableLengthEncoder.encode(shorter, length)); + assertEquals(shorter.position(), offset); + assertEquals(shorter.limit(), shorter.capacity()); + + assertEquals(shorter.mismatch(shorterref), -1); + assertEquals(shorterref.mismatch(shorter), -1); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(offset); + shortSlice.limit(offset + actualSize -1); + ListBuffersReader br = BuffersReader.list(shortSlice); + var actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), offset); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + assertEquals(br.position(), offset); + assertEquals(br.limit(), offset + actualSize - 1); + br.release(); + + // decode with the exact buffer + br = BuffersReader.list(exact); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(exact.position(), offset + actualSize); + assertFalse(exact.hasRemaining()); + assertEquals(br.position(), offset + actualSize); + assertFalse(br.hasRemaining()); + br.release(); + assertEquals(br.read(), actualSize); + assertFalse(br.hasRemaining()); + + + // decode with the longer buffer + long read = br.read(); + br.add(longer); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.remaining(), 10); + assertEquals(br.position(), offset + actualSize); + assertEquals(br.remaining(), 10); + br.release(); + assertEquals(br.read() - read, actualSize); + assertEquals(br.remaining(), 10); + } + + } + } + + // Encode the given length and then peeks it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodePeek(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + return; + } + + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Peek %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + exact.position(0); + longer.position(0); + exact.limit(exact.capacity()); + longer.limit(longer.capacity()); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(0); + shortSlice.limit(offset + actualSize - 1); + // need at least one byte to decode the size len... + var expectedSize = shortSlice.limit() <= offset ? -1 : actualSize; + assertEquals(VariableLengthEncoder.peekEncodedValueSize(shortSlice, offset), expectedSize); + var actualLength = VariableLengthEncoder.peekEncodedValue(shortSlice, offset); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), 0); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + + // decode with the exact buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(exact, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(exact, offset); + assertEquals(actualLength, length); + assertEquals(exact.position(), 0); + assertEquals(exact.limit(), exact.capacity()); + + // decode with the longer buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(longer, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(longer, offset); + assertEquals(actualLength, length); + assertEquals(longer.position(), 0); + assertEquals(longer.limit(), longer.capacity()); + } + + } + + + private ByteBuffer getTestBuffer(long length, int capacity) { + return switch (capacity) { + case 0 -> ByteBuffer.allocate(1).put((byte) length); + case 1 -> ByteBuffer.allocate(capacity).put((byte) length); + case 2 -> ByteBuffer.allocate(capacity).putShort((short) length); + case 4 -> ByteBuffer.allocate(capacity).putInt((int) length); + case 8 -> ByteBuffer.allocate(capacity).putLong(length); + default -> throw new SkippedException("bad value used for capacity"); + }; + } +} diff --git a/test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java b/test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java new file mode 100644 index 00000000000..e7fabeeecac --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java @@ -0,0 +1,170 @@ +/* + * 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. + * + * 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. + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; + +import jdk.internal.net.http.quic.QuicConnectionIdFactory; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +/** + * @test + * @run testng/othervm ConnectionIDSTest + */ +public class ConnectionIDSTest { + + record ConnID(long token, byte[] bytes) { + ConnID { + bytes = bytes.clone(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnID connID = (ConnID) o; + return Arrays.equals(bytes, connID.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @Override + public String toString() { + return "ConnID{" + + "token=" + token + + ", bytes=" + HexFormat.of().formatHex(bytes) + + '}'; + } + } + + @Test + public void testConnectionIDS() { + List ids = new ArrayList<>(); + + // regular test, for length in [-21, 21] + long previous = 0; + QuicConnectionIdFactory idFactory = QuicConnectionIdFactory.getClient(); + for (int length = -21; length <= 22 ; length++) { + int expectedLength = Math.min(length, 20); + expectedLength = Math.max(9, expectedLength); + long token = idFactory.newToken(); + assertEquals(token, previous +1); + previous = token; + var id = idFactory.newConnectionId(length, token); + var cid = new ConnID(token, id); + System.out.printf("%s: %s/%s%n", length, token, cid); + assertEquals(id.length, expectedLength); + assertEquals(idFactory.getConnectionIdLength(id), expectedLength); + assertEquals(idFactory.getConnectionIdToken(id), token); + ids.add(cid); + } + + // token length test, for token coded on [1, 8] bytes, + // with positive and negative values, for cid length=9 + // Ox7F, -Ox7F, 0x7F7F, -0x7F7F, etc... + previous = 0; + int length = 9; + for (int i=0; i<8; i++) { + long ptoken = (previous << 8) + 0x7F; + long ntoken = - ptoken; + previous = ptoken; + for (long token : List.of(ptoken, ntoken)) { + long expectedToken = token >= 0 ? token : -token -1; + var id = idFactory.newConnectionId(length, token); + var cid = new ConnID(expectedToken, id); + System.out.printf("%s: %s/%s%n", length, token, cid); + assertEquals(id.length, length); + assertEquals(idFactory.getConnectionIdLength(id), length); + assertEquals(idFactory.getConnectionIdToken(id), expectedToken); + ids.add(cid); + } + } + + // test token bounds, for various cid length... + var bounds = List.of(Long.MIN_VALUE, Long.MIN_VALUE + 1L, Long.MIN_VALUE + 255L, -1L, + 0L, 1L, Long.MAX_VALUE -255L, Long.MAX_VALUE - 1L, Long.MAX_VALUE); + // test the bounds twice to try to trigger duplicates with length = 9 + bounds = bounds.stream().mapMulti((n,c) -> {c.accept(n); c.accept(n);}).toList(); + for (length=9; length <= 20; length++) { + for (long token : bounds) { + long expectedToken = token >= 0 ? token : -token - 1; + var id = idFactory.newConnectionId(length, token); + var cid = new ConnID(expectedToken, id); + System.out.printf("%s: %s/%s%n", length, token, cid); + assertEquals(id.length, length); + assertEquals(idFactory.getConnectionIdLength(id), length); + assertEquals(idFactory.getConnectionIdToken(id), expectedToken); + ids.add(cid); + } + } + + // now verify uniqueness + Map tested = new HashMap(); + record duplicates(ConnID first, ConnID second) {} + List duplicates = new ArrayList<>(); + for (var cid : ids) { + if (tested.containsKey(cid)) { + var dup = new duplicates(tested.get(cid), cid); + System.out.printf("duplicate ids: %s%n", dup); + duplicates.add(dup); + } else { + tested.put(cid, cid); + } + } + + // some duplicates can be expected if the connection id is too short + // and the token value is too big; check and remove them + for (var iter = duplicates.iterator(); iter.hasNext(); ) { + var dup = iter.next(); + assertEquals(dup.first.token(), dup.second.token()); + assertEquals(dup.first.bytes().length, dup.second.bytes().length); + assertEquals(dup.first.bytes(), dup.second.bytes()); + long mask = 0x00FFFFFF00000000L; + for (int i=0; i<3; i++) { + mask = mask << 8; + assert (mask & 0xFF00000000000000L) != 0 + : "mask: " + Long.toHexString(mask); + if (dup.first.bytes().length == (9+i)) { + if ((dup.first.token() & mask) != 0L) { + iter.remove(); + System.out.println("duplicates expected due to lack of entropy: " + dup); + } + } + } + } + + // verify no unexpected duplicates + for (var dup : duplicates) { + System.out.println("unexpected duplicate: " + dup); + } + assertEquals(duplicates.size(), 0); + } +} diff --git a/test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java b/test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java new file mode 100644 index 00000000000..cfc62625605 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java @@ -0,0 +1,73 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.streams.CryptoWriterQueue; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; + +import static org.testng.Assert.*; + +/** + * @test + * @summary Tests jdk.internal.net.http.quic.streams,CryptoWriterQueue + * @modules java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.quic.frames + * @run testng CryptoWriterQueueTest + */ +public class CryptoWriterQueueTest { + + /** + * {@link CryptoWriterQueue#enqueue(ByteBuffer) enqueues} data from multiple ByteBuffer + * instances and then expects the {@link CryptoWriterQueue#produceFrame(int)} to process + * the enqueued data correctly. + */ + @Test + public void testProduceFrame() throws Exception { + final CryptoWriterQueue writerQueue = new CryptoWriterQueue(); + final ByteBuffer buff1 = createByteBuffer(83); + final ByteBuffer buff2 = createByteBuffer(1429); + final ByteBuffer buff3 = createByteBuffer(4); + // enqueue them + writerQueue.enqueue(buff1); + writerQueue.enqueue(buff2); + writerQueue.enqueue(buff3); + final int expectedRemaining = buff1.remaining() + buff2.remaining() + buff3.remaining(); + assertEquals(writerQueue.remaining(), expectedRemaining, + "Unexpected remaining bytes in CryptoWriterQueue"); + // create frame(s) from the enqueued buffers + final int maxPayloadSize = 1134; + while (writerQueue.remaining() > 0) { + final CryptoFrame frame = writerQueue.produceFrame(maxPayloadSize); + assertNotNull(frame, "Crypto frame is null"); + assertTrue(frame.size() <= maxPayloadSize, "Crypto size " + frame.size() + + " exceeds max payload size of " + maxPayloadSize); + } + } + + private static ByteBuffer createByteBuffer(final int numBytes) { + return ByteBuffer.wrap(new byte[numBytes]); + } + +} diff --git a/test/jdk/java/net/httpclient/quic/KeyUpdateTest.java b/test/jdk/java/net/httpclient/quic/KeyUpdateTest.java new file mode 100644 index 00000000000..631a054b2f9 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/KeyUpdateTest.java @@ -0,0 +1,272 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Stack; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import sun.security.ssl.QuicTLSEngineImpl; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/* + * @test + * @summary verifies the QUIC TLS key update process + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @modules java.net.http/jdk.internal.net.http + * java.net.http/jdk.internal.net.http.common + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * + * @modules java.base/jdk.internal.util + * java.base/sun.security.ssl + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.httpclient.test.lib.common.TestUtil + * jdk.test.lib.net.SimpleSSLContext + * @comment the test is run with -Djava.security.properties= to augment + * the master java.security file + * @run testng/othervm -Djava.security.properties=${test.src}/quic-tls-keylimits-java.security + * -Djdk.internal.httpclient.debug=true + * -Djavax.net.debug=all + * KeyUpdateTest + */ +public class KeyUpdateTest { + + private QuicStandaloneServer server; + private SSLContext sslContext; + private ExecutorService executor; + + private static final byte[] HELLO_MSG = "Hello Quic".getBytes(StandardCharsets.UTF_8); + private static final EchoHandler handler = new EchoHandler(HELLO_MSG.length); + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .build(); + // add a handler which deals with incoming connections + server.addHandler(handler); + server.start(); + System.out.println("Server started at " + server.getAddress()); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + if (executor != null) { + executor.close(); + } + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + @Test + public void test() throws Exception { + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + final Stack clientConnKeyPhases = new Stack<>(); + for (int i = 1; i <= 100; i++) { + System.out.println("Iteration: " + i); + // open a bidi stream + final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + // write data on the stream + try (final OutputStream os = bidiStream.outputStream()) { + os.write(HELLO_MSG); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for response + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("client: reading from bidi stream's input stream"); + final byte[] data = is.readAllBytes(); + System.out.println("client: Received response of size " + data.length); + final String response = new String(data, StandardCharsets.UTF_8); + // verify response + System.out.println("client: Response: " + response); + if (!Arrays.equals(response.getBytes(StandardCharsets.UTF_8), HELLO_MSG)) { + throw new AssertionError("Unexpected response: " + response); + } + } finally { + System.err.println("client: Closing bidi stream from test"); + bidiStream.close(); + } + // keep track of the 1-RTT key phase that was used by the client connection + final int invocation = i; + getKeyPhase(conn.underlyingQuicConnection()).ifPresent((keyPhase) -> { + if (clientConnKeyPhases.empty() || clientConnKeyPhases.peek() != keyPhase) { + // new key phase detected, add it + clientConnKeyPhases.push(keyPhase); + System.out.println("Detected client 1-RTT key phase " + keyPhase + + " on connection " + conn + " for invocation " + invocation); + } + }); + } + // verify that the client and server did do a key update + // stacks should contain at least a sequence of 0, 1, 0 + System.out.println("Number of 1-RTT keys used by client connection: " + + clientConnKeyPhases.size() + ", key phase switches: " + clientConnKeyPhases); + System.out.println("Number of 1-RTT keys used by server connection: " + + handler.serverConnKeyPhases.size() + ", key phase switches: " + + handler.serverConnKeyPhases); + assertTrue(clientConnKeyPhases.size() >= 3, "Client connection" + + " didn't do a key update"); + assertTrue(handler.serverConnKeyPhases.size() >= 3, "Server connection" + + " didn't do a key update"); + + assertEquals(0, (int) clientConnKeyPhases.getFirst(), "Client connection used" + + " unexpected first key phase"); + assertEquals(0, (int) handler.serverConnKeyPhases.getFirst(), "Server connection used" + + " unexpected first key phase"); + + assertEquals(1, (int) clientConnKeyPhases.get(1), "Client connection used" + + " unexpected second key phase"); + assertEquals(1, (int) handler.serverConnKeyPhases.get(1), "Server connection used" + + " unexpected second key phase"); + + assertEquals(0, (int) clientConnKeyPhases.get(2), "Client connection used" + + " unexpected third key phase"); + assertEquals(0, (int) handler.serverConnKeyPhases.get(2), "Server connection used" + + " unexpected third key phase"); + } + } + + /** + * Reads data from incoming client initiated bidirectional stream of a Quic connection + * and writes back a response which is same as the read data + */ + private static final class EchoHandler implements QuicServerHandler { + + private final int numBytesToRead; + private final AtomicInteger numInvocations = new AtomicInteger(); + private final Stack serverConnKeyPhases = new Stack<>(); + + private EchoHandler(final int numBytesToRead) { + this.numBytesToRead = numBytesToRead; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + final int invocation = numInvocations.incrementAndGet(); + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + // keep track of the 1-RTT key phase that was used by the server connection + getKeyPhase(conn).ifPresent((keyPhase) -> { + if (this.serverConnKeyPhases.empty() + || this.serverConnKeyPhases.peek() != keyPhase) { + // new key phase detected, add it + this.serverConnKeyPhases.push(keyPhase); + System.out.println("Detected server 1-RTT key phase " + keyPhase + + " on connection " + conn + " for invocation " + invocation); + } + }); + final byte[] data; + // read the request content + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("Handler reading data from bidi stream's inputstream " + is); + data = is.readAllBytes(); + System.out.println("Handler read " + data.length + " bytes of data"); + } + if (data.length != numBytesToRead) { + throw new IOException("Expected to read " + numBytesToRead + + " bytes but read only " + data.length + " bytes"); + } + // write response + try (final OutputStream os = bidiStream.outputStream()) { + System.out.println("Handler writing data to bidi stream's outputstream " + os); + os.write(data); + } + System.out.println("Handler invocation complete"); + } + } + + private static Optional getKeyPhase(final QuicConnection conn) throws IOException { + if (!(conn.getTLSEngine() instanceof QuicTLSEngineImpl qtls)) { + return Optional.empty(); + } + final int keyPhase; + try { + keyPhase = qtls.getOneRttKeyPhase(); + } catch (QuicKeyUnavailableException e) { + throw new IOException("failed to get key phase, reason: " + e.getMessage()); + } + if (keyPhase != 0 && keyPhase != 1) { + throw new IOException("Unexpected 1-RTT key phase on connection: " + conn); + } + return Optional.of(keyPhase); + } +} diff --git a/test/jdk/java/net/httpclient/quic/OrderedFlowTest.java b/test/jdk/java/net/httpclient/quic/OrderedFlowTest.java new file mode 100644 index 00000000000..cb95620c00e --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/OrderedFlowTest.java @@ -0,0 +1,401 @@ +/* + * 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. + * + * 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. + */ + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.ToLongFunction; +import java.util.stream.Collectors; + +import jdk.internal.net.http.quic.OrderedFlow; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; + +/** + * @test + * @summary tests the reordering logic implemented by OrderedFlow + * and its two concrete subclasses + * @library /test/lib + * @run testng OrderedFlowTest + * @run testng/othervm -Dseed=-2680947227866359853 OrderedFlowTest + * @run testng/othervm -Dseed=-273117134353023275 OrderedFlowTest + * @run testng/othervm -Dseed=3649132517916066643 OrderedFlowTest + * @run testng/othervm -Dseed=4568737726943220431 OrderedFlowTest + */ +public class OrderedFlowTest { + + static final int WITH_DUPS = 1; + static final int WITH_OVERLAPS = 2; + + record TestData(Class frameType, + Supplier> flowSupplier, + Function payloadAccessor, + Comparator framesComparator, + List frames, + String expectedResult, + boolean duplicates, + boolean shuffled) { + + boolean hasEmptyFrames() { + return frames.stream().map(payloadAccessor) + .mapToInt(String::length) + .anyMatch((i) -> i == 0); + } + + @Override + public String toString() { + return frameType.getSimpleName() + + "(frames=" + frames.size() + + ", duplicates=" + duplicates + + ", shuffled=" + shuffled + + ", hasEmptyFrames=" + hasEmptyFrames() + + ")"; + } + } + + static final Random RANDOM = jdk.test.lib.RandomFactory.getRandom(); + static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer + id elementum sem. In rhoncus nisi a ante convallis, at iaculis augue + elementum. Ut eget imperdiet justo, sed sodales est. In nec laoreet + lorem. Integer et arcu nibh. Quisque quis felis consectetur, luctus + libero eu, facilisis risus. Aliquam at viverra diam. Sed nec lacus + eget dui hendrerit porttitor et nec ligula. Suspendisse rutrum, + augue non ultricies vestibulum, metus orci faucibus est, et tempus + ante diam a quam. Proin venenatis justo eleifend vestibulum tincidunt. + + Nullam nec elementum sem. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. Integer euismod, + purus ut sollicitudin semper, quam turpis condimentum arcu, sit amet + suscipit sapien elit ac nisi. Orci varius natoque penatibus et magnis + dis parturient montes, nascetur ridiculus mus. Duis vel tortor non purus + scelerisque iaculis at efficitur dolor. Nam dapibus tellus non aliquet + suscipit. Nulla facilisis mi eget ex blandit sodales. Pellentesque enim + sem, aliquet non luctus id, feugiat in eros. Aliquam molestie felis + lorem, eget tristique nisi mollis lobortis. + + Suspendisse aliquam vitae purus nec mollis. Quisque et urna nec nunc + porttitor blandit quis a magna. Maecenas porta est velit, in volutpat + felis suscipit eget. Vivamus porta semper ipsum, et sodales nibh molestie + eget. Vivamus tincidunt quam id ante efficitur tincidunt. Suspendisse + potenti. Integer posuere felis ut semper feugiat. Vivamus id dui quam. + + Pellentesque accumsan quam non est pretium faucibus. Donec vel euismod + magna, ac scelerisque mauris. Nullam vitae varius diam, hendrerit semper + velit. Vestibulum et nisl felis. Orci varius natoque penatibus et magnis + dis parturient montes, nascetur ridiculus mus. Cras elementum auctor lacus, + vel tempor erat lobortis sed. Suspendisse sed felis ut mi condimentum + eleifend. Proin et arcu cursus, fermentum arcu non, tristique nulla. + Suspendisse tristique volutpat elit, et blandit metus aliquet id. Nunc non + dapibus dui. Nam sagittis justo magna. Nulla pharetra ex nec sem porta + consequat. + + Nam sit amet luctus ante, nec eleifend nunc. Phasellus lobortis lorem a + auctor ornare. Sed venenatis fermentum arcu, ut tincidunt turpis auctor + at. Praesent felis mi, tincidunt a sem et, luctus condimentum libero. + Phasellus egestas ac lectus vitae tincidunt. Etiam eu lobortis felis. + Nulla semper est ac nisl placerat, vitae sollicitudin diam lobortis. + Cras pellentesque semper purus at rutrum. Suspendisse a pellentesque + orci, ac tincidunt libero. Integer ex augue, ultrices sit amet aliquam + eget, laoreet eget elit. + """; + + interface FramesFactory { + public T create(int offset, String payload, boolean fin); + public int length(T frame); + public long offset(T frame); + public String getPayload(T frame); + public OrderedFlow flow(); + public Class frameType(); + public Comparator comparator(); + } + + static class StreamFrameFactory implements FramesFactory { + final long streamId = RANDOM.nextInt(0, Integer.MAX_VALUE); + + @Override + public StreamFrame create(int offset, String payload, boolean fin) { + byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + int length = RANDOM.nextBoolean() ? bytes.length : -1; + return new StreamFrame(streamId, offset, length, + fin, ByteBuffer.wrap(bytes)); + } + + @Override + public int length(StreamFrame frame) { + return frame.dataLength(); + } + + @Override + public long offset(StreamFrame frame) { + return frame.offset(); + } + + @Override + public String getPayload(StreamFrame frame) { + int length = frame.dataLength(); + byte[] bytes = new byte[length]; + frame.payload().get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public OrderedFlow flow() { + return new OrderedFlow.StreamDataFlow(); + } + + @Override + public Class frameType() { + return StreamFrame.class; + } + + @Override + public Comparator comparator() { + return StreamFrame::compareOffsets; + } + } + + static class CryptoFrameFactory implements FramesFactory { + final long streamId = RANDOM.nextInt(0, Integer.MAX_VALUE); + + @Override + public CryptoFrame create(int offset, String payload, boolean fin) { + byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + int length = bytes.length; + return new CryptoFrame(offset, length, ByteBuffer.wrap(bytes)); + } + + @Override + public int length(CryptoFrame frame) { + return frame.length(); + } + + @Override + public long offset(CryptoFrame frame) { + return frame.offset(); + } + + @Override + public String getPayload(CryptoFrame frame) { + int length = frame.length(); + byte[] bytes = new byte[length]; + frame.payload().get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public OrderedFlow flow() { + return new OrderedFlow.CryptoDataFlow(); + } + + @Override + public Class frameType() { + return CryptoFrame.class; + } + + @Override + public Comparator comparator() { + return CryptoFrame::compareOffsets; + } + } + + static TestData generateData(FramesFactory factory, int options) { + int length = LOREM.length(); + int chunks = length/20; + int offset = 0; + int remaining = length; + List frames = new ArrayList<>(); + T first = null; + T second = null; + boolean duplicates = (options & WITH_DUPS) == WITH_DUPS; + boolean overlaps = (options & WITH_OVERLAPS) == WITH_OVERLAPS; + while (remaining > 0) { + int len = remaining < 20 + ? remaining + : RANDOM.nextInt(Math.min(19, remaining - 1), Math.min(chunks, remaining)); + remaining -= len; + String data = LOREM.substring(offset, offset + len); + T frame; + if (overlaps && len > 4) { + int start = RANDOM.nextInt(0, len/4); + int end = RANDOM.nextInt(3*len/4, len); + for (int i=start; i < end; i+=2) { + frame = factory.create(offset+i, data.substring(i, i+1), + i == len-1 && remaining == 0); + frames.add(frame); + } + } + frame = factory.create(offset, data, remaining == 0); + frames.add(frame); + if (first == null) first = frame; + else if (second == null) second = frame; + if (duplicates && RANDOM.nextInt(1, 5) > 3) { + frames.add(factory.create(offset, data, remaining == 0)); + } else if (overlaps && RANDOM.nextInt(1, 5) > 3 && second != null && len > 1) { + // next frame will overlap with this one. + offset -= len / 2; remaining += len / 2; + } + offset += len; + } + if (duplicates) frames.add(first); + if (overlaps && frames.size() > 1) { + if (factory.length(first) > 0) { + frames.remove(second); + String firstpayload = factory.getPayload(first); + String secondpayload = factory.getPayload(second); + String newpayload = firstpayload.charAt(firstpayload.length() - 1) + + secondpayload; + long newoffset = factory.offset(second) - 1; + assert newoffset >= 0; + frames.add(1, factory.create((int) newoffset, newpayload, frames.size() == 2)); + } + } + return new TestData<>(factory.frameType(), factory::flow, + factory::getPayload, factory.comparator(), + List.copyOf(frames), LOREM, + duplicates, false); + } + + // Returns a new data set where all frames have been shuffled randomly. + // This should help flush bugs with buffering of frames that come out of order. + static TestData shuffle(TestData data) { + List shuffled = new ArrayList<>(data.frames()); + Collections.shuffle(shuffled, RANDOM); + return new TestData<>(data.frameType(),data.flowSupplier(), data.payloadAccessor(), + data.framesComparator(), List.copyOf(shuffled), data.expectedResult(), + data.duplicates(), true); + } + + // Returns a new data set where all frames have been sorted in reverse + // order: largest offset first. This is the worst case scenario for + // buffering. This should help checking that the amount of data buffered + // never exceeds the length of the stream, as duplicates and overlaps should + // not be buffered. + static TestData reversed(TestData data) { + List sorted = new ArrayList<>(data.frames()); + Collections.sort(sorted, data.framesComparator().reversed()); + return new TestData<>(data.frameType(),data.flowSupplier(), data.payloadAccessor(), + data.framesComparator(), List.copyOf(sorted), data.expectedResult(), + data.duplicates(), true); + } + + static List> generateData(FramesFactory factory) { + List> result = new ArrayList<>(); + TestData data = generateData(factory, 0); + TestData withdups = generateData(factory, WITH_DUPS); + TestData withoverlaps = generateData(factory, WITH_OVERLAPS); + TestData withall = generateData(factory, WITH_DUPS | WITH_OVERLAPS); + result.add(data); + result.add(withdups); + result.add(withoverlaps); + result.add(withall); + result.add(reversed(data)); + result.add(reversed(withdups)); + result.add(reversed(withoverlaps)); + result.add(reversed(withall)); + for (int i=0; i<5; i++) { + result.add(shuffle(data)); + result.add(shuffle(withdups)); + result.add(shuffle(withoverlaps)); + result.add(shuffle(withall)); + } + return List.copyOf(result); + } + + @DataProvider(name="CryptoFrame") + Object[][] generateCryptoFrames() { + return generateData(new CryptoFrameFactory()) + .stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider(name="StreamFrame") + Object[][] generateStreanFrames() { + return generateData(new StreamFrameFactory()) + .stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + private void testOrderedFlow(TestData testData, ToLongFunction offset) { + System.out.println("\n ---------------- " + + testData.frameType().getName() + + " ---------------- \n"); + System.out.println("testOrderedFlow: " + testData); + String offsets = testData.frames().stream().mapToLong(offset) + .mapToObj(Long::toString).collect(Collectors.joining(", ")); + System.out.println("offsets: " + offsets); + + // we should not have empty frames, but maybe we do? + // if we do - should we make allowance for that? + var hasEmptyFrames = testData.hasEmptyFrames(); + assertFalse(hasEmptyFrames, "generated data has empty frames"); + + var flow = testData.flowSupplier().get(); + var size = LOREM.length(); + StringBuilder result = new StringBuilder(size); + long maxBuffered = 0; + for (var f : testData.frames()) { + T received = flow.receive(f); + var buffered = flow.buffered(); + maxBuffered = Math.max(buffered, maxBuffered); + assertTrue(buffered < size, + "buffered data %s exceeds or equals payload size %s".formatted(buffered, size)); + while (received != null) { + var payload = testData.payloadAccessor.apply(received); + assertNotEquals(payload, "", "empty frames not expected: " + received); + result.append(payload); + received = flow.poll(); + } + } + assertEquals(result.toString(), testData.expectedResult); + } + + @Test(dataProvider = "CryptoFrame") + public void testCryptoFlow(TestData testData) { + testOrderedFlow(testData, CryptoFrame::offset); + } + + @Test(dataProvider = "StreamFrame") + public void testStreamFlow(TestData testData) { + testOrderedFlow(testData, StreamFrame::offset); + } + +} diff --git a/test/jdk/java/net/httpclient/quic/PacketEncodingTest.java b/test/jdk/java/net/httpclient/quic/PacketEncodingTest.java new file mode 100644 index 00000000000..495e8d41403 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacketEncodingTest.java @@ -0,0 +1,1440 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.http.quic.QuicConnectionIdFactory; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.HandshakePacket; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.LongHeaderPacket; +import jdk.internal.net.http.quic.packets.OneRttPacket; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.packets.QuicPacketNumbers; +import jdk.internal.net.http.quic.packets.RetryPacket; +import jdk.internal.net.http.quic.packets.ShortHeaderPacket; +import jdk.internal.net.http.quic.packets.VersionNegotiationPacket; +import jdk.internal.net.http.quic.packets.ZeroRttPacket; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.crypto.AEADBadTagException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static org.testng.Assert.*; + +/** + * @test + * @library /test/lib + * @summary test packet encoding and decoding in unencrypted form and without + * any network involvement. + * @run testng/othervm -Dseed=2646683818688275736 PacketEncodingTest + * @run testng/othervm -Dseed=-3723256402256409075 PacketEncodingTest + * @run testng/othervm -Dseed=-3689060484817342283 PacketEncodingTest + * @run testng/othervm -Dseed=2425718686525936108 PacketEncodingTest + * @run testng/othervm -Dseed=-2996954753243104355 PacketEncodingTest + * @run testng/othervm -Dseed=8750823652999067800 PacketEncodingTest + * @run testng/othervm -Dseed=2906555779406889127 PacketEncodingTest + * @run testng/othervm -Dseed=902801756808168822 PacketEncodingTest + * @run testng/othervm -Dseed=5643545543196691308 PacketEncodingTest + * @run testng/othervm -Dseed=2646683818688275736 PacketEncodingTest + * @run testng/othervm -Djdk.internal.httpclient.debug=true PacketEncodingTest + */ +public class PacketEncodingTest { + + @DataProvider + public Object[][] longHeaderPacketProvider() { + final QuicVersion[] quicVersions = QuicVersion.values(); + final List params = new ArrayList<>(); + for (final QuicVersion version : quicVersions) { + final var p = new Object[][] { + // quic-version, srcIdLen, dstIdLen, pn, largestAck + new Object[] {version, 20, 20, 0L, -1L}, + new Object[] {version, 10, 20, 1L, 0L}, + new Object[] {version, 10, 20, 255L, 0L}, + new Object[] {version, 12, 15, 0xFFFFL, 0L}, + new Object[] {version, 9, 8, 0x7FFFFFFFL, 255L}, + new Object[] {version, 13, 11, 0x8FFFFFFFL, 0x10000000L}, + new Object[] {version, 19, 6, 0xFFFFFFFFL, 0xFFFFFFFEL}, + new Object[] {version, 6, 17, 0xFFFFFFFFFFL, 0xFFFFFFFF00L}, + new Object[] {version, 15, 14, 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L}, + new Object[] {version, 7, 9, 0xa82f9b32L, 0xa82f30eaL}, + new Object[] {version, 18, 16, 0xace8feL, 0xabe8b3L}, + new Object[] {version, 16, 19, 0xac5c02L, 0xabe8b3L} + }; + params.addAll(Arrays.asList(p)); + } + return params.toArray(Object[][]::new); + } + + @DataProvider + public Object[][] shortHeaderPacketProvider() { + final QuicVersion[] quicVersions = QuicVersion.values(); + final List params = new ArrayList<>(); + for (final QuicVersion version : quicVersions) { + final var p = new Object[][] { + new Object[] {version, 20, 0L, -1L}, + new Object[] {version, 17, 1L, 0L}, + new Object[] {version, 10, 255L, 0L}, + new Object[] {version, 12, 0xFFFFL, 0L}, + new Object[] {version, 9, 0x7FFFFFFFL, 255L}, + new Object[] {version, 13, 0x8FFFFFFFL, 0x10000000L}, + new Object[] {version, 19, 0xFFFFFFFFL, 0xFFFFFFFEL}, + new Object[] {version, 6, 0xFFFFFFFFFFL, 0xFFFFFFFF00L}, + new Object[] {version, 15, 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L}, + new Object[] {version, 7, 0xa82f9b32L, 0xa82f30eaL}, + new Object[] {version, 18, 0xace8feL, 0xabe8b3L}, + new Object[] {version, 16, 0xac5c02L, 0xabe8b3L}, + }; + params.addAll(Arrays.asList(p)); + } + return params.toArray(Object[][]::new); + } + + @DataProvider + public Object[][] versionAndRetryProvider() { + final QuicVersion[] quicVersions = QuicVersion.values(); + final List params = new ArrayList<>(); + for (final QuicVersion version : quicVersions) { + final var p = new Object[][] { + // quic-version, srcIdLen, dstIdLen, pn, largestAck + new Object[] {version, 20, 20}, + new Object[] {version, 10, 20}, + new Object[] {version, 12, 15}, + new Object[] {version, 9, 8}, + new Object[] {version, 13, 11}, + new Object[] {version, 19, 6}, + new Object[] {version, 6, 17}, + new Object[] {version, 15, 14}, + new Object[] {version, 7, 9}, + new Object[] {version, 18, 16}, + new Object[] {version, 16, 19}, + }; + params.addAll(Arrays.asList(p)); + } + return params.toArray(Object[][]::new); + } + + private static final AtomicLong IDS = new AtomicLong(); + private static final Random RANDOM = jdk.test.lib.RandomFactory.getRandom(); + private static final int MAX_DATAGRAM_IPV6 = 65527; + + byte[] randomIdBytes(int connectionLength) { + byte[] bytes = new byte[connectionLength]; + RANDOM.nextBytes(bytes); + return bytes; + } + + private static class DummyQuicTLSEngine implements QuicTLSEngine { + @Override + public HandshakeState getHandshakeState() { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean isTLSHandshakeComplete() { + return true; + } + + @Override + public KeySpace getCurrentSendKeySpace() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean keysAvailable(KeySpace keySpace) { + return true; + } + + @Override + public void discardKeys(KeySpace keySpace) { + // no-op + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + throw new AssertionError("should not come here!"); + } + + @Override + public void restartHandshake() throws IOException { + throw new AssertionError("should not come here!"); + } + + @Override + public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) { + throw new AssertionError("should not come here!"); + } + @Override + public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { } + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return 0; + } + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) { + return ByteBuffer.allocate(5); + } + + @Override + public int getAuthTagSize() { + return 0; + } + + @Override + public void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException { + // this dummy QUIC TLS engine doesn't do any encryption. + // we just copy over the raw packet payload into the output buffer + output.put(packetPayload); + } + + @Override + public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) { + packet.position(packet.position() + headerLength); + output.put(packet); + } + + @Override + public void signRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) { + output.put(ByteBuffer.allocate(16)); + } + @Override + public void verifyRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException { + } + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) { + throw new AssertionError("should not come here!"); + } + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) { + throw new AssertionError("should not come here!"); + } + @Override + public Runnable getDelegatedTask() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryMarkHandshakeDone() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryReceiveHandshakeDone() { + throw new AssertionError("should not come here!"); + } + + @Override + public Set getSupportedQuicVersions() { + return Set.of(QuicVersion.QUIC_V1); + } + + @Override + public void setUseClientMode(boolean mode) { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean getUseClientMode() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLParameters getSSLParameters() { + throw new AssertionError("should not come here!"); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + throw new AssertionError("should not come here!"); + } + + @Override + public String getApplicationProtocol() { + return null; + } + + @Override + public SSLSession getSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLSession getHandshakeSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public void versionNegotiated(QuicVersion quicVersion) { + // no-op + } + + @Override + public void setOneRttContext(QuicOneRttContext ctx) { + // no-op + } + } + + private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine(); + private static abstract class TestCodingContext implements CodingContext { + TestCodingContext() { } + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new AssertionError("should not come here!"); + } + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new AssertionError("should not come here!"); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicConnectionId originalServerConnId() { + throw new AssertionError("should not come here!"); + } + + @Override + public QuicTLSEngine getTLSEngine() { + return TLS_ENGINE; + } + + @Override + public int minShortPacketPayloadSize(int destConnectionIdLength) { + return 100 - (destConnectionIdLength - connectionIdLength()); + } + } + + private void checkLongHeaderPacket(LongHeaderPacket packet, + PacketType packetType, + int versionNumber, + PacketNumberSpace packetNumberSpace, + long packetNumber, + QuicConnectionId srcConnectionId, + QuicConnectionId destConnectionId, + List payload, + int padding) { + List expected; + if (padding == 0) { + expected = payload; + } else if (payload.get(0) instanceof PaddingFrame pf) { + expected = new ArrayList<>(payload); + expected.set(0, new PaddingFrame(padding + pf.size())); + } else { + expected = new ArrayList<>(payload.size()+1); + expected.add(new PaddingFrame(padding)); + expected.addAll(payload); + } + checkLongHeaderPacket(packet, packetType, versionNumber, packetNumberSpace, packetNumber, + srcConnectionId, destConnectionId, expected); + + } + + private void checkLongHeaderPacket(LongHeaderPacket packet, + PacketType packetType, + int versionNumber, + PacketNumberSpace packetNumberSpace, + long packetNumber, + QuicConnectionId srcConnectionId, + QuicConnectionId destConnectionId, + List payload) { + // Check created packet + assertEquals(packet.headersType(), HeadersType.LONG); + assertEquals(packet.packetType(), packetType); + boolean hasLength = switch (packetType) { + case VERSIONS, RETRY -> false; + default -> true; + }; + assertEquals(packet.hasLength(), hasLength); + assertEquals(packet.numberSpace(), packetNumberSpace); + if (payload == null) { + assertTrue(packet.frames().isEmpty()); + } else { + assertEquals(getBuffers(packet.frames()), getBuffers(payload)); + } + assertEquals(packet.version(), versionNumber); + assertEquals(packet.packetNumber(), packetNumber); + assertEquals(packet.sourceId(), srcConnectionId); + assertEquals(packet.destinationId(), destConnectionId); + + } + + private static ByteBuffer encodeFrame(QuicFrame frame) { + ByteBuffer result = ByteBuffer.allocate(frame.size()); + frame.encode(result); + return result; + } + private static List getBuffers(List payload) { + return payload.stream().map(PacketEncodingTest::encodeFrame).toList(); + } + private static List getBuffers(List payload, int minSize) { + int payloadSize = payload.stream().mapToInt(QuicFrame::size).sum(); + if (payloadSize < minSize) { + payload = new ArrayList<>(payload); + payload.add(0, new PaddingFrame(minSize - payloadSize)); + } + return payload.stream().map(PacketEncodingTest::encodeFrame).toList(); + } + + private static String toHex(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, buffer.remaining()); + return HexFormat.of().formatHex(bytes); + } + + private static String toHex(List byteBuffers) { + return "0x" + byteBuffers.stream() + .map(PacketEncodingTest::toHex) + .collect(Collectors.joining(":")); + } + + private void checkLongHeaderPacketAt(ByteBuffer datagram, int offset, + PacketType packetType, int versionNumber, + QuicConnectionId srcConnectionId, + QuicConnectionId destConnectionId) { + assertEquals(QuicPacketDecoder.peekHeaderType(datagram, offset), HeadersType.LONG); + assertEquals(QuicPacketDecoder.of(datagram, offset).peekPacketType(datagram, offset), packetType); + LongHeader header = QuicPacketDecoder.peekLongHeader(datagram, offset); + assertNotNull(header, "Could not parse packet header"); + assertEquals(header.version(), versionNumber); + assertTrue(header.destinationId() + .matches(destConnectionId.asReadOnlyBuffer()), "Destination ID doesn't match"); + assertTrue(header.sourceId() + .matches(srcConnectionId.asReadOnlyBuffer()), "Source ID doesn't match"); + } + + private List frames(byte[] payload) throws IOException { + return frames(payload, false); + } + + private List frames(byte[] payload, boolean insert) throws IOException { + int payloadSize = payload.length; + ByteBuffer buf = ByteBuffer.wrap(payload); + List frames = new ArrayList<>(); + int remaining = payloadSize; + while (remaining > 7) { + int size = RANDOM.nextInt(1, remaining - 6); + byte[] data = new byte[size]; + RANDOM.nextBytes(data); + QuicFrame frame = new CryptoFrame(0, size, ByteBuffer.wrap(data)); + int encoded = frame.size(); + assertTrue(encoded > 0, String.valueOf(encoded)); + assertTrue(encoded <= remaining, String.valueOf(encoded)); + if (insert) { + frames.add(0, frame); + buf.position(remaining - encoded); + } else { + frames.add(frame); + } + frame.encode(buf); + remaining -= encoded; + } + if (remaining > 0) { + var padding = new PaddingFrame(remaining); + if (insert) { + frames.add(0, padding); + buf.position(0); + } else { + frames.add(padding); + } + padding.encode(buf); + } + if (insert) { + assertEquals(buf.position(), remaining); + assertEquals(buf.remaining(), payloadSize - remaining); + } else { + assertEquals(buf.remaining(), 0); + } + return List.copyOf(frames); + } + + private ByteBuffer toByteBuffer(QuicPacketEncoder encoder, QuicPacket outgoingQuicPacket, CodingContext context) + throws Exception { + int size = outgoingQuicPacket.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + encoder.encode(outgoingQuicPacket, buffer, context); + assertEquals(buffer.position(), size, " for " + outgoingQuicPacket); + buffer.flip(); + return buffer; + } + + private void checkShortHeaderPacket(ShortHeaderPacket packet, + PacketType packetType, + PacketNumberSpace packetNumberSpace, + long packetNumber, + QuicConnectionId destConnectionId, + List payload, + int minSize) { + // Check created packet + assertEquals(packet.headersType(), HeadersType.SHORT); + assertEquals(packet.packetType(), packetType); + assertEquals(packet.hasLength(), false); + assertEquals(packet.numberSpace(), packetNumberSpace); + assertEquals(getBuffers(packet.frames()), getBuffers(payload, minSize)); + assertEquals(packet.packetNumber(), packetNumber); + assertEquals(packet.destinationId(), destConnectionId); + } + + private void checkShortHeaderPacketAt(ByteBuffer datagram, int offset, + PacketType packetType, + QuicConnectionId destConnectionId, + CodingContext context) { + assertEquals(QuicPacketDecoder.peekHeaderType(datagram, offset), HeadersType.SHORT); + assertEquals(QuicPacketDecoder.of(QuicVersion.QUIC_V1).peekPacketType(datagram, offset), packetType); + assertEquals(QuicPacketDecoder.peekVersion(datagram, offset), 0); + int pos = datagram.position(); + if (pos != offset) datagram.position(offset); + try { + assertEquals(QuicPacketDecoder.peekShortConnectionId(datagram, destConnectionId.length()) + .mismatch(destConnectionId.asReadOnlyBuffer()), -1); + } finally { + if (pos != offset) datagram.position(pos); + } + } + + @Test(dataProvider = "longHeaderPacketProvider") + public void testInitialPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestInitialPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + final QuicConnectionId destConnectionId = new PeerConnectionId(destid); + assertEquals(destid.length, destConnectionId.length(), "dcid length"); + destIdLength = destid.length; + + byte[] srcid = randomIdBytes(srcIdLength); + final QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + assertEquals(srcid.length, srcConnectionId.length(), "scid length"); + + int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + // ensure that bound - tokenLength - 1 > 0 + assert bound > 4; + int tokenLength = RANDOM.nextInt(bound - 4); + + byte[] token = tokenLength == 0 ? null : new byte[tokenLength]; + if (token != null) RANDOM.nextBytes(token); + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - tokenLength - 1) + 1, 4 - packetNumberLength); + System.out.printf("testInitialPacket.encode(scid:%s, dcid:%s, token:%d, payload:%d)%n", + srcIdLength, destIdLength, tokenLength, payloadSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.INITIAL ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.INITIAL ? largestAcked : -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + int minsize = encoder.computeMaxInitialPayloadSize(context, + computePacketNumberLength(packetNumber, + context.largestAckedPN(PacketNumberSpace.INITIAL)), + tokenLength, srcIdLength, + destIdLength, 1200); + int padding = (payloadSize < minsize) ? minsize - payloadSize : 0; + System.out.println("testInitialPacket: available=%s, payload=%s, padding=%s" + .formatted(minsize, payloadSize, padding)); + + + byte[] payload = new byte[payloadSize]; + List frames = frames(payload, padding != 0); + assertEquals(frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact), payloadSize); + + + // Create an initial packet + var packet = encoder.newInitialPacket(srcConnectionId, + destConnectionId, + token, + packetNumber, + largestAcked, + frames, + context); + + if (padding > 0) { + var frames2 = new ArrayList<>(frames); + frames2.add(0, new PaddingFrame(padding)); + var packet2 = encoder.newInitialPacket(srcConnectionId, + destConnectionId, + token, + packetNumber, + largestAcked, + frames2, + context); + assertEquals(padding, padding + (1200 - packet2.size())); + } + + // Check created packet + assertTrue(packet instanceof InitialPacket); + var initialPacket = (InitialPacket) packet; + System.out.printf("%s: pn:%s, tklen:%s, payloadSize:%s, padding:%s, packet::size:%s, " + + "\n\tinputFrames: %s, " + + "\n\tencodedFrames:%s%n", + PacketType.INITIAL, packetNumber, tokenLength, payload.length, padding, + packet.size(), frames, packet.frames()); + checkLongHeaderPacket(initialPacket, PacketType.INITIAL, quicVersion.versionNumber(), + PacketNumberSpace.INITIAL, packetNumber, + srcConnectionId, destConnectionId, frames, padding); + assertEquals(initialPacket.tokenLength(), tokenLength); + assertEquals(initialPacket.token(), token); + assertEquals(initialPacket.hasLength(), true); + assertEquals(initialPacket.length(), packetNumberLength + payloadSize + padding); + + // Check that peeking at the encoded packet returns correct information + + // Decode the two packets in the datagram + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.INITIAL, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // coalesce two packets in a single datagram and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int second = offset + encoded.limit(); + System.out.printf("testInitialPacket.encode(offset:%d, second:%d)%n", + offset, second); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n", + offset, second, datagram.position(), datagram.limit()); + System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.INITIAL, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n", + second, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, second, PacketType.INITIAL, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip both packets + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), second); + decoder.skipPacket(datagram, second); + assertEquals(datagram.remaining(), offset); + + datagram.position(offset); + int size = second - offset; + for (int i=0; i<2; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof InitialPacket, "decoded: " + decodedPacket); + InitialPacket initialDecoded = InitialPacket.class.cast(decodedPacket); + checkLongHeaderPacket(initialDecoded, PacketType.INITIAL, quicVersion.versionNumber(), + PacketNumberSpace.INITIAL, packetNumber, + srcConnectionId, destConnectionId, frames, padding); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(initialDecoded.tokenLength(), tokenLength); + assertEquals(initialDecoded.token(), token); + assertEquals(initialDecoded.length(), initialPacket.length()); + assertEquals(initialDecoded.length(), packetNumberLength + payloadSize + padding); + } + assertEquals(datagram.position(), second + second - offset); + } + + @Test(dataProvider = "longHeaderPacketProvider") + public void testHandshakePacket(QuicVersion quicVersion, int srcIdLength, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestHandshakePacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength); + byte[] payload = new byte[payloadSize]; + var frames = frames(payload); + System.out.printf("testHandshakePacket.encode(payload:%d)%n", payloadSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.HANDSHAKE ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.HANDSHAKE ? largestAcked : -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + // Create an initial packet + var packet = encoder.newHandshakePacket(srcConnectionId, + destConnectionId, + packetNumber, + largestAcked, + frames, + context); + + // Check created packet + assertTrue(packet instanceof HandshakePacket); + var handshakePacket = (HandshakePacket) packet; + checkLongHeaderPacket(handshakePacket, PacketType.HANDSHAKE, quicVersion.versionNumber(), + PacketNumberSpace.HANDSHAKE, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(handshakePacket.hasLength(), true); + assertEquals(handshakePacket.length(), packetNumberLength + payloadSize); + + // Decode the two packets in the datagram + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.HANDSHAKE, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // coalesce two packets in a single datagram and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int second = offset + encoded.limit(); + System.out.printf("testHandshakePacket.encode(offset:%d, second:%d)%n", + offset, second); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n", + offset, second, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.HANDSHAKE, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n", + second, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, second, PacketType.HANDSHAKE, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip both packets + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), second); + decoder.skipPacket(datagram, second); + assertEquals(datagram.remaining(), offset); + + datagram.position(offset); + int size = second - offset; + for (int i=0; i<2; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof HandshakePacket, "decoded: " + decodedPacket); + HandshakePacket handshakeDecoded = HandshakePacket.class.cast(decodedPacket); + checkLongHeaderPacket(handshakeDecoded, PacketType.HANDSHAKE, quicVersion.versionNumber(), + PacketNumberSpace.HANDSHAKE, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(handshakeDecoded.length(), handshakePacket.length()); + assertEquals(handshakeDecoded.length(), packetNumberLength + payloadSize); + } + assertEquals(datagram.position(), second + second - offset); + } + + @Test(dataProvider = "longHeaderPacketProvider") + public void testZeroRTTPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestZeroRTTPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength); + byte[] payload = new byte[payloadSize]; + var frames = frames(payload); + System.out.printf("testZeroRTTPacket.encode(payload:%d)%n", payloadSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + // Create an initial packet + var packet = encoder.newZeroRttPacket(srcConnectionId, + destConnectionId, + packetNumber, + largestAcked, + frames, + context); + + // Check created packet + assertTrue(packet instanceof ZeroRttPacket); + var zeroRttPacket = (ZeroRttPacket) packet; + checkLongHeaderPacket(zeroRttPacket, PacketType.ZERORTT, quicVersion.versionNumber(), + PacketNumberSpace.APPLICATION, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(zeroRttPacket.hasLength(), true); + assertEquals(zeroRttPacket.length(), packetNumberLength + payloadSize); + + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.ZERORTT, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // coalesce two packets in a single datagram and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int second = offset + encoded.limit(); + System.out.printf("testZeroRTTPacket.encode(offset:%d, second:%d)%n", + offset, second); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n", + offset, second, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.ZERORTT, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n", + second, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, second, PacketType.ZERORTT, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip both packets + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), second); + decoder.skipPacket(datagram, second); + assertEquals(datagram.remaining(), offset); + + // Decode the two packets in the datagram + datagram.position(offset); + int size = second - offset; + for (int i=0; i<2; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof ZeroRttPacket, "decoded: " + decodedPacket); + ZeroRttPacket zeroRttDecoded = ZeroRttPacket.class.cast(decodedPacket); + checkLongHeaderPacket(zeroRttDecoded, PacketType.ZERORTT, quicVersion.versionNumber(), + PacketNumberSpace.APPLICATION, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(zeroRttDecoded.length(), zeroRttPacket.length()); + assertEquals(zeroRttDecoded.length(), packetNumberLength + payloadSize); + } + assertEquals(datagram.position(), second + second - offset); + } + + @Test(dataProvider = "versionAndRetryProvider") + public void testVersionNegotiationPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength) + throws Exception { + System.out.printf("%ntestVersionNegotiationPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, -1, -1); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + + final List versionList = new ArrayList<>(); + for (final QuicVersion qv : QuicVersion.values()) { + versionList.add(qv.versionNumber()); + } + System.out.printf("testVersionNegotiationPacket.encode(versions:%d)%n", versionList.size()); + + // Create an initial packet + var packet = QuicPacketEncoder.newVersionNegotiationPacket(srcConnectionId, + destConnectionId, + versionList.stream().mapToInt(Integer::intValue).toArray()); + + // Check created packet + assertTrue(packet instanceof VersionNegotiationPacket); + var versionPacket = (VersionNegotiationPacket) packet; + checkLongHeaderPacket(versionPacket, PacketType.VERSIONS, 0, + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(versionPacket.hasLength(), false); + assertEquals(versionPacket.supportedVersions(), + versionList.stream().mapToInt(Integer::intValue).toArray()); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.VERSIONS, 0, + srcConnectionId, destConnectionId); + + // version negotiation packets can't be coalesced + int offset = RANDOM.nextInt(256); + int end = offset + encoded.limit(); + System.out.printf("testVersionNegotiationPacket.encode(offset:%d, end:%d)%n", + offset, end); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.VERSIONS, 0, + srcConnectionId, destConnectionId); + + // check that skip packet can skip packet + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), end); + assertEquals(datagram.remaining(), 0); + + // Decode the two packets in the datagram + datagram.position(offset); + int size = end - offset; + for (int i=0; i<1; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof VersionNegotiationPacket, "decoded: " + decodedPacket); + VersionNegotiationPacket decodedVersion = VersionNegotiationPacket.class.cast(decodedPacket); + checkLongHeaderPacket(decodedVersion, PacketType.VERSIONS, 0, + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(decodedVersion.supportedVersions(), + versionList.stream().mapToInt(Integer::intValue).toArray()); + } + assertEquals(datagram.position(), end); + } + + @Test(dataProvider = "versionAndRetryProvider") + public void testRetryPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength) + throws Exception { + System.out.printf("%ntestRetryPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, -1, -1); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + byte[] origId = randomIdBytes(destIdLength); + QuicConnectionId origConnectionId = new PeerConnectionId(origId); + int bound = (MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7); + + int retryTokenLength = RANDOM.nextInt(bound - 16) + 1; + byte[] retryToken = new byte[retryTokenLength]; + RANDOM.nextBytes(retryToken); + System.out.printf("testRetryPacket.encode(token:%d)%n", retryTokenLength); + int expectedSize = 7 + 16 + destid.length + srcIdLength + retryTokenLength; + + // Create an initial packet + var packet = encoder.newRetryPacket(srcConnectionId, + destConnectionId, + retryToken); + + // Check created packet + assertTrue(packet instanceof RetryPacket); + var retryPacket = (RetryPacket) packet; + checkLongHeaderPacket(retryPacket, PacketType.RETRY, quicVersion.versionNumber(), + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(retryPacket.hasLength(), false); + assertEquals(retryPacket.retryToken(), retryToken); + assertEquals(retryPacket.size(), expectedSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + @Override public QuicConnectionId originalServerConnId() { return origConnectionId; } + }; + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.RETRY, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // version negotiation packets can't be coalesced + int offset = RANDOM.nextInt(256); + int end = offset + encoded.limit(); + System.out.printf("testRetryPacket.encode(offset:%d, end:%d)%n", + offset, end); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.RETRY, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip packet + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), end); + assertEquals(datagram.remaining(), 0); + + // Decode the two packets in the datagram + datagram.position(offset); + int size = end - offset; + for (int i=0; i<1; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof RetryPacket, "decoded: " + decodedPacket); + RetryPacket decodedRetry = RetryPacket.class.cast(decodedPacket); + checkLongHeaderPacket(decodedRetry, PacketType.RETRY, quicVersion.versionNumber(), + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(decodedPacket.size(), expectedSize); + assertEquals(decodedRetry.retryToken(), retryToken); + } + assertEquals(datagram.position(), end); + } + + @Test(dataProvider = "shortHeaderPacketProvider") + public void testOneRTTPacket(QuicVersion quicVersion, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestOneRTTPacket(qv:%s, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + int bound = MAX_DATAGRAM_IPV6 - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength); + byte[] payload = new byte[payloadSize]; + var frames = frames(payload); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + // since we're going to decode the short packet, we need to return + // the same length that was used as destination cid in the packet + @Override public int connectionIdLength() { + return destid.length; + } + }; + + int paddedPayLoadSize = Math.max(payloadSize + packetNumberLength, context.minShortPacketPayloadSize(destid.length)); + System.out.printf("testOneRTTPacket.encode(payload:%d, padded:%d, destid.length: %d)%n", + payloadSize, paddedPayLoadSize, destid.length); + int expectedSize = 1 + destid.length + paddedPayLoadSize; + // Create an 1-RTT packet + OneRttPacket packet = encoder.newOneRttPacket(destConnectionId, + packetNumber, + largestAcked, + frames, + context); + + int minPayloadSize = context.minShortPacketPayloadSize(destConnectionId.length()) - packetNumberLength; + checkShortHeaderPacket(packet, PacketType.ONERTT, + PacketNumberSpace.APPLICATION, packetNumber, + destConnectionId, frames, minPayloadSize); + assertEquals(packet.hasLength(), false); + assertEquals(packet.size(), expectedSize); + + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkShortHeaderPacketAt(encoded, 0, PacketType.ONERTT, + destConnectionId, context); + + // write packet at an offset in the datagram to simulate + // short packet coalesced after long packet and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int end = offset + encoded.limit(); + System.out.printf("testOneRTTPacket.encode(offset:%d, end:%d)%n", + offset, end); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + assert datagram.limit() == offset + encoded.remaining(); + + // set position to first packet to check connection ids + System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkShortHeaderPacketAt(datagram, offset, PacketType.ONERTT, + destConnectionId, context); + + // check that skip packet can skip packet at offset + datagram.position(0); + datagram.limit(end); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), offset + expectedSize); + assertEquals(datagram.position(), datagram.limit()); + assertEquals(datagram.position(), datagram.capacity() - offset); + + + // Decode the packet in the datagram + datagram.position(offset); + int size = expectedSize; + for (int i=0; i<1; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof OneRttPacket, "decoded: " + decodedPacket); + OneRttPacket oneRttDecoded = OneRttPacket.class.cast(decodedPacket); + List expectedFrames = frames; + if (frames.size() > 0 && frames.get(0) instanceof PaddingFrame) { + // The first frame should be a crypto frame, except if payloadSize + // was less than 7. + int frameSizes = frames.stream().mapToInt(QuicFrame::size).sum(); + assert frameSizes == payloadSize; + assert frameSizes <= 7; + // decoder will coalesce padding frames. So instead of finding + // two padding frames in the decoded packet we will find just one. + // To make the check pass, we should expect a bigger padding frame. + if (minPayloadSize > frameSizes) { + // replace the first frame with a bigger padding frame + expectedFrames = new ArrayList<>(frames); + var first = frames.get(0); + // replace the first frame with a bigger padding frame that + // coalesce the first padding payload frame with the padding that + // should have been added by the encoder. + // We will then be able to check that the decoded packet contains + // that single bigger padding frame. + expectedFrames.set(0, new PaddingFrame(minPayloadSize - frameSizes + first.size())); + } + } + checkShortHeaderPacket(oneRttDecoded, PacketType.ONERTT, + PacketNumberSpace.APPLICATION, packetNumber, + destConnectionId, expectedFrames, minPayloadSize); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + } + assertEquals(datagram.position(), offset + size); + assertEquals(datagram.remaining(), 0); + assertEquals(datagram.limit(), end); + + } + + @Test + public void testNoMismatch() { + List match1 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20}) + ); + List match2 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + List match3 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + assertEquals(Utils.mismatch(match1, match1), -1); + assertEquals(Utils.mismatch(match2, match2), -1); + assertEquals(Utils.mismatch(match3, match3), -1); + assertEquals(Utils.mismatch(match1, match2), -1); + assertEquals(Utils.mismatch(match2, match1), -1); + assertEquals(Utils.mismatch(match1, match3), -1); + assertEquals(Utils.mismatch(match3, match1), -1); + assertEquals(Utils.mismatch(match2, match3), -1); + assertEquals(Utils.mismatch(match3, match2), -1); + } + + @Test + public void testMismatch() { + // match1, match2, match3 match with each others + List match1 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20}) + ); + List match2 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + List match3 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + // nomatch0, nomatch10, nomatch19 differ from the previous + // list at some index in [0..20[ + // nomatch0 mismatches at index 0 + List nomatch0 = List.of( + ByteBuffer.wrap(new byte[] {21, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20}) + ); + // nomatch10 mismatches at index 10 + List nomatch10 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 31}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + // nomatch19 mismatches at index 19 + List nomatch19 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 40}) + ); + // morematch1 has one more byte at the end + List morematch1 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20, 41}) + ); + // morematch2 and morematch3 have the same 3 additional + // bytes at the end + List morematch2 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}), + ByteBuffer.wrap(new byte[] {41, 42, 43}) + ); + List morematch3 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20, 41, 42, 43}) + ); + + assertEquals(Utils.mismatch(nomatch0, nomatch0), -1L); + assertEquals(Utils.mismatch(nomatch10, nomatch10), -1L); + assertEquals(Utils.mismatch(nomatch19, nomatch19), -1L); + assertEquals(Utils.mismatch(morematch1, morematch1), -1L); + assertEquals(Utils.mismatch(morematch2, morematch2), -1L); + assertEquals(Utils.mismatch(morematch3, morematch3), -1L); + assertEquals(Utils.mismatch(morematch2, morematch3), -1L); + assertEquals(Utils.mismatch(morematch3, morematch2), -1L); + + for (var match : List.of(match1, match2, match3)) { + assertEquals(Utils.mismatch(match, nomatch0), 0L); + assertEquals(Utils.mismatch(match, nomatch10), 10L); + assertEquals(Utils.mismatch(match, nomatch19), 19L); + assertEquals(Utils.mismatch(nomatch0, match), 0L); + assertEquals(Utils.mismatch(nomatch10, match), 10L); + assertEquals(Utils.mismatch(nomatch19, match), 19L); + for (var morematch : List.of(morematch1, morematch2, morematch3)) { + assertEquals(Utils.mismatch(match, morematch), 20L); + assertEquals(Utils.mismatch(morematch, match), 20L); + } + + } + } + +} diff --git a/test/jdk/java/net/httpclient/quic/PacketLossTest.java b/test/jdk/java/net/httpclient/quic/PacketLossTest.java new file mode 100644 index 00000000000..1298e0977de --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacketLossTest.java @@ -0,0 +1,253 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.DatagramDeliveryPolicy; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/* + * @test + * @summary Verifies QUIC client interaction against servers which exhibit packet loss + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.httpclient.test.lib.common.TestUtil + * jdk.test.lib.net.SimpleSSLContext + * @run junit/othervm/timeout=240 -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=10 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.httpclient.HttpClient.log=quic,errors PacketLossTest + */ +public class PacketLossTest { + + private static SSLContext sslContext; + private static ExecutorService executor; + + private static final byte[] HELLO_MSG = "Hello Quic".getBytes(StandardCharsets.UTF_8); + + @BeforeAll + public static void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + } + + @AfterAll + public static void afterAll() throws Exception { + if (executor != null) { + executor.close(); + } + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + sealed interface DropPolicy { + final record DropRandomly() implements DropPolicy {} + final record DropEveryNth(int n) implements DropPolicy {} + final record DropNone() implements DropPolicy {} + final record DropAll() implements DropPolicy {} + default DatagramDeliveryPolicy policy() { + if (this instanceof DropRandomly) { + return DatagramDeliveryPolicy.dropRandomly(); + } else if (this instanceof DropEveryNth en) { + return DatagramDeliveryPolicy.dropEveryNth(en.n()); + } else if (this instanceof DropNone) { + return DatagramDeliveryPolicy.alwaysDeliver(); + } else if (this instanceof DropAll) { + return DatagramDeliveryPolicy.neverDeliver(); + } + throw new IllegalStateException("Unknown policy: " + this); + } + } + + record DropServer(String name, QuicStandaloneServer server) implements AutoCloseable { + @Override + public void close() throws IOException { + server.close(); + } + public InetSocketAddress getAddress() { + return server.getAddress(); + } + } + + DropServer of(QuicServer.Builder builder, DropPolicy incomimg, DropPolicy outgoing) + throws IOException { + QuicStandaloneServer server = builder + .incomingDeliveryPolicy(incomimg.policy()) + .outgoingDeliveryPolicy(outgoing.policy()) + .build(); + String name = "DropServer(%s, in: %s, out: %s)".formatted(server.name(), incomimg, outgoing); + return new DropServer(name, server); + } + + DropPolicy dropEveryNth(int n) { + return new DropPolicy.DropEveryNth(n); + } + DropPolicy dropRandomly() { + return new DropPolicy.DropRandomly(); + } + + // returns a List of unstarted Quic servers configured with different incoming/outgoing + // datagram delivery policies + private List unstartedServers() throws Exception { + final QuicServer.Builder builder = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext); + final List servers = new ArrayList<>(); + servers.add(of(builder, dropEveryNth(3), dropEveryNth(7))); + servers.add(of(builder, dropRandomly(), dropRandomly())); + servers.add(of(builder, dropEveryNth(5), dropRandomly())); + return servers; + } + + private static void startServer(final QuicStandaloneServer server) throws IOException { + // add a handler which deals with incoming connections + server.addHandler(new EchoHandler(HELLO_MSG.length)); + server.start(); + System.out.println("Server " + server.name() + " started at " + server.getAddress()); + } + + /** + * Uses {@link QuicClient} to pass data and expect back the data to/from Quic servers which + * might drop incoming/outgoing packets. + */ + @Test + public void testDataTransfer() throws Exception { + for (final DropServer server : unstartedServers()) { + startServer(server.server()); + try (server) { + System.out.printf("%n%n===== %s =====%n%n", server.name()); + System.err.printf("%n%n===== %s =====%n%n", server.name()); + final int numTimes = 20; + try (final QuicClient client = createClient()) { + final InetSocketAddress serverAddr = server.getAddress(); + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, serverAddr); + for (int i = 1; i <= numTimes; i++) { + System.out.println("iteration " + i + " against server: " + server.name() + + ", server addr: " + serverAddr); + // open a bidi stream + final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + // write data on the stream + try (final OutputStream os = bidiStream.outputStream()) { + os.write(HELLO_MSG); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for response + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("client: reading from bidi stream's input stream"); + final byte[] data = is.readAllBytes(); + System.out.println("client: Received response of size " + data.length); + final String response = new String(data, StandardCharsets.UTF_8); + // verify response + System.out.println("client: Response: " + response); + if (!Arrays.equals(response.getBytes(StandardCharsets.UTF_8), HELLO_MSG)) { + throw new AssertionError("Unexpected response: " + response); + } + } finally { + System.err.println("client: Closing bidi stream from test"); + bidiStream.close(); + } + } + } + } + } + } + + /** + * Reads data from incoming client initiated bidirectional stream of a Quic connection + * and writes back a response which is same as the read data + */ + private static final class EchoHandler implements QuicServerHandler { + + private final int numBytesToRead; + + private EchoHandler(final int numBytesToRead) { + this.numBytesToRead = numBytesToRead; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + final byte[] data; + // read the request content + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("Handler reading data from bidi stream's inputstream " + is); + data = is.readAllBytes(); + System.out.println("Handler read " + data.length + " bytes of data"); + } + if (data.length != numBytesToRead) { + throw new IOException("Expected to read " + numBytesToRead + + " bytes but read only " + data.length + " bytes"); + } + // write response + try (final OutputStream os = bidiStream.outputStream()) { + System.out.println("Handler writing data to bidi stream's outputstream " + os); + os.write(data); + } + System.out.println("Handler invocation complete"); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/PacketNumbersTest.java b/test/jdk/java/net/httpclient/quic/PacketNumbersTest.java new file mode 100644 index 00000000000..3a4237d0847 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacketNumbersTest.java @@ -0,0 +1,159 @@ +/* + * 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. + * + * 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. + */ + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import jdk.internal.net.http.quic.packets.QuicPacketNumbers; +import org.testng.SkipException; +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.expectThrows; + +/** + * @test + * @run testng PacketNumbersTest + */ +public class PacketNumbersTest { + + record EncodeResult(int expected, boolean assertion, Class failure) { + public static EncodeResult illegal() { + return new EncodeResult(-1, true, IllegalArgumentException.class); + } + public static EncodeResult asserting() { + return new EncodeResult(-1, true, AssertionError.class); + } + public static EncodeResult success(int result) { + return new EncodeResult(result, false, null); + } + public static EncodeResult fail(Class failure) { + return new EncodeResult(-1, false, failure); + } + public boolean fail() { + return failure() != null; + } + + @Override + public String toString() { + return fail() ? failure.getSimpleName() + : String.valueOf(expected); + } + } + record TestCase(String desc, long fullPN, long largestAck, EncodeResult result) { + static AtomicInteger count = new AtomicInteger(); + TestCase { + desc = count.incrementAndGet() + " - expecting " + desc; + } + public byte[] encode() { + return QuicPacketNumbers.encodePacketNumber(fullPN(), largestAck()); + } + + public long decode() { + byte[] encoded = encode(); + var largestProcessed = largestAck(); + return QuicPacketNumbers.decodePacketNumber(largestProcessed, + ByteBuffer.wrap(encoded), encoded.length); + } + + @Override + public String toString() { + return "%s: (%d, %d) -> %s".formatted(desc, fullPN, largestAck, result); + } + } + + @DataProvider + public Object[][] encode() { + return List.of( + // these first three test cases are extracted from RFC 9000, appendix A.2 and A.3 + new TestCase("success", 0xa82f9b32L, 0xa82f30eaL, EncodeResult.success(0x9b32)), + new TestCase("success", 0xace8feL, 0xabe8b3L, EncodeResult.success(0xace8fe & 0xFFFFFF)), + new TestCase("success", 0xac5c02L, 0xabe8b3L, EncodeResult.success(0xac5c02 & 0xFFFF)), + // additional test cases - these have been obtained empirically to test at the limits + new TestCase("success", 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L, EncodeResult.success(0x0000FFFF)), + new TestCase("success", 0xFFFFFFFFFFL, 0xFFFFFFFF00L, EncodeResult.success(0x0000FFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xFFFFFFFEL, EncodeResult.success(0x000000FF)), + new TestCase("success", 0xFFFFFFFFL, 0xFFFFFF00L, EncodeResult.success(0x0000FFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xFFFF0000L, EncodeResult.success(0x00FFFFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xFF000000L, EncodeResult.success(0xFFFFFFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xF0000000L, EncodeResult.success(0xFFFFFFFF)), + new TestCase("success", 0xFFFFFFFFL, 0x80000000L, EncodeResult.success(0xFFFFFFFF)), + new TestCase("illegal(5)",0xFFFFFFFFL, 0x7FFFFFFFL, EncodeResult.illegal()), + new TestCase("success", 0x8FFFFFFFL, 0x10000000L, EncodeResult.success(0x8FFFFFFF)), + new TestCase("illegal(5)",0x8FFFFFFFL, 0x0FFFFFFFL, EncodeResult.illegal()), + new TestCase("illegal(5)",0x8FFFFFFFL, 256L, EncodeResult.illegal()), + new TestCase("success", 0x7FFFFFFFL, 255L, EncodeResult.success(0x7FFFFFFF)), + new TestCase("success", 0x7FFFFFFFL, 0L, EncodeResult.success(0x7FFFFFFF)), + new TestCase("illegal(5)",0x7FFFFFFFL, -1L, EncodeResult.illegal()), + new TestCase("success", 0x6FFFFFFFL, 0L, EncodeResult.success(0x6FFFFFFF)), + new TestCase("success", 0xFFFFFFL, 0L, EncodeResult.success(0xFFFFFF)), + new TestCase("success", 0xFFFFL, 0L, EncodeResult.success(0xFFFF)), + new TestCase("success", 255L, 0L, EncodeResult.success(255)), + new TestCase("success", 1L, 0L, EncodeResult.success(1)), + new TestCase("success", 0x6FFFFFFFL, -1L, EncodeResult.success(0x6FFFFFFF)), + new TestCase("success", 0xFFFFFFL, -1L, EncodeResult.success(0xFFFFFF)), + new TestCase("success", 0xFFFFL, -1L, EncodeResult.success(0xFFFF)), + new TestCase("success", 255L, -1L, EncodeResult.success(255)), + new TestCase("success", 1L, -1L, EncodeResult.success(1)), + new TestCase("success", 0L, -1L, EncodeResult.success(0)), + new TestCase("assert", 0L, 1L, EncodeResult.asserting()), + new TestCase("assert", 0L, 0L, EncodeResult.asserting()), + new TestCase("assert", 1L, 1L, EncodeResult.asserting()) + ).stream().map(Stream::of) + .map(Stream::toArray) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "encode") + public void testEncodePacketNumber(TestCase test) { + System.out.println(test); + if (test.result().assertion()) { + if (!QuicPacketNumbers.class.desiredAssertionStatus()) { + throw new SkipException("needs assertion enabled (-esa)"); + } + Throwable t = expectThrows(test.result().failure(), test::encode); + System.out.println("Got expected assertion: " + t); + return; + } + if (test.result().fail()) { + Throwable t = expectThrows(test.result().failure(), test::encode); + System.out.println("Got expected exception: " + t); + return; + + } + byte[] res = test.encode(); + int truncated = 0; + for (int i=0; i initial; + case HANDSHAKE -> handshake; + case APPLICATION -> app; + case NONE -> throw new AssertionError("invalid number space: " + packetNumberSpace); + }; + if (result == null) { + throw new AssertionError("invalid number space: " + packetNumberSpace); + } + return result; + } + } + + private static class DummyQuicTLSEngine implements QuicTLSEngine { + @Override + public HandshakeState getHandshakeState() { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean isTLSHandshakeComplete() { + return true; + } + + @Override + public KeySpace getCurrentSendKeySpace() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean keysAvailable(KeySpace keySpace) { + return true; + } + + @Override + public void discardKeys(KeySpace keySpace) { + // no-op + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + throw new AssertionError("should not come here!"); + } + + @Override + public void restartHandshake() throws IOException { + throw new AssertionError("should not come here!"); + } + + @Override + public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) { + throw new AssertionError("should not come here!"); + } + @Override + public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { } + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return 0; + } + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) { + return ByteBuffer.allocate(5); + } + + @Override + public int getAuthTagSize() { + return 0; + } + + @Override + public void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException { + // this dummy QUIC TLS engine doesn't do any encryption. + // we just copy over the raw packet payload into the output buffer + output.put(packetPayload); + } + + @Override + public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) { + packet.position(packet.position() + headerLength); + output.put(packet); + } + + @Override + public void signRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) { + throw new AssertionError("should not come here!"); + } + @Override + public void verifyRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException { + throw new AssertionError("should not come here!"); + } + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) { + throw new AssertionError("should not come here!"); + } + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) { + throw new AssertionError("should not come here!"); + } + @Override + public Runnable getDelegatedTask() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryMarkHandshakeDone() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryReceiveHandshakeDone() { + throw new AssertionError("should not come here!"); + } + + @Override + public Set getSupportedQuicVersions() { + return Set.of(QuicVersion.QUIC_V1); + } + + @Override + public void setUseClientMode(boolean mode) { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean getUseClientMode() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLParameters getSSLParameters() { + throw new AssertionError("should not come here!"); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + throw new AssertionError("should not come here!"); + } + + @Override + public String getApplicationProtocol() { + return null; + } + + @Override + public SSLSession getSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLSession getHandshakeSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public void versionNegotiated(QuicVersion quicVersion) { + // no-op + } + + @Override + public void setOneRttContext(QuicOneRttContext ctx) { + // no-op + } + } + + private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine(); + private static class TestCodingContext implements CodingContext { + final QuicPacketEncoder encoder; + final QuicPacketDecoder decoder; + final PacketSpaces spaces; + TestCodingContext(PacketSpaces spaces) { + this.spaces = spaces; + this.encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1); + this.decoder = QuicPacketDecoder.of(QuicVersion.QUIC_V1); + } + + @Override + public long largestProcessedPN(PacketNumberSpace packetNumberSpace) { + return spaces.get(packetNumberSpace).getLargestProcessedPN(); + } + + @Override + public long largestAckedPN(PacketNumberSpace packetNumberSpace) { + return spaces.get(packetNumberSpace).getLargestPeerAckedPN(); + } + + @Override + public int connectionIdLength() { + return CIDLEN; + } + + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException { + int pos = buffer.position(); + encoder.encode(packet, buffer, this); + return buffer.position() - pos; + } + @Override + public QuicPacket parsePacket(ByteBuffer src) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + return decoder.decode(src, this); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicConnectionId originalServerConnId() { + return null; + } + + @Override + public QuicTLSEngine getTLSEngine() { + return TLS_ENGINE; + } + } + + /** + * An acknowledgement range, where acknowledged packets are [first..last]. + * For instance [9,9] acknowledges only packet 9. + * @param first the first packet acknowledged, inclusive + * @param last the last packet acknowledged, inclusive + */ + public static record Acknowledged(long first, long last) { + public Acknowledged { + assert first >= 0 && first <= last; + } + public boolean contains(long packet) { + return first <= packet && last >= packet; + } + public static List of(long... numbers) { + if (numbers == null || numbers.length == 0) return List.of(); + if (numbers.length % 2 != 0) throw new IllegalArgumentException(); + List res = new ArrayList<>(numbers.length/2); + for (int i = 0; i < numbers.length; i += 2) { + res.add(new Acknowledged(numbers[i], numbers[i+1])); + } + return List.copyOf(res); + } + } + + /** + * A packet to be emitted, followed by a pouse of {@code delay} in milliseconds. + * @param packetNumber the packet number of the packet to send + * @param delay a delay before the next packet should be emitted + */ + public static record Packet(long packetNumber, long delay) { + Packet(long packetNumber) { + this(packetNumber, RANDOM.nextLong(1, 255)); + } + static List ofAcks(List acks) { + return packets(acks); + } + static List of(long... numbers) { + return LongStream.of(numbers).mapToObj(Packet::new).toList(); + } + static final Comparator COMPARE_NUMBERS = Comparator.comparingLong(Packet::packetNumber); + } + + /** + * A test case. Composed of a list of acknowledgements, a list of packets, + * and a list of AckFrames. The list of packets is built from the list of + * acknowledgement - that is - every packet emitted should eventually be + * acknowledged. The list of AckFrame is built from the list of Packets, + * by randomly selecting a few consecutive packets in the packet list + * to acknowledge. The list of AckFrame is sorted by increasing + * largestAcknowledged. The list of Packets can be shuffled, which + * result on having AckFrames with gaps. + * @param acks A list of acknowledgement ranges + * @param packets A list of packets generated from the acknowledgement ranges. + * The list can be shuffled. + * @param ackframes A list of AckFrames, derived from the possibly shuffled + * list of packets. The list of AckFrame is sorted by increasing + * largestAcknowledged (since a packet can't be acknowledged + * before it's been emitted). + * @param shuffled whether the list of packets is shuffled. + */ + public static record TestCase(List acks, + List packets, + List ackframes, + boolean shuffled) { + public TestCase(List acks, List packets) { + this(acks, packets, ackFrames(packets),false); + } + public TestCase(List acks, List packets, boolean shuffled) { + this(acks, packets, ackFrames(packets), shuffled); + } + public TestCase(List acks) { + this(acks, Packet.ofAcks(acks)); + } + public TestCase shuffle() { + List shuffled = new ArrayList<>(packets); + Collections.shuffle(shuffled, RANDOM); + return new TestCase(acks, List.copyOf(shuffled), true); + } + } + + /** + * Construct a list of AckFrames from the possibly shuffled list + * of Packets. + * @param packets a list of packets + * @return a sorted list of AckFrames + */ + private static List ackFrames(List packets) { + List result = new ArrayList<>(); + int remaining = packets.size(); + int i = 0; + while (remaining > 0) { + int ackCount = Math.min(RANDOM.nextInt(1, 5), remaining); + AckFrameBuilder builder = new AckFrameBuilder(); + for (int j=0; j < ackCount; j++) { + builder.addAck(packets.get(i + j).packetNumber); + } + result.add(builder.build()); + i += ackCount; + remaining -= ackCount; + } + result.sort(Comparator.comparingLong(AckFrame::largestAcknowledged)); + return List.copyOf(result); + } + + /** + * Generates test cases - by concatenating a list of simple test case, + * a list of special testcases, and a list of random testcases. + * @return A list of TestCases to test. + */ + List generateTests() { + List tests = new ArrayList<>(); + List simples = List.of( + new TestCase(List.of(new Acknowledged(5,5))), + new TestCase(List.of(new Acknowledged(5,7))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,9))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,7))), + new TestCase(List.of(new Acknowledged(3,3), new Acknowledged(5,7))) + ); + tests.addAll(simples); + List specials = List.of( + new TestCase(Acknowledged.of(5,5,7,7), Packet.of(5,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(5,7,6), true), + new TestCase(Acknowledged.of(6,7), Packet.of(6,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(6,7,5), true), + new TestCase(Acknowledged.of(5,7), Packet.of(5,6,7), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(5, 7, 8), true), + new TestCase(Acknowledged.of(5,5,8,8), Packet.of(8, 5), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(8, 5, 7), true), + new TestCase(Acknowledged.of(3,5,7,9), Packet.of(8,5,7,4,9,3), true), + new TestCase(Acknowledged.of(27,27,31,31), + Packet.of(27, 31), true), + new TestCase(Acknowledged.of(27,27,29,29,31,31), + Packet.of(27, 31, 29), true), + new TestCase(Acknowledged.of(3,5,7,7,9,9,22,22,27,27,29,29,31,31), + Packet.of(4,22,27,31,9,29,7,5,3), true) + ); + tests.addAll(specials); + for (int i=0; i < 5; i++) { + List acks = generateAcks(); + List packets = packets(acks); + TestCase test = new TestCase(acks, List.copyOf(packets), false); + tests.add(test); + for (int j = 0; j < 5; j++) { + tests.add(test.shuffle()); + } + } + return tests; + } + + /** + * Generate a random list of increasing acknowledgement ranges. + * A packet should only be present once. + * @return a random list of increasing acknowledgement ranges. + */ + List generateAcks() { + int count = RANDOM.nextInt(3, 10); + List acks = new ArrayList<>(count); + long prev = -1; + for (int i=0; i packets(List acks) { + List res = new ArrayList<>(); + for (Acknowledged ack : acks) { + for (long i = ack.first() ; i<= ack.last() ; i++) { + var packet = new Packet(i); + assert !res.contains(packet); + res.add(packet); + } + } + return res; + } + + @DataProvider(name = "tests") + public Object[][] tests() { + return generateTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + // TODO: + // 1. TestCase should have an ordered list of packets. + // 2. packets will be emitted in order - that is - + // PacketSpaceManager::packetSent will be called for each packet + // in order. + // 3. acknowledgements of packet should arrive in random order. + // a selection of packets should be acknowledged in bunch... + // However, a packet shouldn't be acknowledged before it is emitted. + // 4. some packets should not be acknowledged in time, causing + // them to be retransmitted. + // 5. all of retransmitted packets should eventually be acknowledged, + // but which packet number (among the list of numbers under which + // a packet is retransmitted should be random). + // 6. packets that are acknowledged should no longer be retransmitted + // 7. code an AsynchronousTestDriver that uses the EXECUTOR + // this is more difficult as it makes it more difficult to guess + // at when exactly a packet will be retransmitted... + + /** + * A synchronous test driver to drive a TestCase. + * The method {@link #run()} drives the test. + */ + static class SynchronousTestDriver implements PacketEmitter { + final TestCase test; + final long timeline; + final QuicTimerQueue timerQueue; + final PriorityBlockingQueue packetQueue; + final PriorityBlockingQueue framesQueue; + final PacketSpaceManager manager; + final PacketNumberSpace space; + final Executor executor = this::execute; + final Logger debug = TestLoggerUtil.getErrOutLogger(this::toString); + final TestCodingContext codingContext; + final ConcurrentLinkedQueue emittedAckPackets; + final QuicRttEstimator rttEstimator; + final QuicCongestionController congestionController; + final QuicConnectionId localId; + final QuicConnectionId peerId; + final TimeSource timeSource = new TimeSource(); + final long maxPacketNumber; + final AckFrameBuilder allAcks = new AckFrameBuilder(); + + SynchronousTestDriver(TestCase test) { + this.space = PacketNumberSpace.INITIAL; + this.test = test; + + localId = newId(); + peerId = newId(); + timeline = test.packets().stream() + .mapToLong(Packet::delay) + .reduce(0, Math::addExact); + timerQueue = new QuicTimerQueue(this::notifyQueue, debug); + packetQueue = new PriorityBlockingQueue<>(test.packets.size(), Packet.COMPARE_NUMBERS); + packetQueue.addAll(test.packets); + framesQueue = new PriorityBlockingQueue<>(test.ackframes.size(), + Comparator.comparingLong(AckFrame::largestAcknowledged)); + framesQueue.addAll(test.ackframes); + emittedAckPackets = new ConcurrentLinkedQueue<>(); + rttEstimator = new QuicRttEstimator() { + @Override + public synchronized Duration getLossThreshold() { + return Duration.ofMillis(250); + } + + @Override + public synchronized Duration getBasePtoDuration() { + return Duration.ofMillis(250); + } + }; + congestionController = new QuicCongestionController() { + @Override + public boolean canSendPacket() { + return true; + } + @Override + public void updateMaxDatagramSize(int newSize) { } + @Override + public void packetSent(int packetBytes) { } + @Override + public void packetAcked(int packetBytes, Deadline sentTime) { } + @Override + public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { } + @Override + public void packetDiscarded(Collection discardedPackets) { } + }; + manager = new PacketSpaceManager(space, this, timeSource, + rttEstimator, congestionController, new DummyQuicTLSEngine(), + this::toString); + maxPacketNumber = test.packets().stream().mapToLong(Packet::packetNumber) + .max().getAsLong(); + manager.getNextPN().set(maxPacketNumber + 1); + codingContext = new TestCodingContext(new PacketSpaces(manager, null, null)); + } + + static class TimeSource implements TimeLine { + final Deadline first = jdk.internal.net.http.common.TimeSource.now(); + volatile Deadline current = first; + public synchronized Deadline advance(long duration, TemporalUnit unit) { + return current = current.plus(duration, unit); + } + public Deadline advanceMillis(long millis) { + return advance(millis, ChronoUnit.MILLIS); + } + @Override + public Deadline instant() { + return current; + } + } + + void notifyQueue() { + timerQueue.processEventsAndReturnNextDeadline(now(), executor); + } + + @Override + public QuicTimerQueue timer() { return timerQueue;} + + @Override + public void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) { + if (!(packet instanceof InitialPacket initial)) + throw new AssertionError("unexpected packet type: " + packet); + long newPacketNumber = packetSpaceManager.allocateNextPN(); + debug.log("Retransmitting packet %d as %d (%d attempts)", + packet.packetNumber(), newPacketNumber, attempts); + assert attempts >= 0; + QuicPacket newPacket = codingContext.encoder + .newInitialPacket(initial.sourceId(), initial.destinationId(), + initial.token(), newPacketNumber, + packetSpaceManager.getLargestPeerAckedPN(), + initial.frames(), codingContext); + long number = initial.packetNumber(); + Deadline now = now(); + manager.packetSent(newPacket, number, newPacket.packetNumber()); + retransmissions.add(new Retransmission(initial.packetNumber(), now, + AckFrame.largestAcknowledgedInPacket(newPacket))); + expectedRetransmissions.stream() + .filter(r -> r.isFor(number)) + .forEach(r -> { + assertTrue(r.isDue(now) || + packetSpaceManager.getLargestPeerAckedPN() - 3 > number, + "retransmitted packet %d is not yet due".formatted(number)); + successfulExpectations.add(r); + }); + boolean removed = expectedRetransmissions.removeIf(r -> r.isFor(number)); + if (number <= maxPacketNumber) { + assertTrue(removed, "retransmission of packet %d was not expected" + .formatted(number)); + } + } + + @Override + public long emitAckPacket(PacketSpace packetSpaceManager, + AckFrame ackFrame, boolean sendPing) { + long newPacketNumber = packetSpaceManager.allocateNextPN(); + debug.log("Emitting ack packet %d for %s (sendPing: %s)", + newPacketNumber, ackFrame, sendPing); + List frames; + if (ackFrame != null) { + frames = sendPing + ? List.of(new PingFrame(), ackFrame) + : List.of(ackFrame); + } else { + assert sendPing; + frames = List.of(new PingFrame()); + } + QuicPacket newPacket = codingContext.encoder + .newInitialPacket(localId, peerId, + null, newPacketNumber, + packetSpaceManager.getLargestPeerAckedPN(), + frames, codingContext); + packetSpaceManager.packetSent(newPacket, -1, newPacketNumber); + emittedAckPackets.offer(newPacket); + return newPacket.packetNumber(); + } + + @Override + public void acknowledged(QuicPacket packet) { + // TODO: nothing to do? + } + + @Override + public boolean sendData(PacketNumberSpace packetNumberSpace) { + return false; + } + + @Override + public Executor executor() { + return this::execute; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void checkAbort(PacketNumberSpace packetNumberSpace) { } + + final CopyOnWriteArrayList expectedRetransmissions = new CopyOnWriteArrayList<>(); + final CopyOnWriteArrayList retransmissions = new CopyOnWriteArrayList<>(); + final CopyOnWriteArrayList successfulExpectations = new CopyOnWriteArrayList<>(); + + static record Retransmission(long packetNumber, Deadline atOrAfter, long largestAckSent) { + boolean isFor(long number) { + return number == packetNumber; + } + boolean isDue(Deadline now) { + return !atOrAfter.isAfter(now); + } + } + + /** + * Drives the test by pretending to emit each packet in order, + * then pretending to receive ack frames (as soon as possible + * given the largest packet number emitted). + * The timeline is advanced by chunks as instructed by + * the test. + * This method checks that the retransmission logic works as + * expected. + * @throws Exception + */ + // TODO: in the end we need to check that everything that was + // expected to happen happened. What is missing is to + // check the generation of ACK packets... Also a + // retransmitted packet may need to itself retransmitted + // again and we have no test for that. + public void run() throws Exception { + long timeline = 0; + long serverPacketNumbers = 0; + long maxAck; + Packet packet; + debug.log("Packets: %s", test.packets.stream().mapToLong(Packet::packetNumber) + .mapToObj(String::valueOf) + .collect(Collectors.joining(", ", "[", "]"))); + debug.log("Frames: %s", test.ackframes.stream().mapToLong(AckFrame::largestAcknowledged) + .mapToObj(String::valueOf) + .collect(Collectors.joining(", ", "[", "]"))); + long maxRetransmissionDelay = 250; + long maxAckDelay = manager.getMaxAckDelay(); + + Deadline start = now(); + Deadline nextSendAckDeadline = Deadline.MAX; + long firstAckPaket = -1, lastAckPacket = -1; + long largestAckAcked = -1; + boolean previousAckEliciting = false; + // simulate sending each packet, ordered by their packet number + while ((packet = packetQueue.poll()) != null) { + long offset = packet.packetNumber; + AckFrame nextAck = framesQueue.peek(); + maxAck = nextAck == null ? Long.MAX_VALUE : nextAck.largestAcknowledged(); + long largestReceivedAckedPN = manager.getLargestPeerAckedPN(); + debug.log("timeline: at %dms", timeline); + debug.log("sending packet: %d, largest ACK received: %d", + packet.packetNumber, largestReceivedAckedPN); + + // randomly decide whether we should attempt to include an ack frame + // with the next packet we send out... + boolean sendAck = RANDOM.nextBoolean(); + AckFrame ackFrameToSend = sendAck ? manager.getNextAckFrame(false) : null; + long largestAckSent = -1; + if (ackFrameToSend != null) { + previousAckEliciting = false; + debug.log("including ACK frame: " + ackFrameToSend); + nextSendAckDeadline = Deadline.MAX; + // assertFalse used on purpose here to make + // sure the stack trace can't be confused with one that + // originate in another similar lambda that use assertTrue below. + LongStream.range(firstAckPaket, lastAckPacket + 1).sequential() + .forEach(p -> assertFalse(!ackFrameToSend.isAcknowledging(p), + "frame %s should acknowledge %d" + .formatted(ackFrameToSend, p))); + largestAckSent = ackFrameToSend.largestAcknowledged(); + debug.log("largestAckSent is: " + largestAckAcked); + } + + // add a crypto frame and build the packet + CryptoFrame crypto = new CryptoFrame(offset, 1, + ByteBuffer.wrap(new byte[] {nextByte(offset)})); + List frames = ackFrameToSend == null ? + List.of(crypto) : List.of(crypto, ackFrameToSend); + QuicPacket newPacket = codingContext.encoder + .newInitialPacket(localId, peerId, + null, + packet.packetNumber, + largestReceivedAckedPN, + frames, codingContext); + // pretend that we sent a packet + manager.packetSent(newPacket, -1, packet.packetNumber); + + // compute next deadline + var nextDeadline = timerQueue.nextDeadline(); + var nextScheduledDeadline = manager.nextScheduledDeadline(); + var nextComputedDeadline = manager.computeNextDeadline(); + var now = now(); + debugDeadline("nextDeadline", start, now, nextDeadline); + debugDeadline("nextScheduledDeadline", start, now, nextScheduledDeadline); + debugDeadline("nextComputedDeadline", start, now, nextComputedDeadline); + assertFalse(nextDeadline.isAfter(now.plusMillis(maxRetransmissionDelay * rttEstimator.getPtoBackoff())), + "nextDeadline should not be after %dms from now!" + .formatted(maxRetransmissionDelay)); + expectedRetransmissions.add(new Retransmission(packet.packetNumber, + now.plus(maxRetransmissionDelay, ChronoUnit.MILLIS), largestAckSent)); + + List pending = manager.pendingAcknowledgements((s) ->s.boxed().toList()); + debug.log("pending ack: %s", pending); + assertContains(Assertion.TRUE, pending, packet.packetNumber, + "pending ack"); + + pending = manager.pendingRetransmission((s) ->s.boxed().toList()); + debug.log("pending retransmission: %s", pending); + assertContains(Assertion.FALSE, pending, packet.packetNumber, + "pending retransmission"); + + pending = manager.triggeredForRetransmission((s) ->s.boxed().toList()); + debug.log("triggered for retransmission: %s", pending); + assertContains(Assertion.FALSE, pending, packet.packetNumber, + "triggered for retransmission"); + + if (!nextDeadline.isAfter(now) || !nextSendAckDeadline.isAfter(now)) { + var nextI = timerQueue + .processEventsAndReturnNextDeadline(now, executor); + // this might have triggered sending an ACK packet, unless we + // already sent the ack with the initial packet just above. + debugDeadline("new deadline after events", start, now, nextI); + } + + // check generated ack packets, if any should have been generated... + if (!nextSendAckDeadline.isAfter(now)) { + debug.log("checking emitted ack packets: emitted %d", emittedAckPackets.size()); + var ackPacket = emittedAckPackets.poll(); + assertNotNull(ackPacket); + List ackFrames = ackPacket.frames() + .stream().filter(AckFrame.class::isInstance) + .map(AckFrame.class::cast) + .toList(); + assertEquals(frames.size(), 1, + "unexpected ack frames: " + frames); + AckFrame ackFrame = ackFrames.get(0); + LongStream.range(firstAckPaket, lastAckPacket + 1) + .forEach(p -> assertTrue(ackFrame.isAcknowledging(p), + "frame %s should acknowledge %d" + .formatted(ackFrame, p))); + assertNull(emittedAckPackets.peek(), + "emitted ackPacket queue not empty: " + emittedAckPackets); + debug.log("Got expected ackFrame for emitted ack packet: %s", ackFrame); + previousAckEliciting = false; + nextSendAckDeadline = Deadline.MAX; + } + + // advance the timeline by the instructed delay... + Deadline next = timeSource.advanceMillis(packet.delay); + timeline = timeline + packet.delay; + debug.log("advance deadline by %dms at %dms", packet.delay, timeline); + // note: beyond this point now > now(); packets between now and now() + // may not be retransmitted yet + + // Do not pretend to receive acknowledgement for + // packets that we haven't sent yet. + if (packet.packetNumber >= maxAck) { + // pretend to be receiving the next ack frame... + nextAck = framesQueue.poll(); + debug.log("Receiving acks for " + nextAck); + long spn = serverPacketNumbers++; + boolean isAckEliciting = RANDOM.nextBoolean(); + manager.packetReceived(PacketType.INITIAL, spn, isAckEliciting); + + // calculate if and when we should send out the ack frame for + // the ACK packet we just received + if (firstAckPaket == -1) firstAckPaket = spn; + lastAckPacket = spn; + debug.log("next sent ack should acknowledge [%d..%d]", + firstAckPaket, lastAckPacket); + if (isAckEliciting) { + debug.log("prevEliciting: %s", + previousAckEliciting); + if (previousAckEliciting) { + nextSendAckDeadline = min(now, nextSendAckDeadline); + } else { + nextSendAckDeadline = min(nextSendAckDeadline, next.plusMillis(maxAckDelay)); + } + debugDeadline("next ack deadline", start, next, nextSendAckDeadline); + } + previousAckEliciting |= isAckEliciting; + + // process the ack frame we just received + assertNotNull(nextAck); + manager.processAckFrame(nextAck); + firstAckPaket = Math.max(firstAckPaket, manager.getMinPNThreshold() + 1); + + // Here we can compute which packets will not be acknowledged yet, + // and which packets will be retransmitted. + long largestAckAckedBefore = largestAckAcked; + for (long number : acknowledgePackets(nextAck)) { + allAcks.addAck(number); + pending = manager.pendingAcknowledgements((s) -> s.boxed().toList()); + assertContains(Assertion.FALSE, pending, number, + "pending ack"); + + pending = manager.pendingRetransmission((s) -> s.boxed().toList()); + assertContains(Assertion.FALSE, pending, number, + "pending retransmission"); + + pending = manager.triggeredForRetransmission((s) -> s.boxed().toList()); + assertContains(Assertion.FALSE, pending, number, + "triggered for retransmission"); + // TODO check if we only retransmitted the expected packets + // ...need to replicate the logic + + /*expectedRetransmissions.stream() + .filter(r -> r.isFor(number)) + .forEach(r -> assertFalse(r.isDue(now), + "due packet %d was not retransmitted".formatted(number))); + for (Retransmission r : expectedRetransmissions) { + if (r.isFor(number)) { + largestAckAcked = Math.max(largestAckAcked, r.largestAckSent); + } + }*/ + for (Retransmission r : successfulExpectations) { + if (r.isFor(number)) { + largestAckAcked = Math.max(largestAckAcked, r.largestAckSent); + } + } + if (largestAckAcked != largestAckAckedBefore) { + debug.log("largestAckAcked is now %d", largestAckAcked); + largestAckAckedBefore = largestAckAcked; + } + if (largestAckAcked > -1) { + boolean changed = false; + if (firstAckPaket <= largestAckAcked) { + changed = true; + if (lastAckPacket > largestAckAcked) { + firstAckPaket = largestAckAcked + 1; + } else firstAckPaket = -1; + } + if (lastAckPacket <= largestAckAcked) { + changed = true; + lastAckPacket = -1; + } + if (changed) { + debug.log("next sent ack should now be [%d..%d]", + firstAckPaket, lastAckPacket); + } + } + boolean removed = expectedRetransmissions.removeIf(r -> r.isFor(number)); + successfulExpectations.stream() + .filter(r -> r.isFor(number)) + .forEach( r -> assertTrue(r.isDue(now()) || + manager.getLargestPeerAckedPN() - 3 > r.packetNumber, + "packet %d was retransmitted too early (deadline was %d)" + .formatted(number, start + .until(r.atOrAfter, ChronoUnit.MILLIS)))); + retransmissions.stream().filter(r -> r.isFor(number)).forEach( r -> { + assertTrue(r.isDue(now()), + "packet %d was retransmitted too early at %d" + .formatted(number, start + .until(r.atOrAfter, ChronoUnit.MILLIS))); + assertFalse(removed, "packet %d was in both lists" + .formatted(number)); + }); + } + } + } + } + + Deadline min(Deadline one, Deadline two) { + return one.isAfter(two) ? two : one; + } + + /** + * Should be called after {@link #run()}. + */ + void check() { + assertFalse(now().isBefore(timeSource.first.plusMillis(timeline))); + assertTrue(expectedRetransmissions.isEmpty()); + assertEquals(retransmissions.stream() + .map(Retransmission::packetNumber) + .filter(pn -> pn <= maxPacketNumber).toList(), + successfulExpectations.stream().map(Retransmission::packetNumber) + .filter(pn -> pn <= maxPacketNumber).toList()); + for (Retransmission r : retransmissions) { + if (r.packetNumber > maxPacketNumber || + manager.getLargestPeerAckedPN() - 3 > r.packetNumber) continue; + List succesful = successfulExpectations.stream() + .filter(s -> s.isFor(r.packetNumber)) + .toList(); + assertEquals(succesful.size(), 1); + succesful.forEach(s -> assertFalse(s.atOrAfter.isAfter(r.atOrAfter))); + } + + List acknowledged = new ArrayList<>(acknowledgePackets(allAcks.build())); + Collections.sort(acknowledged); + assertEquals(acknowledged, test.packets.stream() + .map(Packet::packetNumber).sorted().toList()); + } + + // TODO: add a LongStream acknowledged() to AckFrame - write a spliterator + // for that. + List acknowledgePackets(AckFrame frame) { + List list = new ArrayList<>(); + long largest = frame.largestAcknowledged(); + long smallest = largest + 2; + for (AckRange range : frame.ackRanges()) { + largest = smallest - range.gap() -2; + smallest = largest - range.range(); + for (long i = largest; i >= smallest; i--) { + assert frame.isAcknowledging(i) + : "%s is not acknowledging %d".formatted(frame, i); + list.add(i); + } + } + return list; + } + + interface Assertion { + void check(boolean result, String message); + default String negation() { + return (this == FALSE) ? "doesn't " : ""; + } + Assertion TRUE = Assert::assertTrue; + Assertion FALSE = Assert::assertFalse; + } + static void assertContains(Assertion assertion, List list, long number, String desc) { + assertion.check(list.contains(number), + "%s: %s %scontains %d".formatted(desc, list, assertion.negation(), number)); + } + + void debugDeadline(String desc, Deadline start, Deadline now, Deadline deadline) { + long nextMs = deadline.equals(Deadline.MAX) ? 0 : + now.until(deadline, ChronoUnit.MILLIS); + long at = deadline.equals(Deadline.MAX) ? 0 : + start.until(deadline, ChronoUnit.MILLIS); + String when = deadline.equals(Deadline.MAX) ? "never" + : nextMs >= 0 ? ("at %d in %dms".formatted(at, nextMs)) + : ("at %d due by %dms".formatted(at, (-nextMs))); + debug.log("%s: %s", desc, when); + } + + byte nextByte(long offset) { + long start = 'a'; + int len = 'z' - 'a' + 1; + long res = start + offset % len; + assert res >= 'a' && res <= 'z'; + return (byte) res; + } + + private void execute(Runnable runnable) { + runnable.run(); + } + + Deadline now() { + return timeSource.instant(); + } + + static QuicConnectionId newId() { + byte[] idbites = new byte[CIDLEN]; + RANDOM.nextBytes(idbites); + return new PeerConnectionId(idbites); + } + } + + @Test(dataProvider = "tests") + public void testPacketSpaceManager(TestCase testCase) throws Exception { + System.out.printf("%n ------- testPacketSpaceManager ------- %n"); + SynchronousTestDriver driver = new SynchronousTestDriver(testCase); + driver.run(); + driver.check(); + } + + } diff --git a/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java b/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java new file mode 100644 index 00000000000..f0d4b316e46 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java @@ -0,0 +1,298 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + +import static org.testng.Assert.*; + + +/* + * @test + * @library /test/lib + * @summary Tests to check QUIC frame decoding errors are handled correctly + * @run testng/othervm QuicFramesDecoderTest + */ +public class QuicFramesDecoderTest { + + + // correct frames. Single byte frames (padding, ping, handshake_done) omitted. + // ACK without ECN, acked = 0, 2; delay = 2 + private static final byte[] ACK_BASE = HexFormat.of().parseHex("02020201000000"); + private static final byte[] ACK_BASE_B = HexFormat.of().parseHex("0202020100004000"); + + // ACK with ECN, acked = 0, 2; delay = 2, ECN=(3,4,5) + private static final byte[] ACK_ECN = HexFormat.of().parseHex("03020201000000030405"); + private static final byte[] ACK_ECN_B = HexFormat.of().parseHex("0302020100000003044005"); + + // RESET_STREAM, stream 3, error 2, final size 1 + private static final byte[] RESET_STREAM = HexFormat.of().parseHex("04030201"); + private static final byte[] RESET_STREAM_B = HexFormat.of().parseHex("0403024001"); + + // STOP_SENDING, stream 4, error 3 + private static final byte[] STOP_SENDING = HexFormat.of().parseHex("050403"); + private static final byte[] STOP_SENDING_B = HexFormat.of().parseHex("05044003"); + + // CRYPTO, offset 5, length 4, data + private static final byte[] CRYPTO = HexFormat.of().parseHex("06050403020100"); + + // NEW_TOKEN, length 6, data + private static final byte[] NEW_TOKEN = HexFormat.of().parseHex("0706050403020100"); + + // STREAM-o-l-f, stream 7, no data + private static final byte[] STREAM = HexFormat.of().parseHex("0807"); + + // STREAM-o-l+f, stream 8, no data + private static final byte[] STREAM_F = HexFormat.of().parseHex("0908"); + + // STREAM-o+l-f, stream 9, length 8 + private static final byte[] STREAM_L = HexFormat.of().parseHex("0a09080706050403020100"); + + // STREAM-o+l+f, stream 10, length 9 + private static final byte[] STREAM_LF = HexFormat.of().parseHex("0b0a09080706050403020100"); + + // STREAM+o-l-f, stream 11, offset 0, no data + private static final byte[] STREAM_O = HexFormat.of().parseHex("0c000a"); + + // STREAM+o-l+f, stream 12, offset 0, no data + private static final byte[] STREAM_OF = HexFormat.of().parseHex("0d000b"); + + // STREAM+o+l-f, stream 13, offset 0, length 3 + private static final byte[] STREAM_OL = HexFormat.of().parseHex("0e000c03020100"); + + // STREAM+o+l+f, stream 14, offset 0, length 3 + private static final byte[] STREAM_OLF = HexFormat.of().parseHex("0f000d03020100"); + + // MAX_DATA, max=15 + private static final byte[] MAX_DATA = HexFormat.of().parseHex("100f"); + private static final byte[] MAX_DATA_B = HexFormat.of().parseHex("10400f"); + + // MAX_STREAM_DATA, stream = 16 max=15 + private static final byte[] MAX_STREAM_DATA = HexFormat.of().parseHex("11100f"); + private static final byte[] MAX_STREAM_DATA_B = HexFormat.of().parseHex("1110400f"); + + // MAX_STREAMS, bidi, streams = 2^60 + private static final byte[] MAX_STREAMS_B = HexFormat.of().parseHex("12d000000000000000"); + + // MAX_STREAMS, uni, streams = 2^60 + private static final byte[] MAX_STREAMS_U = HexFormat.of().parseHex("13d000000000000000"); + + // DATA_BLOCKED, max=19 + private static final byte[] DATA_BLOCKED = HexFormat.of().parseHex("1413"); + private static final byte[] DATA_BLOCKED_B = HexFormat.of().parseHex("144013"); + + // STREAM_DATA_BLOCKED, stream = 20 max=19 + private static final byte[] STREAM_DATA_BLOCKED = HexFormat.of().parseHex("151413"); + private static final byte[] STREAM_DATA_BLOCKED_B = HexFormat.of().parseHex("15144013"); + + // STREAMS_BLOCKED, bidi, streams = 2^60 + private static final byte[] STREAMS_BLOCKED_B = HexFormat.of().parseHex("16d000000000000000"); + + // STREAMS_BLOCKED, uni, streams = 2^60 + private static final byte[] STREAMS_BLOCKED_U = HexFormat.of().parseHex("17d000000000000000"); + + // NEW_CONNECTION_ID, seq=23, retire=22, len = 5 + private static final byte[] NEW_CONNECTION_ID = HexFormat.of().parseHex("181716"+ + "051413121110"+"0f0e0d0c0b0a09080706050403020100"); + + // RETIRE_CONNECTION_ID, seq=24 + private static final byte[] RETIRE_CONNECTION_ID = HexFormat.of().parseHex("1918"); + private static final byte[] RETIRE_CONNECTION_ID_B = HexFormat.of().parseHex("194018"); + + // PATH_CHALLENGE + private static final byte[] PATH_CHALLENGE = HexFormat.of().parseHex("1a0706050403020100"); + + // PATH_RESPONSE + private static final byte[] PATH_RESPONSE = HexFormat.of().parseHex("1b0706050403020100"); + + // CONNECTION_CLOSE, quic, error 27, frame type 26, reason='\0' + private static final byte[] CONNECTION_CLOSE_Q = HexFormat.of().parseHex("1c1b1a0100"); + // CONNECTION_CLOSE, quic, error 27, frame type 26, reason= + // efbfbf (U+FFFF) - "not a valid unicode character + // edb080 (U+DC00) - low surrogate, prohibited in UTF8 (RFC3629), must be preceded by high surrogate otherwise + // eda080 (U+D800) - high surrogate, prohibited in UTF8 (RFC3629), must be followed by low surrogate otherwise + // 80 - not a first byte of UTF8 sequence + // c0d0e0f0ff - not a valid UTF8 sequence + private static final byte[] CONNECTION_CLOSE_Q_BAD_REASON = HexFormat.of().parseHex("1c1b1a0fefbfbfedb080eda08080c0d0e0f0ff"); + + // CONNECTION_CLOSE, app, error 28, reason='\0' + private static final byte[] CONNECTION_CLOSE_A = HexFormat.of().parseHex("1d1c0100"); + // CONNECTION_CLOSE, app, error 28, reason= same as CONNECTION_CLOSE_Q_BAD_REASON + private static final byte[] CONNECTION_CLOSE_A_BAD_REASON = HexFormat.of().parseHex("1d1c0fefbfbfedb080eda08080c0d0e0f0ff"); + // end of correct frames + + // malformed frames other than truncated + // ACK acknowledging negative packet + // ACK without ECN, acked = -1, 1; delay = 2 + private static final byte[] ACK_NEG_BASE = HexFormat.of().parseHex("02010201000000"); + + // ACK with ECN, acked = -1, 1; delay = 2, ECN=(3,4,5) + private static final byte[] ACK_NEG_ECN = HexFormat.of().parseHex("03010201000000030405"); + + // ACK without ECN, acked = -1, 0; delay = 2 + private static final byte[] ACK_NEG_BASE_2 = HexFormat.of().parseHex("0200020001"); + + // CRYPTO out of range: offset MAX_VL_INT, len=1 + private static final byte[] CRYPTO_OOR = HexFormat.of().parseHex("06ffffffffffffffff0100"); + + // NEW_TOKEN empty + private static final byte[] NEW_TOKEN_EMPTY = HexFormat.of().parseHex("0700"); + + // MAX_STREAMS out of range + // MAX_STREAMS, bidi, streams = 2^60+1 + private static final byte[] MAX_STREAMS_B_OOR = HexFormat.of().parseHex("12d000000000000001"); + // MAX_STREAMS, uni, streams = 2^60+1 + private static final byte[] MAX_STREAMS_U_OOR = HexFormat.of().parseHex("13d000000000000001"); + + // STREAMS_BLOCKED out of range + // STREAMS_BLOCKED, bidi, streams = 2^60+1 + private static final byte[] STREAMS_BLOCKED_B_OOR = HexFormat.of().parseHex("16d000000000000001"); + + // STREAMS_BLOCKED, uni, streams = 2^60+1 + private static final byte[] STREAMS_BLOCKED_U_OOR = HexFormat.of().parseHex("17d000000000000001"); + + // NEW_CONNECTION_ID, seq=23, retire=22, len = 0 + private static final byte[] NEW_CONNECTION_ID_ZERO = HexFormat.of().parseHex("181716"+ + "00"+"0f0e0d0c0b0a09080706050403020100"); + + @DataProvider + public static Object[][] goodFrames() { + return new Object[][]{ + new Object[]{"ack without ecn", ACK_BASE, false}, + new Object[]{"ack without ecn", ACK_BASE_B, true}, + new Object[]{"ack with ecn", ACK_ECN, false}, + new Object[]{"ack with ecn", ACK_ECN_B, true}, + new Object[]{"RESET_STREAM", RESET_STREAM, false}, + new Object[]{"RESET_STREAM", RESET_STREAM_B, true}, + new Object[]{"STOP_SENDING", STOP_SENDING, false}, + new Object[]{"STOP_SENDING", STOP_SENDING_B, true}, + new Object[]{"CRYPTO", CRYPTO, false}, + new Object[]{"NEW_TOKEN", NEW_TOKEN, false}, + new Object[]{"STREAM-o-l-f", STREAM, false}, + new Object[]{"STREAM-o-l+f", STREAM_F, false}, + new Object[]{"STREAM-o+l-f", STREAM_L, false}, + new Object[]{"STREAM-o+l+f", STREAM_LF, false}, + new Object[]{"STREAM+o-l-f", STREAM_O, false}, + new Object[]{"STREAM+o-l+f", STREAM_OF, false}, + new Object[]{"STREAM+o+l-f", STREAM_OL, false}, + new Object[]{"STREAM+o+l+f", STREAM_OLF, false}, + new Object[]{"MAX_DATA", MAX_DATA, false}, + new Object[]{"MAX_DATA", MAX_DATA_B, true}, + new Object[]{"MAX_STREAM_DATA", MAX_STREAM_DATA, false}, + new Object[]{"MAX_STREAM_DATA", MAX_STREAM_DATA_B, true}, + new Object[]{"MAX_STREAMS bidi", MAX_STREAMS_B, false}, + new Object[]{"MAX_STREAMS uni", MAX_STREAMS_U, false}, + new Object[]{"DATA_BLOCKED", DATA_BLOCKED, false}, + new Object[]{"DATA_BLOCKED", DATA_BLOCKED_B, true}, + new Object[]{"STREAM_DATA_BLOCKED", STREAM_DATA_BLOCKED, false}, + new Object[]{"STREAM_DATA_BLOCKED", STREAM_DATA_BLOCKED_B, true}, + new Object[]{"STREAMS_BLOCKED bidi", STREAMS_BLOCKED_B, false}, + new Object[]{"STREAMS_BLOCKED uni", STREAMS_BLOCKED_U, false}, + new Object[]{"NEW_CONNECTION_ID", NEW_CONNECTION_ID, false}, + new Object[]{"RETIRE_CONNECTION_ID", RETIRE_CONNECTION_ID, false}, + new Object[]{"RETIRE_CONNECTION_ID", RETIRE_CONNECTION_ID_B, true}, + new Object[]{"PATH_CHALLENGE", PATH_CHALLENGE, false}, + new Object[]{"PATH_RESPONSE", PATH_RESPONSE, false}, + new Object[]{"CONNECTION_CLOSE QUIC", CONNECTION_CLOSE_Q, false}, + new Object[]{"CONNECTION_CLOSE QUIC non-utf8 reason", CONNECTION_CLOSE_Q_BAD_REASON, false}, + new Object[]{"CONNECTION_CLOSE app", CONNECTION_CLOSE_A, false}, + new Object[]{"CONNECTION_CLOSE app non-utf8 reason", CONNECTION_CLOSE_A_BAD_REASON, false}, + }; + } + + @DataProvider + public static Object[][] badFrames() { + return new Object[][]{ + new Object[]{"ack without ecn, negative pn", ACK_NEG_BASE}, + new Object[]{"ack without ecn, negative pn, v2", ACK_NEG_BASE_2}, + new Object[]{"ack with ecn, negative pn", ACK_NEG_ECN}, + new Object[]{"CRYPTO out of range", CRYPTO_OOR}, + new Object[]{"NEW_TOKEN empty", NEW_TOKEN_EMPTY}, + new Object[]{"MAX_STREAMS bidi out of range", MAX_STREAMS_B_OOR}, + new Object[]{"MAX_STREAMS uni out of range", MAX_STREAMS_U_OOR}, + new Object[]{"STREAMS_BLOCKED bidi out of range", STREAMS_BLOCKED_B_OOR}, + new Object[]{"STREAMS_BLOCKED uni out of range", STREAMS_BLOCKED_U_OOR}, + new Object[]{"NEW_CONNECTION_ID zero length", NEW_CONNECTION_ID_ZERO}, + }; + } + + @Test(dataProvider = "goodFrames") + public void testReencode(String desc, byte[] frame, boolean bloated) throws Exception { + // check if the goodFrames provider indeed contains good frames + ByteBuffer buf = ByteBuffer.wrap(frame); + var qf = QuicFrame.decode(buf); + assertFalse(buf.hasRemaining(), buf.remaining() + " bytes left in buffer after parsing"); + // some frames deliberately use suboptimal encoding, skip them + if (bloated) return; + assertEquals(qf.size(), frame.length, "Frame size mismatch"); + buf.clear(); + ByteBuffer encoded = ByteBuffer.allocate(frame.length); + qf.encode(encoded); + assertFalse(encoded.hasRemaining(), "Actual frame length mismatch"); + encoded.flip(); + assertEquals(buf, encoded, "Encoded buffer is different from the original one"); + } + + @Test(dataProvider = "goodFrames") + public void testToString(String desc, byte[] frame, boolean bloated) throws Exception { + // check if the goodFrames provider indeed contains good frames + ByteBuffer buf = ByteBuffer.wrap(frame); + var qf = QuicFrame.decode(buf); + assertFalse(buf.hasRemaining(), buf.remaining() + " bytes left in buffer after parsing"); + System.out.println(qf); // should not throw + } + + @Test(dataProvider = "goodFrames") + public void testTruncatedFrame(String desc, byte[] frame, boolean bloated) throws Exception { + // check if parsing a truncated frame throws the right error + ByteBuffer buf = ByteBuffer.wrap(frame); + for (int i = 1; i < buf.capacity(); i++) { + buf.position(0); + buf.limit(i); + try { + var qf = QuicFrame.decode(buf); + fail("Expected the decoder to throw on length " + i + ", got: " + qf); + } catch (QuicTransportException e) { + assertEquals(e.getErrorCode(), QuicTransportErrors.FRAME_ENCODING_ERROR.code()); + } + } + } + + @Test(dataProvider = "badFrames") + public void testBadFrame(String desc, byte[] frame) throws Exception { + // check if parsing a bad frame throws the right error + ByteBuffer buf = ByteBuffer.wrap(frame); + try { + var qf = QuicFrame.decode(buf); + fail("Expected the decoder to throw, got: "+qf); + } catch (QuicTransportException e) { + assertEquals(e.getErrorCode(), QuicTransportErrors.FRAME_ENCODING_ERROR.code()); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java b/test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java new file mode 100644 index 00000000000..2c5844dc4c4 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java @@ -0,0 +1,172 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.httpclient.test.lib.common.TestUtil + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.internal.httpclient.debug=true QuicRequestResponseTest + */ +public class QuicRequestResponseTest { + + private QuicStandaloneServer server; + private SSLContext sslContext; + private ExecutorService executor; + + private static final byte[] HELLO_MSG = "Hello Quic".getBytes(StandardCharsets.UTF_8); + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .build(); + // add a handler which deals with incoming connections + server.addHandler(new EchoHandler(HELLO_MSG.length)); + server.start(); + System.out.println("Server started at " + server.getAddress()); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + if (executor != null) executor.close(); + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + @Test + public void test() throws Exception { + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, server.getAddress()); + // open a bidi stream + final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + // write data on the stream + try (final OutputStream os = bidiStream.outputStream()) { + os.write(HELLO_MSG); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for response + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("client: reading from bidi stream's input stream"); + final byte[] data = is.readAllBytes(); + System.out.println("client: Received response of size " + data.length); + final String response = new String(data, StandardCharsets.UTF_8); + // verify response + System.out.println("client: Response: " + response); + if (!Arrays.equals(response.getBytes(StandardCharsets.UTF_8), HELLO_MSG)) { + throw new AssertionError("Unexpected response: " + response); + } + } finally { + System.err.println("client: Closing bidi stream from test"); + bidiStream.close(); + } + } + } + + /** + * Reads data from incoming client initiated bidirectional stream of a Quic connection + * and writes back a response which is same as the read data + */ + private static final class EchoHandler implements QuicServerHandler { + + private final int numBytesToRead; + + private EchoHandler(final int numBytesToRead) { + this.numBytesToRead = numBytesToRead; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + final byte[] data; + // read the request content + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("Handler reading data from bidi stream's inputstream " + is); + data = is.readAllBytes(); + System.out.println("Handler read " + data.length + " bytes of data"); + } + if (data.length != numBytesToRead) { + throw new IOException("Expected to read " + numBytesToRead + + " bytes but read only " + data.length + " bytes"); + } + // write response + try (final OutputStream os = bidiStream.outputStream()) { + System.out.println("Handler writing data to bidi stream's outputstream " + os); + os.write(data); + } + System.out.println("Handler invocation complete"); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java b/test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java new file mode 100644 index 00000000000..8eb7a3663d8 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java @@ -0,0 +1,303 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.NO_VIABLE_PATH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary verify that when a QUIC (client) connection receives a stateless reset + * from the peer, then the connection is properly terminated + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * @run junit/othervm -Djdk.internal.httpclient.debug=true StatelessResetReceiptTest + */ +public class StatelessResetReceiptTest { + + private static QuicStandaloneServer server; + private static SSLContext sslContext; + private static ExecutorService executor; + + @BeforeAll + static void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + } + + @AfterAll + static void afterAll() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + if (executor != null) { + executor.close(); + } + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + /** + * Initiates a connection between client and server. When the connection is still + * active, this test initiates a stateless reset from the server connection against + * the client connection. The test then expects that the client connection, which was active + * until then, is terminated due to this stateless reset. + */ + @Test + public void testActiveConnection() throws Exception { + final CompletableFuture serverConnCF = new MinimalFuture<>(); + final NotifyingHandler handler = new NotifyingHandler(serverConnCF); + server.addHandler(handler); + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + // write data on the stream + try (final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + final OutputStream os = bidiStream.outputStream()) { + os.write("foobar".getBytes(StandardCharsets.UTF_8)); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for the handler on the server's connection to be invoked + System.out.println("waiting for the request to be handled by the server connection"); + final QuicServerConnection serverConn = serverConnCF.get(); + System.out.println("request handled by the server connection " + serverConn); + // verify the connection is still open + assertTrue(conn.underlyingQuicConnection().isOpen(), "QUIC connection is not open"); + sendStatelessResetFrom(serverConn); + // now expect the (active) client connection to be terminated + assertStatelessResetTermination(conn); + } + } + + /** + * Initiates a connection between client and server. The client connection is then + * closed and it thus moves to the closing state. The test then initiates a stateless reset + * from the server connection against this closing client connection. The test then verifies + * that the client connection, which was in closing state, has been completely removed from the + * endpoint upon receiving this stateless reset. + */ + @Test + public void testClosingConnection() throws Exception { + final CompletableFuture serverConnCF = new MinimalFuture<>(); + final NotifyingHandler handler = new NotifyingHandler(serverConnCF); + server.addHandler(handler); + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + // write data on the stream + try (final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + final OutputStream os = bidiStream.outputStream()) { + os.write("foobar".getBytes(StandardCharsets.UTF_8)); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for the handler on the server's connection to be invoked + System.out.println("waiting for the request to be handled by the server connection"); + final QuicServerConnection serverConn = serverConnCF.get(); + System.out.println("request handled by the server connection " + serverConn); + // now close the client/local connection so that it transitions to closing state + System.out.println("closing client connection " + conn); + conn.close(); + // verify connection is no longer open + assertFalse(conn.underlyingQuicConnection().isOpen(), "QUIC connection is still open"); + // now send a stateless reset from the server connection + sendStatelessResetFrom(serverConn); + // wait for the stateless reset to be processed + final Instant waitEnd = Instant.now().plus(Duration.ofSeconds(2)); + while (Instant.now().isBefore(waitEnd)) { + if (conn.endpoint().connectionCount() != 0) { + // wait for a while + Thread.sleep(10); + } + } + // now expect the endpoint to have removed the client connection. + // this isn't a fool proof verification because the connection could have been + // moved from the closing state to draining and then removed, without having processed + // the stateless reset, but we don't have any other credible way of verifying this + assertEquals(0, conn.endpoint().connectionCount(), "unexpected number of connections" + + " known to QUIC endpoint"); + } + } + + /** + * Initiates a connection between client and server. The server connection is then + * closed and the client connection thus moves to the draining state. The test then initiates + * a stateless reset from the server connection against this draining client connection. The + * test then verifies that the client connection, which was in draining state, has been + * completely removed from the endpoint upon receiving this stateless reset. + */ + @Test + public void testDrainingConnection() throws Exception { + final CompletableFuture serverConnCF = new MinimalFuture<>(); + final NotifyingHandler handler = new NotifyingHandler(serverConnCF); + server.addHandler(handler); + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + // write data on the stream + try (final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + final OutputStream os = bidiStream.outputStream()) { + os.write("foobar".getBytes(StandardCharsets.UTF_8)); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for the handler on the server's connection to be invoked + System.out.println("waiting for the request to be handled by the server connection"); + final QuicServerConnection serverConn = serverConnCF.get(); + System.out.println("request handled by the server connection " + serverConn); + // now close the server connection so that the client conn transitions to draining state + System.out.println("closing server connection " + serverConn); + // intentionally use a "unique" error to confidently verify the termination cause + final TerminationCause tc = forTransportError(NO_VIABLE_PATH) + .loggedAs("intentionally closed by server to initiate draining state" + + " on client connection"); + serverConn.connectionTerminator().terminate(tc); + // wait for client conn to terminate + final TerminationCause clientTC = ((QuicConnectionImpl) conn.underlyingQuicConnection()) + .futureTerminationCause().get(); + // verify connection closed for the right reason + assertEquals(NO_VIABLE_PATH.code(), clientTC.getCloseCode(), + "unexpected termination cause"); + // now send a stateless reset from the server connection + sendStatelessResetFrom(serverConn); + // wait for the stateless reset to be processed + final Instant waitEnd = Instant.now().plus(Duration.ofSeconds(2)); + while (Instant.now().isBefore(waitEnd)) { + if (conn.endpoint().connectionCount() != 0) { + // wait for a while + Thread.sleep(10); + } + } + // now expect the endpoint to have removed the client connection. + // this isn't a fool proof verification because the connection could have been + // removed after moving out from the draining state, without having processed the + // stateless reset, but we don't have any other credible way of verifying this + assertEquals(0, conn.endpoint().connectionCount(), "unexpected number of connections" + + " known to QUIC endpoint"); + } + } + + private static void sendStatelessResetFrom(final QuicServerConnection serverConn) + throws IOException { + final QuicConnectionId localConnId = serverConn.localConnectionId(); + final ByteBuffer resetDatagram = serverConn.endpoint().idFactory().statelessReset( + localConnId.asReadOnlyBuffer(), 43); + final InetSocketAddress targetAddr = serverConn.peerAddress(); + ((DatagramChannel) serverConn.channel()).send(resetDatagram, targetAddr); + System.out.println("sent stateless reset from server conn " + serverConn + " to " + + targetAddr); + } + + private static void assertStatelessResetTermination(final ClientConnection conn) + throws Exception { + final CompletableFuture cf = + ((QuicConnectionImpl) conn.underlyingQuicConnection()).futureTerminationCause(); + final TerminationCause tc = cf.get(); + System.out.println("got termination cause " + tc.getCloseCause() + " - " + tc.getLogMsg()); + final IOException closeCause = tc.getCloseCause(); + assertNotNull(closeCause, "close cause IOException is null"); + final String expectedMsg = "stateless reset from peer"; + if (closeCause.getMessage() != null && closeCause.getMessage().contains(expectedMsg)) { + // got expected IOException + return; + } + // unexpected IOException. throw it back + throw closeCause; + } + + private static final class NotifyingHandler implements QuicServerHandler { + + private final CompletableFuture serverConnCF; + + private NotifyingHandler(final CompletableFuture serverConnCF) { + this.serverConnCF = serverConnCF; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) { + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + this.serverConnCF.complete(conn); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/VariableLengthTest.java b/test/jdk/java/net/httpclient/quic/VariableLengthTest.java new file mode 100644 index 00000000000..aec3e999812 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/VariableLengthTest.java @@ -0,0 +1,348 @@ +/* + * 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. + * + * 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. + */ +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jtreg.SkippedException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.quic + * @run testng/othervm VariableLengthTest + * @summary Tests to check quic/util methods encode/decodeVariableLength methods + * work as expected. + */ +public class VariableLengthTest { + static final Class IAE = IllegalArgumentException.class; + + @DataProvider(name = "decode invariants") + public Object[][] decodeInvariants() { + return new Object[][] + { + { new byte[]{7}, 7, 1 }, // 00 + { new byte[]{65, 11}, 267, 2 }, // 01 + { new byte[]{-65, 11, 22, 33}, 1057691169, 4 }, // 10 + { new byte[]{-1, 11, 22, 33, 44, 55, 66, 77}, 4542748980864827981L, 8 }, // 11 + { new byte[]{-1, -11, -22, -33, -44, -55, -66, -77}, 4608848040752168627L, 8 }, + { new byte[]{}, -1, 0 }, + { new byte[]{-65}, -1, 0 }, + }; + } + @DataProvider(name = "encode invariants") + public Object[][] encodeInvariants() { + return new Object[][] + { + { 7, 1, null }, // 00 + { 267, 2, null }, // 01 + { 1057691169, 4, null }, // 10 + { 4542748980864827981L, 8, null }, // 11 + { Long.MAX_VALUE, 0, IAE }, + { -1, 0, IAE }, + }; + } + @DataProvider(name = "prefix invariants") + public Object[][] prefixInvariants() { + return new Object[][] + { + { Long.MAX_VALUE, 0, IAE }, + { 4611686018427387903L+1, 0, IAE }, + { 4611686018427387903L, 3, null }, + { 4611686018427387903L-1, 3, null }, + { 1073741823+1, 3, null }, + { 1073741823, 2, null }, // (length > (1L << 30)-1) + { 1073741823-1, 2, null }, + { 16383+1, 2, null }, + { 16383, 1, null }, // (length > (1L << 14)-1 + { 16383-1, 1, null }, + { 63+1, 1, null }, + { 63 , 0, null }, // (length > (1L << 6)-1 + { 63-1, 0, null }, + { 100, 1, null }, + { 10, 0, null }, + { 1, 0, null }, + { 0, 0, null }, // (length >= 0) + { -1, 0, IAE }, + { -10, 0, IAE }, + { -100, 0, IAE }, + { Long.MIN_VALUE, 0, IAE }, + { -4611686018427387903L-1, 0, IAE }, + { -4611686018427387903L, 0, IAE }, + { -4611686018427387903L+1, 0, IAE }, + { -1073741823-1, 0, IAE }, + { -1073741823, 0, IAE }, // (length > (1L << 30)-1) + { -1073741823+1, 0, IAE }, + { -16383-1, 0, IAE }, + { -16383, 0, IAE }, // (length > (1L << 14)-1 + { -16383+1, 0, IAE }, + { -63-1, 0, IAE }, + { -63 , 0, IAE }, // (length > (1L << 6)-1 + { -63+1, 0, IAE }, + }; + } + + @Test(dataProvider = "decode invariants") + public void testDecode(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + var actualLength = VariableLengthEncoder.decode(bb); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, expectedPosition); + } + + @Test(dataProvider = "decode invariants") + public void testPeek(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + var actualLength = VariableLengthEncoder.peekEncodedValue(bb, 0); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, 0); + } + + @Test(dataProvider = "encode invariants") + public void testEncode(long length, int capacity, Class exception) throws IOException { + var actualBuffer = ByteBuffer.allocate(capacity); + var expectedBuffer = getTestBuffer(length, capacity); + + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.encode(actualBuffer, length)); + // if method fails ensure that position hasn't changed + var actualPosition = actualBuffer.position(); + assertEquals(actualPosition, capacity); + } else { + VariableLengthEncoder.encode(actualBuffer, length); + var actualPosition = actualBuffer.position(); + assertEquals(actualPosition, capacity); + + // check length prefix + int firstByte = actualBuffer.get(0) & 0xFF; + int lengthPrefix = firstByte & 0xC0; + lengthPrefix >>= 6; + int expectedValue = (int)(Math.log(capacity) / Math.log(2)); + assertEquals(lengthPrefix, expectedValue); + + // check length encoded in buffer correctly + int b = firstByte & 0x3F; + actualBuffer.put(0, (byte) b); + assertEquals(actualBuffer.compareTo(expectedBuffer), 0); + } + } + + @Test(dataProvider = "prefix invariants") + public void testLengthPrefix(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getVariableLengthPrefix(length)); + } else { + var actualValue = VariableLengthEncoder.getVariableLengthPrefix(length); + assertEquals(actualValue, expectedPrefix); + } + } + + // Encode the given length and then decodes it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodeDecode(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + } else { + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Decode %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too short, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer shorter = ByteBuffer.allocate(actualSize - 1 + offset); + shorter.position(offset); + ByteBuffer shorterref = ByteBuffer.allocate(actualSize - 1 + offset); + shorterref.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer too short + expectThrows(IAE, () -> VariableLengthEncoder.encode(shorter, length)); + assertEquals(shorter.position(), offset); + assertEquals(shorter.limit(), shorter.capacity()); + + assertEquals(shorter.mismatch(shorterref), -1); + assertEquals(shorterref.mismatch(shorter), -1); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(offset); + shortSlice.limit(offset + actualSize -1); + var actualLength = VariableLengthEncoder.decode(shortSlice); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), offset); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + + // decode with the exact buffer + actualLength = VariableLengthEncoder.decode(exact); + assertEquals(actualLength, length); + assertEquals(exact.position(), offset + actualSize); + assertFalse(exact.hasRemaining()); + + // decode with the longer buffer + actualLength = VariableLengthEncoder.decode(longer); + assertEquals(actualLength, length); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.remaining(), 10); + } + + } + } + + // Encode the given length and then peeks it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodePeek(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + return; + } + + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Peek %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + exact.position(0); + longer.position(0); + exact.limit(exact.capacity()); + longer.limit(longer.capacity()); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(0); + shortSlice.limit(offset + actualSize - 1); + // need at least one byte to decode the size len... + var expectedSize = shortSlice.limit() <= offset ? -1 : actualSize; + assertEquals(VariableLengthEncoder.peekEncodedValueSize(shortSlice, offset), expectedSize); + var actualLength = VariableLengthEncoder.peekEncodedValue(shortSlice, offset); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), 0); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + + // decode with the exact buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(exact, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(exact, offset); + assertEquals(actualLength, length); + assertEquals(exact.position(), 0); + assertEquals(exact.limit(), exact.capacity()); + + // decode with the longer buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(longer, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(longer, offset); + assertEquals(actualLength, length); + assertEquals(longer.position(), 0); + assertEquals(longer.limit(), longer.capacity()); + } + + } + + + private ByteBuffer getTestBuffer(long length, int capacity) { + return switch (capacity) { + case 0 -> ByteBuffer.allocate(1).put((byte) length); + case 1 -> ByteBuffer.allocate(capacity).put((byte) length); + case 2 -> ByteBuffer.allocate(capacity).putShort((short) length); + case 4 -> ByteBuffer.allocate(capacity).putInt((int) length); + case 8 -> ByteBuffer.allocate(capacity).putLong(length); + default -> throw new SkippedException("bad value used for capacity"); + }; + } +} diff --git a/test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java b/test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java new file mode 100644 index 00000000000..d0de5ce9a60 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java @@ -0,0 +1,146 @@ +/* + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @summary Test the version negotiation semantics of Quic + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.internal.httpclient.debug=true VersionNegotiationTest + */ +public class VersionNegotiationTest { + + private static SSLContext sslContext; + private static ExecutorService executor; + + @BeforeClass + public static void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + } + + @AfterClass + public static void afterClass() throws Exception { + if (executor != null) executor.shutdown(); + } + + private QuicClient createClient() { + return new QuicClient.Builder() + .availableVersions(List.of(QuicVersion.QUIC_V1)) + .tlsContext(new QuicTLSContext(sslContext)) + .sslParameters(new SSLParameters()) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + /** + * Uses a Quic client which is enabled for a specific version and a Quic server + * which is enabled for a different version. Verifies that the connection attempt fails + * as noted in RFC-9000, section 6.2 + */ + @Test + public void testUnsupportedClientVersion() throws Exception { + try (final var client = createClient()) { + final QuicVersion serverVersion = QuicVersion.QUIC_V2; + try (final QuicServer server = createAndStartServer(serverVersion)) { + System.out.println("Attempting to connect " + client.getAvailableVersions() + + " client to a " + server.getAvailableVersions() + " server"); + final IOException thrown = expectThrows(IOException.class, + () -> ClientConnection.establishConnection(client, server.getAddress())); + // a version negotiation failure (since it happens during a QUIC connection + // handshake) gets thrown as a SSLHandshakeException + if (!(thrown.getCause() instanceof SSLHandshakeException sslhe)) { + throw thrown; + } + System.out.println("Received (potentially expected) exception: " + sslhe); + // additional check to make sure it was thrown for the right reason + assertEquals(sslhe.getMessage(), "QUIC connection establishment failed"); + // underlying cause of SSLHandshakeException should be version negotiation failure + final Throwable underlyingCause = sslhe.getCause(); + assertNotNull(underlyingCause, "missing cause in SSLHandshakeException"); + assertNotNull(underlyingCause.getMessage(), "missing message in " + underlyingCause); + assertTrue(underlyingCause.getMessage().contains("No support for any of the" + + " QUIC versions being negotiated")); + } + } + } + + /** + * Creates a server which supports only the specified Quic version + */ + private static QuicServer createAndStartServer(final QuicVersion version) throws IOException { + final QuicStandaloneServer server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{version}) + .sslContext(sslContext) + .build(); + server.addHandler(new ExceptionThrowingHandler()); + server.start(); + System.out.println("Quic server with version " + version + " started at " + server.getAddress()); + return server; + } + + private static final class ExceptionThrowingHandler implements QuicServerHandler { + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + throw new AssertionError("Handler shouldn't have been called for " + + bidiStream + " on connection " + conn); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security b/test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security new file mode 100644 index 00000000000..0d66b3a9e2f --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security @@ -0,0 +1,4 @@ +# meant to override the jdk.quic.tls.keyLimits security property +# in the KeyUpdateTest +jdk.quic.tls.keyLimits=AES/GCM/NoPadding 10, \ + ChaCha20-Poly1305 -1 diff --git a/test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java b/test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java new file mode 100644 index 00000000000..ed1cbc08de0 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java @@ -0,0 +1,457 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import org.testng.annotations.Test; +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.QuicTLSEngineImplAccessor; + +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLContext; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicVersion.QUIC_V1; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build java.base/sun.security.ssl.QuicTLSEngineImplAccessor + * @summary known-answer test for packet encryption and decryption + * @run testng/othervm PacketEncryptionTest + */ +public class PacketEncryptionTest { + + // RFC 9001, appendix A + private static final String INITIAL_DCID = "8394c8f03e515708"; + // section A.2 + // header includes 4-byte packet number 2 + private static final String INITIAL_C_HEADER = "c300000001088394c8f03e5157080000449e00000002"; + private static final int INITIAL_C_PAYLOAD_OFFSET = INITIAL_C_HEADER.length() / 2; + private static final int INITIAL_C_PN_OFFSET = INITIAL_C_PAYLOAD_OFFSET - 4; + private static final int INITIAL_C_PN = 2; + // payload is zero-padded to 1162 bytes, not shown here + private static final String INITIAL_C_PAYLOAD = + "060040f1010000ed0303ebf8fa56f129"+"39b9584a3896472ec40bb863cfd3e868" + + "04fe3a47f06a2b69484c000004130113"+"02010000c000000010000e00000b6578" + + "616d706c652e636f6dff01000100000a"+"00080006001d00170018001000070005" + + "04616c706e0005000501000000000033"+"00260024001d00209370b2c9caa47fba" + + "baf4559fedba753de171fa71f50f1ce1"+"5d43e994ec74d748002b000302030400" + + "0d0010000e0403050306030203080408"+"050806002d00020101001c0002400100" + + "3900320408ffffffffffffffff050480"+"00ffff07048000ffff08011001048000" + + "75300901100f088394c8f03e51570806"+"048000ffff"; + private static final int INITIAL_C_PAYLOAD_LENGTH = 1162; + private static final String ENCRYPTED_C_PAYLOAD = + "c000000001088394c8f03e5157080000"+"449e7b9aec34d1b1c98dd7689fb8ec11" + + "d242b123dc9bd8bab936b47d92ec356c"+"0bab7df5976d27cd449f63300099f399" + + "1c260ec4c60d17b31f8429157bb35a12"+"82a643a8d2262cad67500cadb8e7378c" + + "8eb7539ec4d4905fed1bee1fc8aafba1"+"7c750e2c7ace01e6005f80fcb7df6212" + + "30c83711b39343fa028cea7f7fb5ff89"+"eac2308249a02252155e2347b63d58c5" + + "457afd84d05dfffdb20392844ae81215"+"4682e9cf012f9021a6f0be17ddd0c208" + + "4dce25ff9b06cde535d0f920a2db1bf3"+"62c23e596d11a4f5a6cf3948838a3aec" + + "4e15daf8500a6ef69ec4e3feb6b1d98e"+"610ac8b7ec3faf6ad760b7bad1db4ba3" + + "485e8a94dc250ae3fdb41ed15fb6a8e5"+"eba0fc3dd60bc8e30c5c4287e53805db" + + "059ae0648db2f64264ed5e39be2e20d8"+"2df566da8dd5998ccabdae053060ae6c" + + "7b4378e846d29f37ed7b4ea9ec5d82e7"+"961b7f25a9323851f681d582363aa5f8" + + "9937f5a67258bf63ad6f1a0b1d96dbd4"+"faddfcefc5266ba6611722395c906556" + + "be52afe3f565636ad1b17d508b73d874"+"3eeb524be22b3dcbc2c7468d54119c74" + + "68449a13d8e3b95811a198f3491de3e7"+"fe942b330407abf82a4ed7c1b311663a" + + "c69890f4157015853d91e923037c227a"+"33cdd5ec281ca3f79c44546b9d90ca00" + + "f064c99e3dd97911d39fe9c5d0b23a22"+"9a234cb36186c4819e8b9c5927726632" + + "291d6a418211cc2962e20fe47feb3edf"+"330f2c603a9d48c0fcb5699dbfe58964" + + "25c5bac4aee82e57a85aaf4e2513e4f0"+"5796b07ba2ee47d80506f8d2c25e50fd" + + "14de71e6c418559302f939b0e1abd576"+"f279c4b2e0feb85c1f28ff18f58891ff" + + "ef132eef2fa09346aee33c28eb130ff2"+"8f5b766953334113211996d20011a198" + + "e3fc433f9f2541010ae17c1bf202580f"+"6047472fb36857fe843b19f5984009dd" + + "c324044e847a4f4a0ab34f719595de37"+"252d6235365e9b84392b061085349d73" + + "203a4a13e96f5432ec0fd4a1ee65accd"+"d5e3904df54c1da510b0ff20dcc0c77f" + + "cb2c0e0eb605cb0504db87632cf3d8b4"+"dae6e705769d1de354270123cb11450e" + + "fc60ac47683d7b8d0f811365565fd98c"+"4c8eb936bcab8d069fc33bd801b03ade" + + "a2e1fbc5aa463d08ca19896d2bf59a07"+"1b851e6c239052172f296bfb5e724047" + + "90a2181014f3b94a4e97d117b4381303"+"68cc39dbb2d198065ae3986547926cd2" + + "162f40a29f0c3c8745c0f50fba3852e5"+"66d44575c29d39a03f0cda721984b6f4" + + "40591f355e12d439ff150aab7613499d"+"bd49adabc8676eef023b15b65bfc5ca0" + + "6948109f23f350db82123535eb8a7433"+"bdabcb909271a6ecbcb58b936a88cd4e" + + "8f2e6ff5800175f113253d8fa9ca8885"+"c2f552e657dc603f252e1a8e308f76f0" + + "be79e2fb8f5d5fbbe2e30ecadd220723"+"c8c0aea8078cdfcb3868263ff8f09400" + + "54da48781893a7e49ad5aff4af300cd8"+"04a6b6279ab3ff3afb64491c85194aab" + + "760d58a606654f9f4400e8b38591356f"+"bf6425aca26dc85244259ff2b19c41b9" + + "f96f3ca9ec1dde434da7d2d392b905dd"+"f3d1f9af93d1af5950bd493f5aa731b4" + + "056df31bd267b6b90a079831aaf579be"+"0a39013137aac6d404f518cfd4684064" + + "7e78bfe706ca4cf5e9c5453e9f7cfd2b"+"8b4c8d169a44e55c88d4a9a7f9474241" + + "e221af44860018ab0856972e194cd934"; + // section A.3 + // header includes 2-byte packet number 1 + private static final String INITIAL_S_HEADER = "c1000000010008f067a5502a4262b50040750001"; + private static final int INITIAL_S_PAYLOAD_OFFSET = INITIAL_S_HEADER.length() / 2; + private static final int INITIAL_S_PN_OFFSET = INITIAL_S_PAYLOAD_OFFSET - 2; + private static final int INITIAL_S_PN = 1; + // complete packet, no padding + private static final String INITIAL_S_PAYLOAD = + "02000000000600405a020000560303ee"+"fce7f7b37ba1d1632e96677825ddf739" + + "88cfc79825df566dc5430b9a045a1200"+"130100002e00330024001d00209d3c94" + + "0d89690b84d08a60993c144eca684d10"+"81287c834d5311bcf32bb9da1a002b00" + + "020304"; + private static final int INITIAL_S_PAYLOAD_LENGTH = INITIAL_S_PAYLOAD.length() / 2; + private static final String ENCRYPTED_S_PAYLOAD = + "cf000000010008f067a5502a4262b500"+"4075c0d95a482cd0991cd25b0aac406a" + + "5816b6394100f37a1c69797554780bb3"+"8cc5a99f5ede4cf73c3ec2493a1839b3" + + "dbcba3f6ea46c5b7684df3548e7ddeb9"+"c3bf9c73cc3f3bded74b562bfb19fb84" + + "022f8ef4cdd93795d77d06edbb7aaf2f"+"58891850abbdca3d20398c276456cbc4" + + "2158407dd074ee"; + + // section A.4 + private static final String SIGNED_RETRY = + "ff000000010008f067a5502a4262b574"+"6f6b656e04a265ba2eff4d829058fb3f" + + "0f2496ba"; + + // section A.5 + public static final String ONERTT_SECRET = "9ac312a7f877468ebe69422748ad00a1" + + "5443f18203a07d6060f688f30f21632b"; + private static final String ONERTT_HEADER = "4200bff4"; + private static final int ONERTT_PAYLOAD_OFFSET = ONERTT_HEADER.length() / 2; + private static final int ONERTT_PN_OFFSET = 1; + private static final int ONERTT_PN = 654360564; + // payload is zero-padded to 1162 bytes, not shown here + private static final String ONERTT_PAYLOAD = + "01"; + private static final int ONERTT_PAYLOAD_LENGTH = + ONERTT_PAYLOAD.length() / 2; + private static final String ENCRYPTED_ONERTT_PAYLOAD = + "4cfe4189655e5cd55c41f69080575d7999c25a5bfb"; + + private static final class FixedHeaderContent implements IntFunction { + private final ByteBuffer header; + private FixedHeaderContent(ByteBuffer header) { + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyphase) { + // ignore keyphase + return this.header; + } + } + + @Test + public void testEncryptClientInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V1, dcid); + + final int packetLen = INITIAL_C_PAYLOAD_OFFSET + INITIAL_C_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_C_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_C_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_C_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_C_PAYLOAD_OFFSET, INITIAL_C_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_C_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, clientEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_C_PAYLOAD); + } + + @Test + public void testDecryptClientInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV1Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V1, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, + src, INITIAL_C_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_C_HEADER + INITIAL_C_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptClientInitialPacketBadTag() throws Exception { + QuicTLSEngine serverEngine = getQuicV1Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V1, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, + src, INITIAL_C_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + @Test + public void testEncryptServerInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV1Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V1, dcid); + + final int packetLen = INITIAL_S_PAYLOAD_OFFSET + INITIAL_S_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_S_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_S_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_S_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_S_PAYLOAD_OFFSET, INITIAL_S_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_S_PAYLOAD_OFFSET); + serverEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, serverEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_S_PAYLOAD); + } + + @Test + public void testDecryptServerInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V1, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, + src, INITIAL_S_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_S_HEADER + INITIAL_S_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptServerInitialPacketTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V1, dcid); + + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + // must not throw + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + } + + @Test + public void testSignRetry() throws NoSuchAlgorithmException, ShortBufferException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.allocate(SIGNED_RETRY.length() / 2); + packet.put(HexFormat.of().parseHex(SIGNED_RETRY), 0, SIGNED_RETRY.length() / 2 - 16); + + ByteBuffer src = packet.asReadOnlyBuffer(); + src.limit(src.position()); + src.position(0); + + clientEngine.signRetryPacket(QUIC_V1, dcid, src, packet); + + assertEquals(HexFormat.of().formatHex(packet.array()), SIGNED_RETRY); + } + + @Test + public void testVerifyRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + clientEngine.verifyRetryPacket(QUIC_V1, dcid, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testVerifyBadRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + clientEngine.verifyRetryPacket(QUIC_V1, dcid, packet); + fail("Verification should have failed"); + } + + @Test + public void testEncryptChaCha() throws Exception { + QuicTLSEngineImpl clientEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), true); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, clientEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", true); + + final int packetLen = ONERTT_PAYLOAD_OFFSET + ONERTT_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(ONERTT_HEADER)); + packet.put(HexFormat.of().parseHex(ONERTT_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, ONERTT_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(ONERTT_PAYLOAD_OFFSET, ONERTT_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(ONERTT_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN , new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, clientEngine, 0x1f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_ONERTT_PAYLOAD); + } + + @Test + public void testDecryptChaCha() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, (byte) 0, + src, ONERTT_PAYLOAD_OFFSET, packet); + + String expectedContents = ONERTT_HEADER + ONERTT_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptChaChaTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + final int keyPhase = 0; + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyPhase, src, ONERTT_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + // must not throw + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyPhase, src, ONERTT_PAYLOAD_OFFSET, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptChaChaBadTag() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, (byte) 0, + src, ONERTT_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + + private void protect(QuicTLSEngine.KeySpace space, ByteBuffer buffer, + int packetNumberStart, int packetNumberLength, QuicTLSEngine tlsEngine, + int headersMask) throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(space, false, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void unprotect(QuicTLSEngine.KeySpace keySpace, ByteBuffer buffer, + int packetNumberStart, int packetNumberLength, + QuicTLSEngine tlsEngine, int headersMask) throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(keySpace, true, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void maskPacketNumber(ByteBuffer buffer, int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + // returns a QuicTLSEngine with only Quic version 1 enabled + private QuicTLSEngine getQuicV1Engine(SSLContext context, boolean mode) { + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicTLSEngine engine = quicTLSContext.createEngine(); + engine.setUseClientMode(mode); + return engine; + } +} diff --git a/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java new file mode 100644 index 00000000000..e6a99caebad --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java @@ -0,0 +1,122 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build jdk.test.lib.net.SimpleSSLContext + * @summary Verify that QuicTransportExceptions thrown by transport parameter consumer + * are propagated to the QuicTLSEngine user + * @run junit/othervm QuicTLSEngineBadParametersTest + */ +public class QuicTLSEngineBadParametersTest { + + @Test + void testServerConsumerExceptionPropagated() throws IOException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + QuicTransportException ex = + new QuicTransportException("", null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + serverEngine.setRemoteQuicTransportParametersConsumer(p -> { throw ex; }); + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + try { + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertSame(ex, e, "Incorrect exception caught"); + } + } + + @Test + void testClientConsumerExceptionPropagated() throws IOException, QuicTransportException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + QuicTransportException ex = + new QuicTransportException("", null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + clientEngine.setRemoteQuicTransportParametersConsumer(p -> { throw ex; }); + + // client hello + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + assertFalse(cTOs.hasRemaining()); + // server hello + ByteBuffer sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, sTOc); + assertFalse(sTOc.hasRemaining()); + // encrypted extensions + sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE); + try { + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE, sTOc); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertSame(ex, e, "Incorrect exception caught"); + } + + } + + private static QuicTLSEngine createServerEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine(); + engine.setUseClientMode(false); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + + private static QuicTLSEngine createClientEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine("localhost", 1234); + engine.setUseClientMode(true); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + +} diff --git a/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java new file mode 100644 index 00000000000..fa7df3bc5c3 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java @@ -0,0 +1,110 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build jdk.test.lib.net.SimpleSSLContext + * @summary Verify that a missing ALPN extension results in no_application_protocol alert + * @run junit/othervm QuicTLSEngineFailedALPNTest + */ +public class QuicTLSEngineFailedALPNTest { + + @Test + void testServerRequiresALPN() throws IOException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + try { + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x0178, e.getErrorCode(), "Unexpected error code"); + } + } + + @Test + void testClientRequiresALPN() throws IOException, QuicTransportException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + SSLParameters params = clientEngine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + clientEngine.setSSLParameters(params); + // client hello + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + assertFalse(cTOs.hasRemaining()); + // server hello + ByteBuffer sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, sTOc); + assertFalse(sTOc.hasRemaining()); + // encrypted extensions + sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE); + try { + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE, sTOc); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x0178, e.getErrorCode(), "Unexpected error code"); + } + + } + + private static QuicTLSEngine createServerEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine(); + engine.setUseClientMode(false); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(params-> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + + private static QuicTLSEngine createClientEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine("localhost", 1234); + engine.setUseClientMode(true); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(params-> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + +} diff --git a/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.java b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.java new file mode 100644 index 00000000000..da9f06785a2 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.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. + * + * 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. + */ + +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build jdk.test.lib.net.SimpleSSLContext + * @summary Verify that a missing transport parameters extension results in missing_extension alert + * @run junit/othervm QuicTLSEngineMissingParametersTest + */ +public class QuicTLSEngineMissingParametersTest { + + @Test + void testServerRequiresTransportParameters() throws IOException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + try { + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x016d, e.getErrorCode(), "Unexpected error code"); + } + } + + @Test + void testClientRequiresTransportParameters() throws IOException, QuicTransportException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + clientEngine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + // client hello + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + assertFalse(cTOs.hasRemaining()); + // server hello + ByteBuffer sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, sTOc); + assertFalse(sTOc.hasRemaining()); + // encrypted extensions + sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE); + try { + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE, sTOc); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x016d, e.getErrorCode(), "Unexpected error code"); + } + + } + + private static QuicTLSEngine createServerEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine(); + engine.setUseClientMode(false); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + + private static QuicTLSEngine createClientEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine("localhost", 1234); + engine.setUseClientMode(true); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + +} diff --git a/test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java b/test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java new file mode 100644 index 00000000000..a506495ed5e --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java @@ -0,0 +1,451 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import org.testng.annotations.Test; +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.QuicTLSEngineImplAccessor; + +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLContext; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicVersion.QUIC_V2; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build java.base/sun.security.ssl.QuicTLSEngineImplAccessor + * @summary known-answer test for packet encryption and decryption with Quic v2 + * @run testng/othervm Quicv2PacketEncryptionTest + */ +public class Quicv2PacketEncryptionTest { + + // RFC 9369, appendix A + private static final String INITIAL_DCID = "8394c8f03e515708"; + // section A.2 + // header includes 4-byte packet number 2 + private static final String INITIAL_C_HEADER = "d36b3343cf088394c8f03e5157080000449e00000002"; + private static final int INITIAL_C_PAYLOAD_OFFSET = INITIAL_C_HEADER.length() / 2; + private static final int INITIAL_C_PN_OFFSET = INITIAL_C_PAYLOAD_OFFSET - 4; + private static final int INITIAL_C_PN = 2; + // payload is zero-padded to 1162 bytes, not shown here + private static final String INITIAL_C_PAYLOAD = + "060040f1010000ed0303ebf8fa56f129"+"39b9584a3896472ec40bb863cfd3e868" + + "04fe3a47f06a2b69484c000004130113"+"02010000c000000010000e00000b6578" + + "616d706c652e636f6dff01000100000a"+"00080006001d00170018001000070005" + + "04616c706e0005000501000000000033"+"00260024001d00209370b2c9caa47fba" + + "baf4559fedba753de171fa71f50f1ce1"+"5d43e994ec74d748002b000302030400" + + "0d0010000e0403050306030203080408"+"050806002d00020101001c0002400100" + + "3900320408ffffffffffffffff050480"+"00ffff07048000ffff08011001048000" + + "75300901100f088394c8f03e51570806"+"048000ffff"; + private static final int INITIAL_C_PAYLOAD_LENGTH = 1162; + private static final String ENCRYPTED_C_PAYLOAD = + "d76b3343cf088394c8f03e5157080000"+"449ea0c95e82ffe67b6abcdb4298b485" + + "dd04de806071bf03dceebfa162e75d6c"+"96058bdbfb127cdfcbf903388e99ad04" + + "9f9a3dd4425ae4d0992cfff18ecf0fdb"+"5a842d09747052f17ac2053d21f57c5d" + + "250f2c4f0e0202b70785b7946e992e58"+"a59ac52dea6774d4f03b55545243cf1a" + + "12834e3f249a78d395e0d18f4d766004"+"f1a2674802a747eaa901c3f10cda5500" + + "cb9122faa9f1df66c392079a1b40f0de"+"1c6054196a11cbea40afb6ef5253cd68" + + "18f6625efce3b6def6ba7e4b37a40f77"+"32e093daa7d52190935b8da58976ff33" + + "12ae50b187c1433c0f028edcc4c2838b"+"6a9bfc226ca4b4530e7a4ccee1bfa2a3" + + "d396ae5a3fb512384b2fdd851f784a65"+"e03f2c4fbe11a53c7777c023462239dd" + + "6f7521a3f6c7d5dd3ec9b3f233773d4b"+"46d23cc375eb198c63301c21801f6520" + + "bcfb7966fc49b393f0061d974a2706df"+"8c4a9449f11d7f3d2dcbb90c6b877045" + + "636e7c0c0fe4eb0f697545460c806910"+"d2c355f1d253bc9d2452aaa549e27a1f" + + "ac7cf4ed77f322e8fa894b6a83810a34"+"b361901751a6f5eb65a0326e07de7c12" + + "16ccce2d0193f958bb3850a833f7ae43"+"2b65bc5a53975c155aa4bcb4f7b2c4e5" + + "4df16efaf6ddea94e2c50b4cd1dfe060"+"17e0e9d02900cffe1935e0491d77ffb4" + + "fdf85290fdd893d577b1131a610ef6a5"+"c32b2ee0293617a37cbb08b847741c3b" + + "8017c25ca9052ca1079d8b78aebd4787"+"6d330a30f6a8c6d61dd1ab5589329de7" + + "14d19d61370f8149748c72f132f0fc99"+"f34d766c6938597040d8f9e2bb522ff9" + + "9c63a344d6a2ae8aa8e51b7b90a4a806"+"105fcbca31506c446151adfeceb51b91" + + "abfe43960977c87471cf9ad4074d30e1"+"0d6a7f03c63bd5d4317f68ff325ba3bd" + + "80bf4dc8b52a0ba031758022eb025cdd"+"770b44d6d6cf0670f4e990b22347a7db" + + "848265e3e5eb72dfe8299ad7481a4083"+"22cac55786e52f633b2fb6b614eaed18" + + "d703dd84045a274ae8bfa73379661388"+"d6991fe39b0d93debb41700b41f90a15" + + "c4d526250235ddcd6776fc77bc97e7a4"+"17ebcb31600d01e57f32162a8560cacc" + + "7e27a096d37a1a86952ec71bd89a3e9a"+"30a2a26162984d7740f81193e8238e61" + + "f6b5b984d4d3dfa033c1bb7e4f0037fe"+"bf406d91c0dccf32acf423cfa1e70710" + + "10d3f270121b493ce85054ef58bada42"+"310138fe081adb04e2bd901f2f13458b" + + "3d6758158197107c14ebb193230cd115"+"7380aa79cae1374a7c1e5bbcb80ee23e" + + "06ebfde206bfb0fcbc0edc4ebec30966"+"1bdd908d532eb0c6adc38b7ca7331dce" + + "8dfce39ab71e7c32d318d136b6100671"+"a1ae6a6600e3899f31f0eed19e3417d1" + + "34b90c9058f8632c798d4490da498730"+"7cba922d61c39805d072b589bd52fdf1" + + "e86215c2d54e6670e07383a27bbffb5a"+"ddf47d66aa85a0c6f9f32e59d85a44dd" + + "5d3b22dc2be80919b490437ae4f36a0a"+"e55edf1d0b5cb4e9a3ecabee93dfc6e3" + + "8d209d0fa6536d27a5d6fbb17641cde2"+"7525d61093f1b28072d111b2b4ae5f89" + + "d5974ee12e5cf7d5da4d6a31123041f3"+"3e61407e76cffcdcfd7e19ba58cf4b53" + + "6f4c4938ae79324dc402894b44faf8af"+"bab35282ab659d13c93f70412e85cb19" + + "9a37ddec600545473cfb5a05e08d0b20"+"9973b2172b4d21fb69745a262ccde96b" + + "a18b2faa745b6fe189cf772a9f84cbfc"; + // section A.3 + // header includes 2-byte packet number 1 + private static final String INITIAL_S_HEADER = "d16b3343cf0008f067a5502a4262b50040750001"; + private static final int INITIAL_S_PAYLOAD_OFFSET = INITIAL_S_HEADER.length() / 2; + private static final int INITIAL_S_PN_OFFSET = INITIAL_S_PAYLOAD_OFFSET - 2; + private static final int INITIAL_S_PN = 1; + // complete packet, no padding + private static final String INITIAL_S_PAYLOAD = + "02000000000600405a020000560303ee"+"fce7f7b37ba1d1632e96677825ddf739" + + "88cfc79825df566dc5430b9a045a1200"+"130100002e00330024001d00209d3c94" + + "0d89690b84d08a60993c144eca684d10"+"81287c834d5311bcf32bb9da1a002b00" + + "020304"; + private static final int INITIAL_S_PAYLOAD_LENGTH = INITIAL_S_PAYLOAD.length() / 2; + private static final String ENCRYPTED_S_PAYLOAD = + "dc6b3343cf0008f067a5502a4262b500"+"4075d92faaf16f05d8a4398c47089698" + + "baeea26b91eb761d9b89237bbf872630"+"17915358230035f7fd3945d88965cf17" + + "f9af6e16886c61bfc703106fbaf3cb4c"+"fa52382dd16a393e42757507698075b2" + + "c984c707f0a0812d8cd5a6881eaf21ce"+"da98f4bd23f6fe1a3e2c43edd9ce7ca8" + + "4bed8521e2e140"; + + // section A.4 + private static final String SIGNED_RETRY = + "cf6b3343cf0008f067a5502a4262b574"+"6f6b656ec8646ce8bfe33952d9555436" + + "65dcc7b6"; + + // section A.5 + public static final String ONERTT_SECRET = "9ac312a7f877468ebe69422748ad00a1" + + "5443f18203a07d6060f688f30f21632b"; + private static final String ONERTT_HEADER = "4200bff4"; + private static final int ONERTT_PAYLOAD_OFFSET = ONERTT_HEADER.length() / 2; + private static final int ONERTT_PN_OFFSET = 1; + private static final int ONERTT_PN = 654360564; + // payload is zero-padded to 1162 bytes, not shown here + private static final String ONERTT_PAYLOAD = + "01"; + private static final int ONERTT_PAYLOAD_LENGTH = + ONERTT_PAYLOAD.length() / 2; + private static final String ENCRYPTED_ONERTT_PAYLOAD = + "5558b1c60ae7b6b932bc27d786f4bc2bb20f2162ba"; + + private static final class FixedHeaderContent implements IntFunction { + private final ByteBuffer header; + private FixedHeaderContent(ByteBuffer header) { + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyphase) { + // ignore keyphase + return this.header; + } + } + + @Test + public void testEncryptClientInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V2, dcid); + final int packetLen = INITIAL_C_PAYLOAD_OFFSET + INITIAL_C_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_C_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_C_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_C_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_C_PAYLOAD_OFFSET, INITIAL_C_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_C_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, clientEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_C_PAYLOAD); + } + + @Test + public void testDecryptClientInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV2Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V2, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, src, INITIAL_C_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_C_HEADER + INITIAL_C_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptClientInitialPacketBadTag() throws Exception { + QuicTLSEngine serverEngine = getQuicV2Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V2, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, src, INITIAL_C_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + @Test + public void testEncryptServerInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV2Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V2, dcid); + + final int packetLen = INITIAL_S_PAYLOAD_OFFSET + INITIAL_S_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_S_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_S_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_S_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_S_PAYLOAD_OFFSET, INITIAL_S_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_S_PAYLOAD_OFFSET); + serverEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, serverEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_S_PAYLOAD); + } + + @Test + public void testDecryptServerInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V2, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_S_HEADER + INITIAL_S_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptServerInitialPacketTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V2, dcid); + + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + // must not throw + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + } + + @Test + public void testSignRetry() throws NoSuchAlgorithmException, ShortBufferException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.allocate(SIGNED_RETRY.length() / 2); + packet.put(HexFormat.of().parseHex(SIGNED_RETRY), 0, SIGNED_RETRY.length() / 2 - 16); + + ByteBuffer src = packet.asReadOnlyBuffer(); + src.limit(src.position()); + src.position(0); + + clientEngine.signRetryPacket(QUIC_V2, dcid, src, packet); + + assertEquals(HexFormat.of().formatHex(packet.array()), SIGNED_RETRY); + } + + @Test + public void testVerifyRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + clientEngine.verifyRetryPacket(QUIC_V2, dcid, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testVerifyBadRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + clientEngine.verifyRetryPacket(QUIC_V2, dcid, packet); + fail("Verification should have failed"); + } + + @Test + public void testEncryptChaCha() throws Exception { + QuicTLSEngineImpl clientEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), true); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, clientEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", true); + + final int packetLen = ONERTT_PAYLOAD_OFFSET + ONERTT_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(ONERTT_HEADER)); + packet.put(HexFormat.of().parseHex(ONERTT_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, ONERTT_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(ONERTT_PAYLOAD_OFFSET, ONERTT_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(ONERTT_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN , new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, clientEngine, 0x1f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_ONERTT_PAYLOAD); + } + + @Test + public void testDecryptChaCha() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, 0, src, ONERTT_PAYLOAD_OFFSET, packet); + + String expectedContents = ONERTT_HEADER + ONERTT_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptChaChaTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + final int keyphase = 0; + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyphase, src, ONERTT_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + // must not throw + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyphase, src, ONERTT_PAYLOAD_OFFSET, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptChaChaBadTag() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, 0, src, ONERTT_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + + private void protect(QuicTLSEngine.KeySpace space, ByteBuffer buffer, int packetNumberStart, + int packetNumberLength, QuicTLSEngine tlsEngine, int headersMask) + throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(space, false, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void unprotect(QuicTLSEngine.KeySpace keySpace, ByteBuffer buffer, int packetNumberStart, + int packetNumberLength, QuicTLSEngine tlsEngine, int headersMask) + throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(keySpace, true, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void maskPacketNumber(ByteBuffer buffer, int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + // returns a QuicTLSEngine with only Quic version 2 enabled + private QuicTLSEngine getQuicV2Engine(SSLContext context, boolean mode) { + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicTLSEngine engine = quicTLSContext.createEngine(); + engine.setUseClientMode(mode); + return engine; + } +} diff --git a/test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java b/test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java new file mode 100644 index 00000000000..e9a57310e0b --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java @@ -0,0 +1,59 @@ +/* + * 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. + * + * 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 javax.crypto.SecretKey; +import java.io.IOException; +import java.lang.reflect.Field; +import java.security.GeneralSecurityException; + +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicVersion; + +public final class QuicTLSEngineImplAccessor { + // visible for testing + public static void testDeriveOneRTTKeys(QuicVersion version, + QuicTLSEngineImpl engine, + SecretKey client_application_traffic_secret_0, + SecretKey server_application_traffic_secret_0, + String negotiatedCipherSuite, + boolean clientMode) + throws IOException, GeneralSecurityException + { + engine.deriveOneRTTKeys(version, client_application_traffic_secret_0, + server_application_traffic_secret_0, + CipherSuite.valueOf(negotiatedCipherSuite), + clientMode); + } + + // visible for testing + public static void completeHandshake(final QuicTLSEngineImpl engine) { + try { + final Field f = QuicTLSEngineImpl.class.getDeclaredField("handshakeState"); + f.setAccessible(true); + f.set(engine, QuicTLSEngine.HandshakeState.HANDSHAKE_CONFIRMED); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/test/jdk/java/net/httpclient/ssltest/CertificateTest.java b/test/jdk/java/net/httpclient/ssltest/CertificateTest.java index 96ba5c05ed3..1d0502c9ce3 100644 --- a/test/jdk/java/net/httpclient/ssltest/CertificateTest.java +++ b/test/jdk/java/net/httpclient/ssltest/CertificateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -39,8 +39,9 @@ import jdk.test.lib.security.SSLContextBuilder; /* * @test - * @library /test/lib - * @build Server CertificateTest + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build Server CertificateTest jdk.httpclient.test.lib.common.TestServerConfigurator + * @modules java.net.http/jdk.internal.net.http.common * @run main/othervm CertificateTest GOOD_CERT expectSuccess * @run main/othervm CertificateTest BAD_CERT expectFailure * @run main/othervm diff --git a/test/jdk/java/net/httpclient/ssltest/Server.java b/test/jdk/java/net/httpclient/ssltest/Server.java index fc954016991..d2d98268e14 100644 --- a/test/jdk/java/net/httpclient/ssltest/Server.java +++ b/test/jdk/java/net/httpclient/ssltest/Server.java @@ -22,6 +22,7 @@ */ import com.sun.net.httpserver.*; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import java.io.*; import java.net.InetAddress; @@ -44,9 +45,9 @@ public class Server { // response with a short text string. public Server(SSLContext ctx) throws Exception { initLogger(); - Configurator cfg = new Configurator(ctx); InetSocketAddress addr = new InetSocketAddress( InetAddress.getLoopbackAddress(), 0); + Configurator cfg = new Configurator(addr.getAddress(), ctx); server = HttpsServer.create(addr, 10); server.setHttpsConfigurator(cfg); server.createContext("/", new MyHandler()); @@ -94,15 +95,20 @@ public class Server { } class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) throws Exception { + private final InetAddress serverAddr; + + public Configurator(InetAddress addr, SSLContext ctx) throws Exception { super(ctx); + this.serverAddr = addr; } + @Override public void configure(HttpsParameters params) { SSLParameters p = getSSLContext().getDefaultSSLParameters(); for (String cipher : p.getCipherSuites()) System.out.println("Cipher: " + cipher); System.err.println("Params = " + p); + TestServerConfigurator.addSNIMatcher(this.serverAddr, p); params.setSSLParameters(p); } } diff --git a/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java b/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java index 322a3e3789b..2e364461793 100644 --- a/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java +++ b/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.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 @@ -32,14 +32,19 @@ import jdk.test.lib.net.URIBuilder; import jdk.test.lib.security.KeyEntry; import jdk.test.lib.security.KeyStoreUtils; import jdk.test.lib.security.SSLContextBuilder; + +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 java.net.http.HttpResponse.BodyHandlers.ofString; import static java.net.http.HttpClient.Builder.NO_PROXY; /* * @test * @bug 8239594 8239595 - * @library /test/lib - * @build Server TlsVersionTest + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build Server TlsVersionTest jdk.httpclient.test.lib.common.TestServerConfigurator + * @modules java.net.http/jdk.internal.net.http.common * @run main/othervm * -Djdk.internal.httpclient.disableHostnameVerification * TlsVersionTest false @@ -121,11 +126,12 @@ public class TlsVersionTest { System.out.println("Making request to " + serverURI.getPath()); SSLContext ctx = getClientSSLContext(cert); HttpClient client = HttpClient.newBuilder() + .version(HTTP_2) .proxy(NO_PROXY) .sslContext(ctx) .build(); - for (var version : List.of(HttpClient.Version.HTTP_2, HttpClient.Version.HTTP_1_1)) { + for (var version : List.of(HTTP_3, HTTP_2, HTTP_1_1)) { HttpRequest request = HttpRequest.newBuilder(serverURI) .version(version) .GET() diff --git a/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java b/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java index 5065563c9ec..6d242a596e7 100644 --- a/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java +++ b/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java @@ -205,4 +205,3 @@ public class HandshakeUrlEncodingTest { } } } - diff --git a/test/jdk/java/net/httpclient/websocket/ReaderDriver.java b/test/jdk/java/net/httpclient/websocket/ReaderDriver.java index 61829a5a010..0b874792b4f 100644 --- a/test/jdk/java/net/httpclient/websocket/ReaderDriver.java +++ b/test/jdk/java/net/httpclient/websocket/ReaderDriver.java @@ -25,6 +25,6 @@ * @test * @bug 8159053 * @modules java.net.http/jdk.internal.net.http.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/jdk.internal.net.http.websocket.ReaderTest + * @run testng/othervm/timeout=240 --add-reads java.net.http=ALL-UNNAMED java.net.http/jdk.internal.net.http.websocket.ReaderTest */ public final class ReaderDriver { } diff --git a/test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java b/test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java new file mode 100644 index 00000000000..949c950fd76 --- /dev/null +++ b/test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java @@ -0,0 +1,239 @@ +/* + * 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. + * + * 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. + */ + +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.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.http2.BodyOutputStream; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestServerConnection; +import jdk.internal.net.http.AltServicesRegistry; +import jdk.internal.net.http.HttpClientAccess; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.frame.AltSvcFrame; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; +import static java.net.http.HttpResponse.BodyHandlers.ofString; +import static jdk.internal.net.http.AltServicesRegistry.AltService; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/* + * @test + * @summary This test verifies alt-svc registry updation for frames + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build java.net.http/jdk.internal.net.http.HttpClientAccess + * jdk.httpclient.test.lib.http2.Http2TestServer + * @modules java.net.http/jdk.internal.net.http + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.util + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.logging + * java.base/sun.net.www.http + * java.base/sun.net.www + * java.base/sun.net + * @run testng/othervm + * -Dtest.requiresHost=true + * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.internal.httpclient.disableHostnameVerification + * -Djdk.internal.httpclient.debug=true + * AltSvcFrameTest + */ + + +public class AltSvcFrameTest { + + private static final String IGNORED_HOST = "www.should-be-ignored.com"; + private static final String ALT_SVC_TO_IGNORE = "h3=\"" + IGNORED_HOST + ":443\""; + + private static final String ACCEPTED_HOST = "www.example.com"; + private static final String ALT_SVC_TO_ACCEPT = "h3=\"" + ACCEPTED_HOST + ":443\""; + + private static final String STREAM_0_ACCEPTED_HOST = "jdk.java.net"; + private static final String STREAM_0_ALT_SVC_TO_ACCEPT = "h3=\"" + STREAM_0_ACCEPTED_HOST + ":443\""; + + private static final String FOO_BAR_ORIGIN = "https://www.foo-bar.hello-world:443"; + + static Http2TestServer https2Server; + static String https2URI; + static HttpClient client; + static SSLContext server; + + @BeforeTest + public void setUp() throws Exception { + server = SimpleSSLContext.getContext("TLS"); + getRegistry(); + https2Server = new Http2TestServer("localhost", true, server); + https2Server.addHandler(new AltSvcFrameTestHandler(), "/"); + https2Server.setExchangeSupplier(AltSvcFrameTest.CFTHttp2TestExchange::new); + https2Server.start(); + https2URI = "https://" + https2Server.serverAuthority() + "/"; + + + } + + static AltServicesRegistry getRegistry() { + client = HttpClient.newBuilder() + .sslContext(server) + .version(HttpClient.Version.HTTP_2) + .build(); + return HttpClientAccess.getRegistry(client); + } + + /* + * Verify handling of alt-svc frame on a stream other than stream 0 + */ + @Test + public void testNonStream0AltSvcFrame() throws URISyntaxException, IOException, InterruptedException { + AltServicesRegistry registry = getRegistry(); + HttpRequest request = HttpRequest.newBuilder(new URI(https2URI)) + .GET() + .build(); + HttpResponse response = client.send(request, ofString()); + assertEquals(response.statusCode(), 200, "unexpected response code"); + final List services = registry.lookup(URI.create(https2URI), "h3").toList(); + System.out.println("Alt services in registry for " + https2URI + " = " + services); + final boolean hasExpectedAltSvc = services.stream().anyMatch( + alt -> alt.alpn().equals("h3") + && alt.host().contains(ACCEPTED_HOST)); + assertTrue(hasExpectedAltSvc, "missing entry in alt service registry for origin: " + https2URI); + } + + /* + * Verify handling of alt-svc frame on stream 0 of the connection + */ + @Test + public void testStream0AltSvcFrame() throws URISyntaxException, IOException, InterruptedException { + AltServicesRegistry registry = getRegistry(); + HttpRequest request = HttpRequest.newBuilder(new URI(https2URI + "?altsvc-on-stream-0")) + .GET() + .build(); + HttpResponse response = client.send(request, ofString()); + assertEquals(response.statusCode(), 200, "unexpected response code"); + final List services = registry.lookup( + URI.create(FOO_BAR_ORIGIN), "h3").toList(); + System.out.println("Alt services in registry for " + FOO_BAR_ORIGIN + " = " + services); + final boolean containsIgnoredHost = services.stream().anyMatch( + alt -> alt.alpn().equals("h3") + && alt.host().contains(IGNORED_HOST)); + assertFalse(containsIgnoredHost, "unexpected alt service in the registry for origin: " + + FOO_BAR_ORIGIN); + + final List svcs = registry.lookup(URI.create(https2URI), "h3").toList(); + System.out.println("Alt services in registry for " + https2URI + " = " + svcs); + final boolean hasExpectedAltSvc = svcs.stream().anyMatch( + alt -> alt.alpn().equals("h3") + && alt.host().contains(STREAM_0_ACCEPTED_HOST)); + assertTrue(hasExpectedAltSvc, "missing entry in alt service registry for origin: " + https2URI); + } + + static class AltSvcFrameTestHandler implements Http2Handler { + + @Override + public void handle(Http2TestExchange t) throws IOException { + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + byte[] bytes = is.readAllBytes(); + t.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } + } + + // A custom Http2TestExchangeImpl that overrides sendResponseHeaders to + // allow headers to be sent with AltSvcFrame. + static class CFTHttp2TestExchange extends Http2TestExchangeImpl { + + CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders, + HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, + SSLSession sslSession, BodyOutputStream os, + Http2TestServerConnection conn, boolean pushAllowed) { + super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, + os, conn, pushAllowed); + + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + final String reqQuery = getRequestURI().getQuery(); + if (reqQuery != null && reqQuery.contains("altsvc-on-stream-0")) { + final InetSocketAddress addr = this.getLocalAddress(); + final String connectionOrigin = "https://" + addr.getAddress().getHostAddress() + + ":" + addr.getPort(); + // send one alt-svc on stream 0 with the same Origin as the connection's Origin + enqueueAltSvcFrame(new AltSvcFrame(0, 0, + // the Origin for which the alt-svc is being advertised + Optional.of(connectionOrigin), + STREAM_0_ALT_SVC_TO_ACCEPT)); + + // send one alt-svc on stream 0 with a different Origin than the connection's Origin + enqueueAltSvcFrame(new AltSvcFrame(0, 0, + // the Origin for which the alt-svc is being advertised + Optional.of(FOO_BAR_ORIGIN), + ALT_SVC_TO_IGNORE)); + } else { + // send alt-svc on non-zero stream id. + // for non-zero stream id, as per spec, the origin is inferred from the stream's origin + // by the HTTP client + enqueueAltSvcFrame(new AltSvcFrame(streamid, 0, Optional.empty(), ALT_SVC_TO_ACCEPT)); + } + super.sendResponseHeaders(rCode, responseLength); + System.out.println("Sent response headers " + rCode); + } + + private void enqueueAltSvcFrame(final AltSvcFrame frame) throws IOException { + System.out.println("enqueueing Alt-Svc frame: " + frame); + conn.addToOutputQ(frame); + } + } +} diff --git a/test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java b/test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java new file mode 100644 index 00000000000..47e69338715 --- /dev/null +++ b/test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020, 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. + * + * 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. + */ + +import jdk.internal.net.http.HttpClientAccess; +import jdk.internal.net.http.AltServicesRegistry; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + +import static jdk.internal.net.http.AltServicesRegistry.AltService; +import static java.net.http.HttpResponse.BodyHandlers.ofString; + +/* + * @test + * @summary This test verifies alt-svc registry updates + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build java.net.http/jdk.internal.net.http.HttpClientAccess + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http2.Http2TestServer + * @modules java.net.http/jdk.internal.net.http + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.logging + * java.base/sun.net.www.http + * java.base/sun.net.www + * java.base/sun.net + * java.base/jdk.internal.util + * @run testng/othervm + * -Dtest.requiresHost=true + * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.internal.httpclient.disableHostnameVerification + * -Djdk.internal.httpclient.debug=true + * AltSvcRegistryTest + */ + + +public class AltSvcRegistryTest implements HttpServerAdapters { + + static HttpTestServer https2Server; + static String https2URI; + static HttpClient client; + static SSLContext server; + + @BeforeTest + public void setUp() throws Exception { + server = SimpleSSLContext.getContext("TLS"); + getRegistry(); + final ExecutorService executor = Executors.newCachedThreadPool(); + https2Server = HttpServerAdapters.HttpTestServer.of( + new Http2TestServer("localhost", true, 0, executor, 50, null, server, true)); + https2Server.addHandler(new AltSvcRegistryTestHandler("https", https2Server), "/"); + https2Server.start(); + https2URI = "https://" + https2Server.serverAuthority() + "/"; + + + } + + static AltServicesRegistry getRegistry() { + client = HttpClient.newBuilder() + .sslContext(server) + .version(HttpClient.Version.HTTP_2) + .build(); + return HttpClientAccess.getRegistry(client); + } + @Test + public void testAltSvcRegistry() throws URISyntaxException, IOException, InterruptedException { + AltServicesRegistry registry = getRegistry(); + HttpRequest request = HttpRequest.newBuilder(new URI(https2URI)) + .GET() + .build(); + HttpResponse response = client.send(request, ofString()); + assert response.statusCode() == 200; + List h3service = registry.lookup(URI.create(https2URI), "h3") + .toList(); + System.out.println("h3 services: " + h3service); + assert h3service.stream().anyMatch( alt -> alt.alpn().equals("h3") + && alt.host().equals("www.example.com") + && alt.port() == 443 + && alt.isPersist()); + assert h3service.stream().anyMatch( alt -> alt.alpn().equals("h3") + && alt.host().equals(request.uri().getHost()) + && alt.port() == 4567 + && !alt.isPersist()); + + List h34service = registry.lookup(URI.create(https2URI), "h3-34") + .toList(); + System.out.println("h3-34 services: " + h34service); + assert h34service.stream().noneMatch( alt -> alt.alpn().equals("h3-34")); + } + + static class AltSvcRegistryTestHandler implements HttpTestHandler { + final String scheme; + final HttpTestServer server; + + AltSvcRegistryTestHandler(String scheme, HttpTestServer server) { + this.scheme = scheme; + this.server = server; + } + + @Override + public void handle(HttpTestExchange t) throws IOException { + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + var altsvc = """ + h3-34=":5678", h3="www.example.com:443"; persist=1, h3=":4567"; persist=0""" ; + t.getResponseHeaders().addHeader("alt-svc", altsvc.trim()); + byte[] bytes = is.readAllBytes(); + t.sendResponseHeaders(200, 10); + os.write(bytes); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java new file mode 100644 index 00000000000..93cf04dbcc4 --- /dev/null +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java @@ -0,0 +1,10 @@ +package jdk.internal.net.http; + +import java.net.http.HttpClient; + +public class HttpClientAccess { + + public static AltServicesRegistry getRegistry(HttpClient client) { + return ((HttpClientFacade)client).impl.registry(); + } +} diff --git a/test/jdk/java/nio/file/Files/IsSameFile.java b/test/jdk/java/nio/file/Files/IsSameFile.java index 44e03a15a02..ae7cc563472 100644 --- a/test/jdk/java/nio/file/Files/IsSameFile.java +++ b/test/jdk/java/nio/file/Files/IsSameFile.java @@ -22,9 +22,8 @@ */ /* @test - * @bug 8154364 8366254 + * @bug 8154364 8365626 8366254 * @summary Test of Files.isSameFile - * @requires (os.family != "windows") * @library .. /test/lib * @build IsSameFile jdk.test.lib.util.FileUtils * @run junit IsSameFile @@ -50,6 +49,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -66,6 +66,8 @@ public class IsSameFile { private Path c; private List allFiles; + private boolean supportsSymbolicLinks; + @BeforeAll public void init() throws IOException { home = Files.createTempDirectory("TestIsSameFile"); @@ -75,6 +77,12 @@ public class IsSameFile { allFiles.add(aa = home.resolve("a")); allFiles.add(b = home.resolve("b")); allFiles.add(c = home.resolve("c")); + + supportsSymbolicLinks = TestUtil.supportsSymbolicLinks(home); + } + + private boolean supportsSymbolicLinks() { + return supportsSymbolicLinks; } public void deleteFiles() throws IOException { @@ -203,9 +211,11 @@ public class IsSameFile { deleteFiles(); Files.createFile(a); Files.createFile(b); - Files.createSymbolicLink(c, a); List list = new ArrayList(); - list.add(Arguments.of(true, a, c)); + if (supportsSymbolicLinks) { + Files.createSymbolicLink(c, a); + list.add(Arguments.of(true, a, c)); + } list.add(Arguments.of(false, a, b)); return list.stream(); } @@ -220,13 +230,15 @@ public class IsSameFile { private Stream bcExistSource() throws IOException { deleteFiles(); Files.createFile(b); - Files.createSymbolicLink(c, a); List list = new ArrayList(); list.add(Arguments.of(true, a, a)); list.add(Arguments.of(true, a, aa)); list.add(Arguments.of(false, a, b)); list.add(Arguments.of(true, b, b)); - list.add(Arguments.of(false, a, c)); + if (supportsSymbolicLinks) { + list.add(Arguments.of(false, a, c)); + Files.createSymbolicLink(c, a); + } return list.stream(); } @@ -268,6 +280,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("equalFollowingSource") public void equalFollowing(boolean expect, Path x, Path y) @@ -310,6 +323,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("unequalFollowingSource") public void unequalFollowing(boolean expect, Path x, Path y) @@ -347,6 +361,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("unequalNotFollowingSource") public void unequalNotFollowing(boolean expect, Path x, Path y) @@ -382,6 +397,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("multiLinkSource") public void multiLink(boolean expect, Path x, Path y) @@ -419,6 +435,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("multiLinkNoTargetSource") public void multiLinkNoTarget(boolean expect, Path x, Path y) @@ -449,6 +466,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("linkLoopSource") public void linkLoop(boolean expect, Path x, Path y) throws IOException { diff --git a/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java b/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java index 83c2d85f6e4..fb54d468d18 100644 --- a/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java +++ b/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Red Hat, Inc. + * 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 @@ -30,6 +31,7 @@ import java.util.Calendar; import java.util.Locale; import java.util.List; import java.util.ArrayList; +import java.net.URI; import java.nio.file.Paths; import java.nio.file.Path; import java.nio.file.Files; @@ -54,12 +56,9 @@ import static java.util.Calendar.WEDNESDAY; */ public class TestSPISigned { - private static final String TEST_CLASSES = System.getProperty("test.classes", "."); private static final String TEST_SRC = System.getProperty("test.src", "."); private static final Path META_INF_DIR = Paths.get(TEST_SRC, "provider", "meta"); - private static final Path PROVIDER_PARENT = Paths.get(TEST_CLASSES, ".."); - private static final Path PROVIDER_DIR = PROVIDER_PARENT.resolve("provider"); private static final Path MODS_DIR = Paths.get("mods"); private static final Path UNSIGNED_JAR = MODS_DIR.resolve("unsigned-with-locale.jar"); private static final Path SIGNED_JAR = MODS_DIR.resolve("signed-with-locale.jar"); @@ -81,7 +80,9 @@ public class TestSPISigned { // Set up signed jar with custom calendar data provider // // 1. Create jar with custom CalendarDataProvider - JarUtils.createJarFile(UNSIGNED_JAR, PROVIDER_DIR); + var codeSource = baz.CalendarDataProviderImpl.class.getProtectionDomain().getCodeSource(); + var providerDir = Path.of(URI.create(codeSource.getLocation().toString())); + JarUtils.createJarFile(UNSIGNED_JAR, providerDir); JarUtils.updateJarFile(UNSIGNED_JAR, META_INF_DIR); // create signer's keypair SecurityTools.keytool("-genkeypair -keyalg RSA -keystore ks " + diff --git a/test/jdk/java/util/Calendar/RollHoursTest.java b/test/jdk/java/util/Calendar/RollHoursTest.java new file mode 100644 index 00000000000..2eb85caab85 --- /dev/null +++ b/test/jdk/java/util/Calendar/RollHoursTest.java @@ -0,0 +1,139 @@ +/* + * 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 8367901 + * @summary Ensure hour rolling is correct. Particularly, when the HOUR/HOUR_OF_DAY + * amount rolled would cause the calendar to originate on the same hour as before + * the call. + * @run junit RollHoursTest + */ + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RollHoursTest { + + // Should trigger multiple full HOUR/HOUR_OF_DAY cycles + private static final List hours = + IntStream.rangeClosed(-55, 55).boxed().toList(); + // Various calendars to test against + private static final List calendars = List.of( + // GMT, independent of daylight saving time transitions + new GregorianCalendar(TimeZone.getTimeZone("GMT")), + // Daylight saving observing calendars + new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")), + new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")), + new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")), + new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")) + ); + + // Reset the times of each calendar. These calendars provide testing under + // daylight saving transitions (or the lack thereof) and different AM/PM hours. + @BeforeEach + void clear() { + // Reset all calendars each iteration for clean slate + calendars.forEach(Calendar::clear); + + // Basic test, independent of daylight saving transitions + calendars.get(0).set(2005, 8, 20, 12, 10, 25); + + // Transition to daylight saving time (CST/CDT) --- + // Day of transition: 03/13/2016 (Sunday) + // Most interesting test case due to 2 AM skip + calendars.get(1).set(2016, 2, 13, 3, 30, 55); + // Day before transition: 03/12/2016 (Saturday) + calendars.get(2).set(2016, 2, 12, 15, 20, 45); + + // Transition back to standard time (PST/PDT) ---- + // Day of transition: 11/06/2016 (Sunday) + calendars.get(3).set(2016, 10, 6, 4, 15, 20); + // Day before transition: 11/05/2016 (Saturday) + calendars.get(4).set(2016, 10, 5, 12, 25, 30); + } + + // Rolling the HOUR_OF_DAY field. + @ParameterizedTest + @FieldSource("hours") + void HourOfDayTest(int hoursToRoll) { + for (var cal : calendars) { + var savedTime = cal.getTime(); + var savedHour = cal.get(Calendar.HOUR_OF_DAY); + cal.roll(Calendar.HOUR_OF_DAY, hoursToRoll); + assertEquals(getExpectedHour(hoursToRoll, savedHour, 24, cal, savedTime), + cal.get(Calendar.HOUR_OF_DAY), + getMessage(cal.getTimeZone(), savedTime, hoursToRoll)); + } + } + + // Rolling the HOUR field. + @ParameterizedTest + @FieldSource("hours") + void HourTest(int hoursToRoll) { + for (var cal : calendars) { + var savedTime = cal.getTime(); + var savedHour = cal.get(Calendar.HOUR_OF_DAY); + cal.roll(Calendar.HOUR, hoursToRoll); + assertEquals(getExpectedHour(hoursToRoll, savedHour, 12, cal, savedTime), + cal.get(Calendar.HOUR), + getMessage(cal.getTimeZone(), savedTime, hoursToRoll)); + } + } + + // Gets the expected hour after rolling by X hours. Supports 12/24-hour cycle. + // Special handling for non-existent 2 AM case on transition day. + private static int getExpectedHour(int roll, int hour, int hourCycle, Calendar cal, Date oldDate) { + // For example with HOUR_OF_DAY at 15 and a 24-hour cycle + // For rolling forwards 50 hours -> (50 + 15) % 24 = 17 + // For hour backwards 50 hours -> (24 + (15 - 50) % 24) % 24 + // -> (24 - 11) % 24 = 13 + var result = (roll >= 0 ? (hour + roll) : (hourCycle + (hour + roll) % hourCycle)) % hourCycle; + + // 2 AM does not exist on transition day. Calculate normalized value accordingly + if (cal.getTimeZone().inDaylightTime(oldDate) && cal.get(Calendar.MONTH) == Calendar.MARCH && result == 2) { + return roll > 0 ? result + 1 : result - 1; + } else { + // Normal return value + return result; + } + } + + // Get a TimeZone adapted error message + private static String getMessage(TimeZone tz, Date date, int hoursToRoll) { + var fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); + fmt.setTimeZone(tz); + return fmt.format(date) + " incorrectly rolled " + hoursToRoll; + } +} diff --git a/test/jdk/java/util/Comparator/MinMaxTest.java b/test/jdk/java/util/Comparator/MinMaxTest.java new file mode 100644 index 00000000000..f1ef7d2d164 --- /dev/null +++ b/test/jdk/java/util/Comparator/MinMaxTest.java @@ -0,0 +1,90 @@ +/* + * 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 8356995 + * @summary Comparator min/max method tests + * @run junit MinMaxTest + */ + +import org.junit.jupiter.api.Test; + +import java.util.Comparator; + +import static org.junit.jupiter.api.Assertions.*; + +public class MinMaxTest { + @Test + void testMin() { + Comparator c = Comparator.naturalOrder(); + assertEquals("a", c.min("a", "b")); + assertEquals("a", c.min("b", "a")); + } + + @Test + void testMax() { + Comparator c = Comparator.naturalOrder(); + assertEquals("b", c.max("a", "b")); + assertEquals("b", c.max("b", "a")); + } + + @Test + void testThrowsNPE() { + Comparator c = Comparator.naturalOrder(); + assertThrows(NullPointerException.class, () -> c.min(null, "a")); + assertThrows(NullPointerException.class, () -> c.min("a", null)); + assertThrows(NullPointerException.class, () -> c.max(null, "a")); + assertThrows(NullPointerException.class, () -> c.max("a", null)); + } + + @Test + void testThrowsCCE() { + @SuppressWarnings("unchecked") + Comparator c = (Comparator) (Comparator)Comparator.naturalOrder(); + assertThrows(ClassCastException.class, () -> c.min(1, "a")); + assertThrows(ClassCastException.class, () -> c.min("a", 1)); + assertThrows(ClassCastException.class, () -> c.max(1, "a")); + assertThrows(ClassCastException.class, () -> c.max("a", 1)); + } + + @Test + void testEqualReturnFirst() { + Comparator allEqual = (_, _) -> 0; + Object o1 = new Object(); + Object o2 = new Object(); + assertSame(o1, allEqual.min(o1, o2)); + assertSame(o1, allEqual.max(o1, o2)); + } + + @Test + void testComparatorSubtype() { + Comparator byLength = Comparator.comparing(CharSequence::length); + String s1 = "long_string"; + String s2 = "short"; + String min = byLength.min(s1, s2); + String max = byLength.max(s1, s2); + assertEquals(s1, max); + assertEquals(s2, min); + } +} diff --git a/test/jdk/javax/management/monitor/StartStopTest.java b/test/jdk/javax/management/monitor/StartStopTest.java index 4efbb45e609..2b0b5d92db5 100644 --- a/test/jdk/javax/management/monitor/StartStopTest.java +++ b/test/jdk/javax/management/monitor/StartStopTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, 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 @@ -63,6 +63,8 @@ public class StartStopTest { static int maxPoolSize; static final AtomicInteger counter = new AtomicInteger(); + public static final int TIMEOUT = 2500; + // MBean class public class ObservedObject implements ObservedObjectMBean { volatile public boolean called = false; @@ -148,7 +150,7 @@ public class StartStopTest { for (int i = 0; i < nTasks; i++) monitor[i].start(); echo(">>> MONITORS started"); - doSleep(500); + doSleep(TIMEOUT); echo(">>> Check FLAGS true"); for (int i = 0; i < nTasks; i++) if (!monitored[i].called) { @@ -160,7 +162,7 @@ public class StartStopTest { for (int i = 0; i < nTasks; i++) monitor[i].stop(); echo(">>> MONITORS stopped"); - doSleep(500); + doSleep(TIMEOUT); echo(">>> Set FLAGS to false"); for (int i = 0; i < nTasks; i++) monitored[i].called = false; diff --git a/test/jdk/jdk/internal/jimage/ImageReaderTest.java b/test/jdk/jdk/internal/jimage/ImageReaderTest.java index 8db9a3768d4..de52ed1503d 100644 --- a/test/jdk/jdk/internal/jimage/ImageReaderTest.java +++ b/test/jdk/jdk/internal/jimage/ImageReaderTest.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import tests.Helper; import tests.JImageGenerator; @@ -43,9 +44,11 @@ import java.util.Set; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -122,6 +125,65 @@ public class ImageReaderTest { } } + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + "modfoo:com/foo/Alpha.class", + "modbar:com/bar/One.class", + }) + public void testResource_present(String modName, String resPath) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertNotNull(reader.findResourceNode(modName, resPath)); + assertTrue(reader.containsResource(modName, resPath)); + + String canonicalNodeName = "/modules/" + modName + "/" + resPath; + Node node = reader.findNode(canonicalNodeName); + assertTrue(node != null && node.isResource()); + } + } + + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + // Absolute resource names are not allowed. + "modfoo:/com/bar/One.class", + // Resource in wrong module. + "modfoo:com/bar/One.class", + "modbar:com/foo/Alpha.class", + // Directories are not returned. + "modfoo:com/foo", + "modbar:com/bar", + // JImage entries exist for these, but they are not resources. + "modules:modfoo/com/foo/Alpha.class", + "packages:com.foo/modfoo", + // Empty module names/paths do not find resources. + "'':modfoo/com/foo/Alpha.class", + "modfoo:''"}) + public void testResource_absent(String modName, String resPath) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertNull(reader.findResourceNode(modName, resPath)); + assertFalse(reader.containsResource(modName, resPath)); + + // Non-existent resources names should either not be found, + // or (in the case of directory nodes) not be resources. + String canonicalNodeName = "/modules/" + modName + "/" + resPath; + Node node = reader.findNode(canonicalNodeName); + assertTrue(node == null || !node.isResource()); + } + } + + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + // Don't permit module names to contain paths. + "modfoo/com/bar:One.class", + "modfoo/com:bar/One.class", + "modules/modfoo/com:foo/Alpha.class", + }) + public void testResource_invalid(String modName, String resPath) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath)); + assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath)); + } + } + @Test public void testPackageDirectories() throws IOException { try (ImageReader reader = ImageReader.open(image)) { diff --git a/test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java b/test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java new file mode 100644 index 00000000000..3ec16bbc51c --- /dev/null +++ b/test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java @@ -0,0 +1,137 @@ +/* + * 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. + * + * 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. + */ + +import java.nio.ByteBuffer; +import jdk.internal.net.http.quic.packets.*; +import java.util.Arrays; + +/** + * @test + * @summary Unit encoding/decoding tests for the QUICPacketNumbers. + * @modules java.net.http/jdk.internal.net.http.quic.packets + */ +public class QuicPacketNumbersTest { + + public static void main(String[] args) throws Exception { + + // Test encoding logic. + // Test values from the upcoming QUIC RFC, Appendix A.2. + checkEncodePacketNumber(0xac5c02L, 0xabe8b3L, + new byte[]{(byte) 0x5c, (byte) 0x02}); + checkEncodePacketNumber(0xace8feL, 0xabe8bcL, + new byte[]{(byte) 0xac, (byte) 0xe8, (byte) 0xfe} + ); + + // Various checks for "None" encodings. + checkEncodePacketNumber(0x00L, -1L, + new byte[]{(byte) 0x00}); + checkEncodePacketNumber(0x05L, -1L, + new byte[]{(byte) 0x05}); + checkEncodePacketNumber(0x7eL, -1L, + new byte[]{(byte) 0x7E}); + checkEncodePacketNumber(0x7fL, -1L, + new byte[]{(byte) 0x00, (byte) 0x7f}); + checkEncodePacketNumber(0x80L, -1L, + new byte[]{(byte) 0x00, (byte) 0x80}); + checkEncodePacketNumber(0xffL, -1L, + new byte[]{(byte) 0x00, (byte) 0xff}); + checkEncodePacketNumber(0x100L, -1L, + new byte[]{(byte) 0x01, (byte) 0x00}); + + // Various checks for a packet 0 that has been ack'd. + checkEncodePacketNumber(0x7fL, 0L, + new byte[]{(byte) 0x7f}); + checkEncodePacketNumber(0x80L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80}); + checkEncodePacketNumber(0xffL, 0L, + new byte[]{(byte) 0x00, (byte) 0xff}); + checkEncodePacketNumber(0x100L, 0L, + new byte[]{(byte) 0x01, (byte) 0x00}); + checkEncodePacketNumber(0x7FFFL, 0L, + new byte[]{(byte) 0x7f, (byte) 0xFF}); + checkEncodePacketNumber(0x8000L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80, (byte) 0x00}); + checkEncodePacketNumber(0x7FFFFFL, 0L, + new byte[]{(byte) 0x7f, (byte) 0xff, (byte) 0xFF}); + checkEncodePacketNumber(0x800000L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80, (byte) 0x00, (byte) 0x00}); + checkEncodePacketNumber(0x7FFFFFFFL, 0L, + new byte[]{(byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xFF}); + + // Check some similar truncations. + checkEncodePacketNumber(0x0101L, 0x82L, + new byte[]{(byte) 0x01}); + checkEncodePacketNumber(0x0101L, 0x81L, + new byte[]{(byte) 0x01, (byte) 0x01}); + checkEncodePacketNumber(0x10001L, 0xFF82, + new byte[]{(byte) 0x01}); + checkEncodePacketNumber(0x10001L, 0xFF81, + new byte[]{(byte) 0x00, (byte) 0x01}); + checkEncodePacketNumber(0x1000001L, 0xFFFF82, + new byte[]{(byte) 0x01}); + checkEncodePacketNumber(0x1000001L, 0xFFFF81, + new byte[]{(byte) 0x00, (byte) 0x01}); + + // Check that > 4 bytes are not generated. + try { + checkEncodePacketNumber(0x80000000L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80, (byte) 0x00, + (byte) 0x00, (byte) 0x00}); + throw new Exception("Shouldn't encode"); + } catch (RuntimeException e) { + System.out.println("Caught the right exception!"); + } + + // Test decoding logic. + + // Test values from the upcoming QUIC RFC, Appendix A.3. + checkDecodePacketNumber(0xa82f30eaL, + ByteBuffer.wrap(new byte[]{(byte) 0x9b, (byte) 0x32}), + 2, 0xa82f9b32L); + + // TBD: More test values + } + + public static void checkEncodePacketNumber( + long fullPN, long largestAcked, byte[] bytes) throws Exception { + + byte[] answer = QuicPacketNumbers.encodePacketNumber( + fullPN, largestAcked); + + if (!Arrays.equals(answer, bytes)) { + throw new Exception("Encoding Problem"); + } + } + + public static void checkDecodePacketNumber( + long largestPN, ByteBuffer buf, int headerNumBytes, + long answer) throws Exception { + + long result = QuicPacketNumbers.decodePacketNumber( + largestPN, buf, headerNumBytes); + + if (result != answer) { + throw new Exception("Decoding Problem"); + } + } +} diff --git a/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java b/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java index 568104a7b50..d69d47f1911 100644 --- a/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java +++ b/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java @@ -87,8 +87,6 @@ public class TestG1ParallelPhases { .collect(toSet()); Set allPhases = of( - "RetireTLABsAndFlushLogs", - "NonJavaThreadFlushLogs", "ExtRootScan", "ThreadRoots", "VM Global", @@ -100,31 +98,32 @@ public class TestG1ParallelPhases { "CMRefRoots", "MergeER", "MergeRS", - "MergeLB", "ScanHR", "CodeRoots", "ObjCopy", "Termination", - "RedirtyCards", "RecalculateUsed", "ResizeTLABs", "FreeCSet", "UpdateDerivedPointers", "EagerlyReclaimHumongousObjects", "ResetPartialArrayStateManager", - "ClearLoggedCards", + "ClearPendingCards", "MergePSS", "NonYoungFreeCSet", "YoungFreeCSet", "RebuildFreeList", "SampleCandidates", "ResetMarkingState", - "NoteStartOfMark" + "NoteStartOfMark", + "RetireTLABs" ); // Some GC phases may or may not occur depending on environment. Filter them out // since we can not reliably guarantee that they occur (or not). Set optPhases = of( + // Does not always occur + "SweepRT", // The following phases only occur on evacuation failure. "RestoreEvacuationFailedRegions", "RemoveSelfForwards", diff --git a/test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java b/test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java new file mode 100644 index 00000000000..ffb16ec048a --- /dev/null +++ b/test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java @@ -0,0 +1,123 @@ +/* + * 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. + * + * 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.jfr.jmx.streaming; + +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; +import javax.management.MBeanServerConnection; + +import jdk.jfr.Event; +import jdk.jfr.Name; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; +import jdk.management.jfr.RemoteRecordingStream; + +/** + * @test + * @summary Tests retention with RemoteRecordingStream::dump(Path) + * @requires vm.flagless + * @requires vm.hasJFR + * @library /test/lib + * @run main/othervm jdk.jfr.jmx.streaming.TestDumpRetention + */ +public class TestDumpRetention { + + private static final MBeanServerConnection CONNECTION = ManagementFactory.getPlatformMBeanServer(); + + @Name("TestDumpRetention") + static class DumpEvent extends Event { + } + + public static void main(String... args) throws Exception { + testSetNoPolicy(); + testSetMaxAge(); + testSetMaxSize(); + } + + private static List recordWithPolicy(String filename, Consumer policy) throws Exception { + CountDownLatch latch1 = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(2); + CountDownLatch latch3 = new CountDownLatch(3); + try (var rs = new RemoteRecordingStream(CONNECTION)) { + policy.accept(rs); + rs.onEvent(e -> { + latch1.countDown(); + latch2.countDown(); + latch3.countDown(); + }); + rs.startAsync(); + DumpEvent e1 = new DumpEvent(); + e1.commit(); + latch1.await(); + // Force chunk rotation + try (Recording r = new Recording()) { + r.start(); + DumpEvent e2 = new DumpEvent(); + e2.commit(); + } + latch2.await(); + DumpEvent e3 = new DumpEvent(); + e3.commit(); + latch3.await(); + Path p = Path.of(filename); + rs.dump(p); + return RecordingFile.readAllEvents(p); + } + } + + private static void testSetNoPolicy() throws Exception { + var events = recordWithPolicy("no-policy.jfr", rs -> { + // use default policy, remove after consumption + }); + // Since latch3 have been triggered at least two events/chunks + // before must have been consumed, possibly 3, but it's a race. + if (events.size() > 1) { + throw new Exception("Expected at most one event to not be consumed"); + } + } + + private static void testSetMaxAge() throws Exception { + var events = recordWithPolicy("max-age.jfr", rs -> { + // keeps all events for the dump + rs.setMaxAge(Duration.ofDays(1)); + }); + if (events.size() != 3) { + throw new Exception("Expected all 3 events to be in dump after setMaxAge"); + } + } + + private static void testSetMaxSize() throws Exception { + var events = recordWithPolicy("max-size.jfr", rs -> { + // keeps all events for the dump + rs.setMaxSize(100_000_000); + }); + if (events.size() != 3) { + throw new Exception("Expected all 3 events to be in dump after setMaxSize"); + } + } +} diff --git a/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java b/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java index 6731f3c44cf..7319601d147 100644 --- a/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java +++ b/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java @@ -33,7 +33,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import java.util.function.Function; import javax.management.MBeanServerConnection; import jdk.jfr.Event; @@ -41,12 +40,11 @@ import jdk.jfr.Name; import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordingFile; -import jdk.jfr.consumer.RecordingStream; import jdk.management.jfr.RemoteRecordingStream; /** * @test - * @summary Tests RecordingStream::dump(Path) + * @summary Tests RemoteRecordingStream::dump(Path) * @requires vm.flagless * @requires vm.hasJFR * @library /test/lib @@ -66,9 +64,6 @@ public class TestRemoteDump { testOneDump(); testMultipleDumps(); testEventAfterDump(); - testSetNoPolicy(); - testSetMaxAge(); - testSetMaxSize(); } private static void testUnstarted() throws Exception { @@ -97,74 +92,6 @@ public class TestRemoteDump { } } - private static List recordWithPolicy(String filename, boolean awaitEvents, Consumer policy) throws Exception { - CountDownLatch latch1 = new CountDownLatch(1); - CountDownLatch latch2 = new CountDownLatch(2); - CountDownLatch latch3 = new CountDownLatch(3); - try (var rs = new RemoteRecordingStream(CONNECTION)) { - policy.accept(rs); - rs.onEvent(e -> { - latch1.countDown(); - latch2.countDown(); - latch3.countDown(); - }); - rs.startAsync(); - DumpEvent e1 = new DumpEvent(); - e1.commit(); - if (awaitEvents) { - latch1.await(); - } - // Force chunk rotation - try (Recording r = new Recording()) { - r.start(); - DumpEvent e2 = new DumpEvent(); - e2.commit(); - } - if (awaitEvents) { - latch2.await(); - } - DumpEvent e3 = new DumpEvent(); - e3.commit(); - latch3.await(); - Path p = Path.of(filename); - rs.dump(p); - return RecordingFile.readAllEvents(p); - } - } - - private static void testSetMaxSize() throws Exception { - var events = recordWithPolicy("max-size.jfr", false, rs -> { - // keeps all events for the dump - rs.setMaxSize(100_000_000); - }); - if (events.size() != 3) { - throw new Exception("Expected all 3 events to be in dump after setMaxSize"); - } - - } - - private static void testSetMaxAge() throws Exception { - var events = recordWithPolicy("max-age.jfr", false, rs -> { - // keeps all events for the dump - rs.setMaxAge(Duration.ofDays(1)); - }); - if (events.size() != 3) { - throw new Exception("Expected all 3 events to be in dump after setMaxAge"); - } - - } - - private static void testSetNoPolicy() throws Exception { - var events = recordWithPolicy("no-policy.jfr", true, rs -> { - // use default policy, remove after consumption - }); - // Since latch3 have been triggered at least two events/chunks - // before must have been consumed, possibly 3, but it's a race. - if (events.size() > 1) { - throw new Exception("Expected at most one event to not be consumed"); - } - } - private static void testMultipleDumps() throws Exception { CountDownLatch latch = new CountDownLatch(1); try (var rs = new RemoteRecordingStream(CONNECTION)) { diff --git a/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java b/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java index d15988176e3..d9da79be126 100644 --- a/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java +++ b/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java @@ -25,6 +25,7 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.testng.SkipException; import java.io.IOException; import java.net.URI; @@ -35,6 +36,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Iterator; import java.util.Map; +import jdk.test.lib.Platform; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -48,6 +50,7 @@ import static org.testng.Assert.assertTrue; * @bug 8218875 * @summary ZIP File System tests that leverage Files.newFileSystem * @modules jdk.zipfs + * @library /test/lib * @compile NewFileSystemTests.java * @run testng NewFileSystemTests */ @@ -219,6 +222,9 @@ public class NewFileSystemTests { */ @Test public void readOnlyZipFileFailure() throws IOException { + if (Platform.isRoot()) { + throw new SkipException("Test skipped when executed by root user."); + } // Underlying file is read-only. Path readOnlyZip = Utils.createJarFile("read_only.zip", Map.of("file.txt", "Hello World")); // In theory this can fail, and we should avoid unwanted false-negatives. diff --git a/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java b/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java index 087ef483fb2..a784832abef 100644 --- a/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java +++ b/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java @@ -60,13 +60,16 @@ public class DebugPropertyValuesTest extends SSLSocketTemplate { List.of("Produced ClientHello handshake message", "supported_versions")); debugMessages.put("handshake-expand", - List.of("\"message\".*: \"Produced ClientHello handshake message")); + List.of("\"logger\".*: \"javax.net.ssl\",", + "\"message\".*: \"Produced ClientHello handshake message", + "\"specifics\".*:\\ \\[")); debugMessages.put("keymanager", List.of("choosing key:")); debugMessages.put("packet", List.of("Raw write")); debugMessages.put("plaintext", List.of("Plaintext before ENCRYPTION")); debugMessages.put("record", List.of("handshake, length =", "WRITE:")); debugMessages.put("record-expand", - List.of("\"message\".*: \"READ: TLSv1.2 application_data")); + List.of("\"logger\".*: \"javax.net.ssl\",", + "\"message\".*: \"READ: TLSv1.2 application_data")); debugMessages.put("session", List.of("Session initialized:")); debugMessages.put("sslctx", List.of("trigger seeding of SecureRandom")); debugMessages.put("ssl", List.of("jdk.tls.keyLimits:")); @@ -81,7 +84,12 @@ public class DebugPropertyValuesTest extends SSLSocketTemplate { // "ALL" shouldn't be seen as a valid Level debugMessages.put("javax.net.debug.logger.ALL", List.of("ALL:")); debugMessages.put("javax.net.debug.logger", - List.of("FINE: adding as trusted certificates", + List.of("FINE: adding as trusted certificates:" + + System.lineSeparator() + + " \"certificate\" : \\{", + "FINE: Produced ClientHello handshake message:" + + System.lineSeparator() + + "\"ClientHello\": \\{", "FINE: WRITE: TLSv1.3 application_data")); } @@ -106,7 +114,8 @@ public class DebugPropertyValuesTest extends SSLSocketTemplate { "sslctx", "trustmanager", "verbose")), // allow expand option for more verbose output Arguments.of(List.of("-Djavax.net.debug=ssl,handshake,expand"), - List.of("handshake", "handshake-expand", "ssl", "verbose")), + List.of("handshake", "handshake-expand", + "ssl", "verbose")), // filtering on record option, with expand Arguments.of(List.of("-Djavax.net.debug=ssl:record,expand"), List.of("record", "record-expand", "ssl")), @@ -168,11 +177,23 @@ public class DebugPropertyValuesTest extends SSLSocketTemplate { OutputAnalyzer outputAnalyzer = ProcessTools.executeTestJava(args); outputAnalyzer.shouldHaveExitValue(0); for (String s : debugMessages.keySet()) { - for (String output : debugMessages.get(s)) { - if (expected.contains(s)) { + List patterns = debugMessages.get(s); + if (expected.contains(s)) { + for (String output : patterns) { outputAnalyzer.shouldMatch(output); - } else { - outputAnalyzer.shouldNotMatch(output); + } + } else { + // some debug messages overlap with each other. Only fail if + // all the messages in the list were unexpected + boolean allUnexpected = true; + for (String output : patterns) { + if (!outputAnalyzer.contains(output)) { + allUnexpected = false; + break; + } + } + if (allUnexpected) { + throw new AssertionError("Unexpected output for key: " + s); } } } diff --git a/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java b/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java index 2fa046e1dc2..4b14f0c28ce 100644 --- a/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java +++ b/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java @@ -33,7 +33,6 @@ * @run main/othervm MD5NotAllowedInTLS13CertificateSignature */ -import static jdk.test.lib.Asserts.assertEquals; import static jdk.test.lib.Asserts.assertTrue; import static jdk.test.lib.Utils.runAndCheckException; @@ -99,11 +98,12 @@ public class MD5NotAllowedInTLS13CertificateSignature extends serverEx -> { Throwable clientEx = serverEx.getSuppressed()[0]; assertTrue(clientEx instanceof SSLHandshakeException); - assertEquals(clientEx.getMessage(), "(bad_certificate) " - + "PKIX path validation failed: " - + "java.security.cert.CertPathValidatorException: " - + "Algorithm constraints check failed on signature" - + " algorithm: MD5withRSA"); + assertTrue(clientEx.getMessage().startsWith( + "(bad_certificate)" + + " PKIX path validation failed: " + + "java.security.cert.CertPathValidatorException:" + + " Algorithm constraints check failed on " + + "MD5withRSA signature and RSA key")); }); // Should run fine on TLSv1.2. diff --git a/test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java b/test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java new file mode 100644 index 00000000000..f72f16f5374 --- /dev/null +++ b/test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java @@ -0,0 +1,274 @@ +/* + * 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. + */ + +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static jdk.test.lib.Utils.runAndCheckException; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; +import jdk.test.lib.security.CertificateBuilder; +import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.KeyIdentifier; +import sun.security.x509.SerialNumber; +import sun.security.x509.X500Name; + + +/* + * @test + * @bug 8367104 + * @summary Check for RSASSA-PSS parameters when validating certificates + * against algorithm constraints. + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS Rsa_pss_pss_Sha384 true + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS RsaSsa-Pss true + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS rsa_pss_Rsae_sha384 true + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS Rsa true + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS RSASSA-pSS true + * @run main/othervm RsaSsaPssConstraints RSA SHA384withRSA rsa_pkcs1_Sha384 true + * @run main/othervm RsaSsaPssConstraints EC SHA384withECDSA Ecdsa_Secp384r1_sha384 true + * @run main/othervm RsaSsaPssConstraints RSA SHA384withRSA SHA384withRsA true + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS rsa_pss_rsae_sha384 false + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS rsa_pss_pss_sha384 false + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha256 false + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha512 false + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS RSA false + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS rsa_pss_rsae_sha512 false + * @run main/othervm RsaSsaPssConstraints RSA SHA384withRSA rsa_pkcs1_sha256 false + * @run main/othervm RsaSsaPssConstraints EC SHA384withECDSA ecdsa_secp256r1_sha256 false + * @run main/othervm RsaSsaPssConstraints EC SHA384withECDSA SHA512withECDSA false + */ + +public class RsaSsaPssConstraints extends SSLSocketTemplate { + + private final String protocol; + private final String keyAlg; + private final String certSigAlg; + private X509Certificate trustedCert; + private X509Certificate serverCert; + private X509Certificate clientCert; + private KeyPair serverKeys; + private KeyPair clientKeys; + + protected RsaSsaPssConstraints( + String protocol, String keyAlg, + String certSigAlg) throws Exception { + super(); + this.protocol = protocol; + this.keyAlg = keyAlg; + this.certSigAlg = certSigAlg; + setupCertificates(); + } + + public static void main(String[] args) throws Exception { + if (args.length != 4) { + throw new RuntimeException("Wrong number of arguments"); + } + + String keyAlg = args[0]; + String certSigAlg = args[1]; + String constraintAlgo = args[2]; + boolean fail = Boolean.parseBoolean(args[3]); + + // Note: CertificateBuilder generates RSASSA-PSS certificate + // signature using SHA-384 digest algorithm by default. + Security.setProperty("jdk.tls.disabledAlgorithms", + constraintAlgo + " usage CertificateSignature"); + + for (String protocol : new String[]{"TLSv1.3", "TLSv1.2"}) { + var test = new RsaSsaPssConstraints(protocol, keyAlg, certSigAlg); + + final String errorMsg = protocol.equals("TLSv1.2") ? + "no cipher suites in common" : + "No available authentication scheme"; + + if (fail) { + runAndCheckException(test::run, + serverEx -> { + assertTrue( + serverEx instanceof SSLHandshakeException); + assertEquals(serverEx.getMessage(), + "(handshake_failure) " + errorMsg); + }); + } else { + test.run(); + } + } + + // Disable KeyManager's algorithm constraints checking and + // check against TrustManager's local supported signature + // algorithms on the client side. + System.setProperty( + "jdk.tls.SunX509KeyManager.certChecking", "false"); + + for (String protocol : new String[]{"TLSv1.3", "TLSv1.2"}) { + var test = new RsaSsaPssConstraints(protocol, keyAlg, certSigAlg); + + if (fail) { + runAndCheckException(test::run, + serverEx -> { + Throwable clientEx = serverEx.getSuppressed()[0]; + assertTrue(clientEx instanceof SSLHandshakeException + || serverEx instanceof SSLHandshakeException); + }); + } else { + test.run(); + } + } + } + + @Override + public SSLContext createServerSSLContext() throws Exception { + return getSSLContext( + trustedCert, serverCert, serverKeys.getPrivate(), protocol); + } + + @Override + public SSLContext createClientSSLContext() throws Exception { + return getSSLContext( + trustedCert, clientCert, clientKeys.getPrivate(), protocol); + } + + private static SSLContext getSSLContext( + X509Certificate trustedCertificate, X509Certificate keyCertificate, + PrivateKey privateKey, String protocol) + throws Exception { + + // create a key store + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + + // import the trusted cert + ks.setCertificateEntry("TLS Signer", trustedCertificate); + + // generate certificate chain + Certificate[] chain = new Certificate[2]; + chain[0] = keyCertificate; + chain[1] = trustedCertificate; + + // import the key entry. + final char[] passphrase = "passphrase".toCharArray(); + ks.setKeyEntry("Whatever", privateKey, passphrase, chain); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(ks); + + // create SSL context + SSLContext ctx = SSLContext.getInstance(protocol); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + return ctx; + } + + // Certificate-building helper methods. + + private void setupCertificates() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlg); + KeyPair caKeys = kpg.generateKeyPair(); + this.serverKeys = kpg.generateKeyPair(); + this.clientKeys = kpg.generateKeyPair(); + + this.trustedCert = createTrustedCert(caKeys, certSigAlg); + + this.serverCert = customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + serverKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + + this.clientCert = customCertificateBuilder( + "CN=localhost, OU=SSL-Client, O=Some-Org, L=Some-City, ST=Some-State, C=US", + clientKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + } + + private static X509Certificate createTrustedCert( + KeyPair caKeys, String certSigAlg) + throws Exception { + SecureRandom random = new SecureRandom(); + + KeyIdentifier kid = new KeyIdentifier(caKeys.getPublic()); + GeneralNames gns = new GeneralNames(); + GeneralName name = new GeneralName(new X500Name( + "O=Trusted-Org, L=Some-City, ST=Some-State, C=US")); + gns.add(name); + BigInteger serialNumber = BigInteger.valueOf( + random.nextLong(1000000) + 1); + return customCertificateBuilder( + name.toString(), + caKeys.getPublic(), caKeys.getPublic()) + .setSerialNumber(serialNumber) + .addExtension(new AuthorityKeyIdentifierExtension(kid, gns, + new SerialNumber(serialNumber))) + .addBasicConstraintsExt(true, true, -1) + .build(null, caKeys.getPrivate(), certSigAlg); + } + + private static CertificateBuilder customCertificateBuilder( + String subjectName, PublicKey publicKey, PublicKey caKey) + throws CertificateException, IOException { + SecureRandom random = new SecureRandom(); + + CertificateBuilder builder = new CertificateBuilder() + .setSubjectName(subjectName) + .setPublicKey(publicKey) + .setNotBefore( + Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + .setNotAfter(Date.from(Instant.now().plus(1, ChronoUnit.HOURS))) + .setSerialNumber( + BigInteger.valueOf(random.nextLong(1000000) + 1)) + .addSubjectKeyIdExt(publicKey) + .addAuthorityKeyIdExt(caKey); + builder.addKeyUsageExt( + new boolean[]{true, true, true, true, true, true}); + + return builder; + } +} diff --git a/test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java b/test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java new file mode 100644 index 00000000000..125fec01a38 --- /dev/null +++ b/test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java @@ -0,0 +1,284 @@ +/* + * 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. + */ + +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static jdk.test.lib.Utils.runAndCheckException; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import jdk.test.lib.security.CertificateBuilder; +import sun.security.provider.certpath.SunCertPathBuilderException; +import sun.security.validator.ValidatorException; +import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.KeyIdentifier; +import sun.security.x509.SerialNumber; +import sun.security.x509.X500Name; + +/* + * @test + * @bug 8367104 + * @summary Check for RSASSA-PSS parameters when validating certificates + * against algorithm constraints. + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * java.base/sun.security.validator + * java.base/sun.security.provider.certpath + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS Rsa_pss_pss_Sha384 true + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS RsaSsa-Pss true + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS rsa_pss_Rsae_sha384 true + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS Rsa true + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS RSASSA-pSS true + * @run main/othervm CertChainAlgorithmConstraints RSA SHA384withRSA rsa_pkcs1_Sha384 true + * @run main/othervm CertChainAlgorithmConstraints EC SHA384withECDSA Ecdsa_Secp384r1_sha384 true + * @run main/othervm CertChainAlgorithmConstraints RSA SHA384withRSA SHA384withRsA true + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS rsa_pss_rsae_sha384 false + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS rsa_pss_pss_sha384 false + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha256 false + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha512 false + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS RSA false + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS rsa_pss_rsae_sha512 false + * @run main/othervm CertChainAlgorithmConstraints RSA SHA384withRSA rsa_pkcs1_sha256 false + * @run main/othervm CertChainAlgorithmConstraints EC SHA384withECDSA ecdsa_secp256r1_sha256 false + * @run main/othervm CertChainAlgorithmConstraints EC SHA384withECDSA SHA512withECDSA false + */ + +// Testing that an algorithm can be disabled with both a PKIXCertPathValidator +// and a CertPathBuilder code paths. It is somewhat common for JSSE to fall +// back to using a CertPathBuilder to find a valid chain. +public class CertChainAlgorithmConstraints extends SSLEngineTemplate { + + private final String keyAlg; + private final String certSigAlg; + private final boolean fail; + + private X509Certificate trustedCert; + private X509Certificate serverCert; + private X509Certificate linkCert1; + private X509Certificate linkCert2; + + protected CertChainAlgorithmConstraints( + String keyAlg, String certSigAlg, boolean fail) + throws Exception { + super(); + this.keyAlg = keyAlg; + this.certSigAlg = certSigAlg; + this.fail = fail; + setupCertificates(); + } + + public static void main(String[] args) throws Exception { + if (args.length != 4) { + throw new RuntimeException("Wrong number of arguments"); + } + + String keyAlg = args[0]; + String certSigAlg = args[1]; + String constraintAlgo = args[2]; + boolean fail = Boolean.parseBoolean(args[3]); + + // Note: CertificateBuilder generates RSASSA-PSS certificate + // signature using SHA-384 digest algorithm by default. + Security.setProperty("jdk.tls.disabledAlgorithms", + constraintAlgo + " usage CertificateSignature"); + + new CertChainAlgorithmConstraints(keyAlg, certSigAlg, fail).run(); + } + + // Run things in TLS handshake order. + protected void run() throws Exception { + + // Produce client_hello + clientEngine.wrap(clientOut, cTOs); + cTOs.flip(); + + // Consume client_hello. + serverEngine.unwrap(cTOs, serverIn); + runDelegatedTasks(serverEngine); + + // Produce server_hello. + serverEngine.wrap(serverOut, sTOc); + sTOc.flip(); + + // Now that we have a Handshake session attached to the serverEngine, + // do the check. + checkChain(serverEngine); + } + + protected void checkChain(SSLEngine engine) throws Exception { + + // Create a key store. + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + + // Import the trusted cert. + ks.setCertificateEntry("Trusted", trustedCert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + // Init TrustManager with a Key Store. + tmf.init(ks); + + // Generate a mixed-up certificate chain. + X509Certificate[] mixedUpChain = new X509Certificate[3]; + mixedUpChain[0] = linkCert2; + // Put EE cert between 2 link certs to mix up the chain. + mixedUpChain[1] = serverCert; + mixedUpChain[2] = linkCert1; + + // Generate a valid certificate chain - we should get the same + // results with it but a different code path to be used. + X509Certificate[] validChain = new X509Certificate[3]; + validChain[0] = serverCert; + validChain[1] = linkCert2; + validChain[2] = linkCert1; + + var tm = (X509ExtendedTrustManager) tmf.getTrustManagers()[0]; + + if (fail) { + // Mixed-up chain: CertPathBuilder code path. + runAndCheckException( + () -> tm.checkServerTrusted(mixedUpChain, "RSA", engine), + ex -> { + assertTrue(ex instanceof ValidatorException); + assertTrue( + ex.getCause() instanceof SunCertPathBuilderException); + assertEquals(ex.getMessage(), "PKIX path " + + "building failed: " + + "sun.security.provider.certpath." + + "SunCertPathBuilderException: unable to find " + + "valid certification path to requested target"); + }); + + // Valid chain: PKIXCertPathValidator code path. + runAndCheckException( + () -> tm.checkServerTrusted(validChain, "RSA", engine), + ex -> { + assertTrue(ex instanceof ValidatorException); + assertTrue( + ex.getCause() instanceof CertPathValidatorException); + assertTrue(ex.getMessage().startsWith("PKIX path " + + "validation failed: java.security.cert." + + "CertPathValidatorException: Algorithm " + + "constraints check failed on " + + certSigAlg + " signature and " + + keyAlg + " key")); + }); + } else { + tm.checkServerTrusted(mixedUpChain, "RSA", engine); + tm.checkServerTrusted(validChain, "RSA", engine); + } + } + + // Certificate-building helper methods. + + private void setupCertificates() throws Exception { + var kpg = KeyPairGenerator.getInstance(keyAlg); + var caKeys = kpg.generateKeyPair(); + var serverKeys = kpg.generateKeyPair(); + var linkKeys1 = kpg.generateKeyPair(); + var linkKeys2 = kpg.generateKeyPair(); + + this.trustedCert = createTrustedCert(caKeys, certSigAlg); + + this.linkCert1 = customCertificateBuilder( + "O=Link1, L=Some-City, ST=Some-State, C=US", + linkKeys1.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(true, true, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + + this.linkCert2 = customCertificateBuilder( + "O=Link2, L=Some-City, ST=Some-State, C=US", + linkKeys2.getPublic(), linkKeys1.getPublic()) + .addBasicConstraintsExt(true, true, -1) + .build(linkCert1, linkKeys1.getPrivate(), certSigAlg); + + this.serverCert = customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + serverKeys.getPublic(), linkKeys2.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(linkCert2, linkKeys2.getPrivate(), + certSigAlg); + } + + private static X509Certificate createTrustedCert( + KeyPair caKeys, String certSigAlg) + throws Exception { + SecureRandom random = new SecureRandom(); + + KeyIdentifier kid = new KeyIdentifier(caKeys.getPublic()); + GeneralNames gns = new GeneralNames(); + GeneralName name = new GeneralName(new X500Name( + "O=Trusted-Org, L=Some-City, ST=Some-State, C=US")); + gns.add(name); + BigInteger serialNumber = BigInteger.valueOf( + random.nextLong(1000000) + 1); + return customCertificateBuilder( + name.toString(), + caKeys.getPublic(), caKeys.getPublic()) + .setSerialNumber(serialNumber) + .addExtension(new AuthorityKeyIdentifierExtension(kid, gns, + new SerialNumber(serialNumber))) + .addBasicConstraintsExt(true, true, -1) + .build(null, caKeys.getPrivate(), certSigAlg); + } + + private static CertificateBuilder customCertificateBuilder( + String subjectName, PublicKey publicKey, PublicKey caKey) + throws CertificateException, IOException { + SecureRandom random = new SecureRandom(); + + CertificateBuilder builder = new CertificateBuilder() + .setSubjectName(subjectName) + .setPublicKey(publicKey) + .setNotBefore( + Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + .setNotAfter(Date.from(Instant.now().plus(1, ChronoUnit.HOURS))) + .setSerialNumber( + BigInteger.valueOf(random.nextLong(1000000) + 1)) + .addSubjectKeyIdExt(publicKey) + .addAuthorityKeyIdExt(caKey); + builder.addKeyUsageExt( + new boolean[]{true, true, true, true, true, true}); + + return builder; + } +} diff --git a/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java b/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java index f5589484f3d..e2554ee0f91 100644 --- a/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java +++ b/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java @@ -38,12 +38,13 @@ import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import jdk.test.lib.SecurityTools; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class VerifyJarEntryName { @@ -85,7 +86,7 @@ public class VerifyJarEntryName { */ @Test void verifyManifestEntryName() throws Exception { - modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "MANIFEST.MF"); + modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "META-INF/MANIFEST.MF"); SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR) .shouldContain("This JAR file contains internal " + "inconsistencies that may result in different " + @@ -95,6 +96,22 @@ public class VerifyJarEntryName { .shouldHaveExitValue(0); } + /* + * Modify a single byte in signature filename in LOC, and + * validate that jarsigner -verify emits a warning message. + */ + @Test + void verifySignatureEntryName() throws Exception { + modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "META-INF/MYKEY.SF"); + SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR) + .shouldContain("This JAR file contains internal " + + "inconsistencies that may result in different " + + "contents when reading via JarFile and JarInputStream:") + .shouldContain("- Entry XETA-INF/MYKEY.SF is present when reading " + + "via JarInputStream but missing when reading via JarFile") + .shouldHaveExitValue(0); + } + /* * Validate that jarsigner -verify on a valid JAR works without * emitting warnings about internal inconsistencies. @@ -111,9 +128,14 @@ public class VerifyJarEntryName { private void modifyJarEntryName(Path origJar, Path modifiedJar, String entryName) throws Exception { byte[] jarBytes = Files.readAllBytes(origJar); - var jarString = new String(jarBytes, StandardCharsets.UTF_8); - var pos = jarString.indexOf(entryName); - assertTrue(pos != -1, entryName + " is not present in the JAR"); + byte[] entryNameBytes = entryName.getBytes(StandardCharsets.UTF_8); + int pos = 0; + try { + while (!Arrays.equals(jarBytes, pos, pos + entryNameBytes.length, + entryNameBytes, 0, entryNameBytes.length)) pos++; + } catch (ArrayIndexOutOfBoundsException ignore) { + fail(entryName + " is not present in the JAR"); + } jarBytes[pos] = 'X'; Files.write(modifiedJar, jarBytes); } diff --git a/test/jdk/tools/jpackage/apps/UseShutdownHook.java b/test/jdk/tools/jpackage/apps/UseShutdownHook.java new file mode 100644 index 00000000000..c558ee85701 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/UseShutdownHook.java @@ -0,0 +1,88 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class UseShutdownHook { + + public static void main(String[] args) throws InterruptedException { + trace("Started"); + + var outputFile = Path.of(args[0]); + trace(String.format("Write output in [%s] file", outputFile)); + + var shutdownTimeoutSeconds = Integer.parseInt(args[1]); + trace(String.format("Automatically shutdown the app in %ss", shutdownTimeoutSeconds)); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + output(outputFile, "shutdown hook executed"); + } + }); + + var startTime = System.currentTimeMillis(); + var lock = new Object(); + do { + synchronized (lock) { + lock.wait(shutdownTimeoutSeconds * 1000); + } + } while ((System.currentTimeMillis() - startTime) < (shutdownTimeoutSeconds * 1000)); + + output(outputFile, "exit"); + } + + private static void output(Path outputFilePath, String msg) { + + trace(String.format("Writing [%s] into [%s]", msg, outputFilePath)); + + try { + Files.createDirectories(outputFilePath.getParent()); + Files.writeString(outputFilePath, msg, StandardOpenOption.APPEND, StandardOpenOption.CREATE); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static void trace(String msg) { + Date time = new Date(System.currentTimeMillis()); + msg = String.format("UseShutdownHook [%s]: %s", SDF.format(time), msg); + System.out.println(msg); + try { + Files.write(traceFile, List.of(msg), StandardOpenOption.APPEND, StandardOpenOption.CREATE); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss.SSS"); + + private static final Path traceFile = Path.of(System.getProperty("jpackage.test.trace-file")); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java index 9df28b3915e..52e8ecf819b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.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 @@ -59,13 +59,18 @@ public final class CfgFile { } } - public void addValue(String sectionName, String key, String value) { + public CfgFile addValue(String sectionName, String key, String value) { var section = getSection(sectionName); if (section == null) { section = new Section(sectionName, new ArrayList<>()); data.add(section); } section.data.add(Map.entry(key, value)); + return this; + } + + public CfgFile add(CfgFile other) { + return combine(this, other); } public CfgFile() { @@ -89,7 +94,7 @@ public final class CfgFile { this.id = id; } - public void save(Path path) { + public CfgFile save(Path path) { var lines = data.stream().flatMap(section -> { return Stream.concat( Stream.of(String.format("[%s]", section.name)), @@ -98,6 +103,7 @@ public final class CfgFile { })); }); TKit.createTextFile(path, lines); + return this; } private Section getSection(String name) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index a94dfa135c1..b3f188bb371 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -41,7 +41,6 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; -import java.nio.file.WatchService; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; @@ -611,44 +610,44 @@ public final class TKit { trace(String.format("Wait for file [%s] to be available", fileToWaitFor.toAbsolutePath())); - WatchService ws = FileSystems.getDefault().newWatchService(); + try (var ws = FileSystems.getDefault().newWatchService()) { - Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); - watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); + Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); + watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); - var waitUntil = Instant.now().plus(timeout); - for (;;) { - var remainderTimeout = Instant.now().until(waitUntil); - assertTrue(remainderTimeout.isPositive(), String.format( - "Check timeout value %dms is positive", remainderTimeout.toMillis())); + var waitUntil = Instant.now().plus(timeout); + for (;;) { + var remainderTimeout = Instant.now().until(waitUntil); + assertTrue(remainderTimeout.isPositive(), String.format( + "Check timeout value %dms is positive", remainderTimeout.toMillis())); - WatchKey key = ThrowingSupplier.toSupplier(() -> { - return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS); - }).get(); - if (key == null) { - if (Files.exists(fileToWaitFor)) { - trace(String.format( - "File [%s] is available after poll timeout expired", - fileToWaitFor)); - return; + WatchKey key = ThrowingSupplier.toSupplier(() -> { + return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS); + }).get(); + if (key == null) { + if (Files.exists(fileToWaitFor)) { + trace(String.format( + "File [%s] is available after poll timeout expired", + fileToWaitFor)); + return; + } + assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis())); } - assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis())); - } - for (WatchEvent event : key.pollEvents()) { - if (event.kind() == StandardWatchEventKinds.OVERFLOW) { - continue; + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + Path contextPath = (Path) event.context(); + if (Files.exists(fileToWaitFor) && Files.isSameFile(watchDirectory.resolve(contextPath), fileToWaitFor)) { + trace(String.format("File [%s] is available", fileToWaitFor)); + return; + } } - Path contextPath = (Path) event.context(); - if (Files.isSameFile(watchDirectory.resolve(contextPath), - fileToWaitFor)) { - trace(String.format("File [%s] is available", fileToWaitFor)); - return; - } - } - if (!key.reset()) { - assertUnexpected("Watch key invalidated"); + if (!key.reset()) { + assertUnexpected("Watch key invalidated"); + } } } } diff --git a/test/jdk/tools/jpackage/resources/Win8365790Test.ps1 b/test/jdk/tools/jpackage/resources/Win8365790Test.ps1 new file mode 100644 index 00000000000..3a7d8c9a90b --- /dev/null +++ b/test/jdk/tools/jpackage/resources/Win8365790Test.ps1 @@ -0,0 +1,83 @@ +# +# 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. +# + +param ( + # Path to executable to start. + [Parameter(Mandatory=$true)] + [string]$Executable, + + # Timeout to wait after the executable has been started. + [Parameter(Mandatory=$true)] + [double]$TimeoutSeconds +) + +$type = @{ + TypeDefinition = @' +using System; +using System.Runtime.InteropServices; + +namespace Stuff { + + internal struct Details { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); + } + + public struct Facade { + public static void GenerateConsoleCtrlEvent() { + if (!Details.GenerateConsoleCtrlEvent(0, 0)) { + reportLastErrorAndExit("GenerateConsoleCtrlEvent"); + } + } + + internal static void reportLastErrorAndExit(String func) { + int errorCode = Marshal.GetLastWin32Error(); + Console.Error.WriteLine(func + " function failed with error code: " + errorCode); + Environment.Exit(100); + } + } +} +'@ +} +Add-Type @type + +Set-PSDebug -Trace 2 + +# Launch the target executable. +# `-NoNewWindow` parameter will attach the started process to the existing console. +$childProc = Start-Process -PassThru -NoNewWindow $Executable + +# Wait a bit to let the started process complete initialization. +Start-Sleep -Seconds $TimeoutSeconds + +# Call GenerateConsoleCtrlEvent to send a CTRL+C event to the launched executable. +# CTRL+C event will be sent to all processes attached to the console of the current process, +# i.e., it will be sent to this PowerShell process and to the started $Executable process because +# it was configured to attach to the existing console (the console of this PowerShell process). +[Stuff.Facade]::GenerateConsoleCtrlEvent() + +# Wait for child process termination +Wait-Process -InputObject $childProc + +Exit 0 diff --git a/test/jdk/tools/jpackage/windows/Win8365790Test.java b/test/jdk/tools/jpackage/windows/Win8365790Test.java new file mode 100644 index 00000000000..6376a16cecc --- /dev/null +++ b/test/jdk/tools/jpackage/windows/Win8365790Test.java @@ -0,0 +1,128 @@ +/* + * 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. + */ + +import static jdk.jpackage.test.HelloApp.configureAndExecute; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import jdk.jpackage.test.AdditionalLauncher; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.CfgFile; +import jdk.jpackage.test.Executor; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.LauncherVerifier; +import jdk.jpackage.test.TKit; + +/** + * Test the child process has a chance to handle Ctrl+C signal. + */ + +/* + * @test + * @summary Test case for JDK-8365790 + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @build Win8365790Test + * @requires (os.family == "windows") + * @run main/othervm/timeout=100 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=Win8365790Test + */ +public class Win8365790Test { + + @Test + public void test() throws InterruptedException, IOException { + + var outputDir = TKit.createTempDirectory("response-dir"); + + var mainOutputFile = outputDir.resolve("output.txt"); + var mainTraceFile = outputDir.resolve("trace.txt"); + + var probeOutputFile = outputDir.resolve("probe-output.txt"); + var probeTraceFile = outputDir.resolve("probe-trace.txt"); + + var cmd = JPackageCommand + .helloAppImage(TEST_APP_JAVA + "*UseShutdownHook") + .ignoreFakeRuntime() + .addArguments("--java-options", "-Djpackage.test.trace-file=" + mainTraceFile.toString()) + .addArguments("--arguments", mainOutputFile.toString()) + .addArguments("--arguments", Long.toString(Duration.ofSeconds(TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS).getSeconds())); + + new AdditionalLauncher("probe") + .withoutVerifyActions(LauncherVerifier.Action.values()) + .addJavaOptions("-Djpackage.test.trace-file=" + probeTraceFile.toString()) + .addDefaultArguments(probeOutputFile.toString(), Long.toString(Duration.ofSeconds(TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS).getSeconds())) + .applyTo(cmd); + + cmd.executeAndAssertImageCreated(); + + cmd.readLauncherCfgFile("probe") + .add(new CfgFile().addValue("Application", "win.norestart", Boolean.TRUE.toString())) + .save(cmd.appLauncherCfgPath("probe")); + + // Try Ctrl+C signal on a launcher with disabled restart functionality. + // It will create a single launcher process instead of the parent and the child processes. + // Ctrl+C always worked for launcher with disabled restart functionality. + var probeOutput = runLauncher(cmd, "probe", probeTraceFile, probeOutputFile); + + if (!probeOutput.equals("shutdown hook executed")) { + // Ctrl+C signal didn't make it. Test environment doesn't support Ctrl+C signal + // delivery from the prowershell process to a child process, don't run the main + // test. + TKit.throwSkippedException( + "The environment does NOT support Ctrl+C signal delivery from the prowershell process to a child process"); + } + + var mainOutput = runLauncher(cmd, null, mainTraceFile, mainOutputFile); + + TKit.assertEquals("shutdown hook executed", mainOutput, "Check shutdown hook executed"); + } + + private static String runLauncher(JPackageCommand cmd, String launcherName, Path traceFile, Path outputFile) throws IOException { + // Launch the specified launcher and send Ctrl+C signal to it. + Thread.ofVirtual().start(() -> { + configureAndExecute(0, Executor.of("powershell", "-NonInteractive", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Unrestricted") + .addArgument("-File").addArgument(TEST_PS1) + .addArguments("-TimeoutSeconds", Long.toString(Duration.ofSeconds(5).getSeconds())) + .addArgument("-Executable").addArgument(cmd.appLauncherPath(launcherName)) + .dumpOutput()); + }); + + TKit.waitForFileCreated(traceFile, Duration.ofSeconds(20), Duration.ofSeconds(2)); + + try { + TKit.waitForFileCreated(outputFile, Duration.ofSeconds(TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS * 2), Duration.ofSeconds(2)); + } finally { + TKit.traceFileContents(traceFile, "Test app trace"); + } + + TKit.assertFileExists(outputFile); + return Files.readString(outputFile); + } + + private static final long TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS = 30; + + private static final Path TEST_APP_JAVA = TKit.TEST_SRC_ROOT.resolve("apps/UseShutdownHook.java"); + private static final Path TEST_PS1 = TKit.TEST_SRC_ROOT.resolve(Path.of("resources/Win8365790Test.ps1")).normalize(); +} diff --git a/test/langtools/TEST.ROOT b/test/langtools/TEST.ROOT index e3bd3b74bcf..1aaaa7dffe1 100644 --- a/test/langtools/TEST.ROOT +++ b/test/langtools/TEST.ROOT @@ -15,7 +15,7 @@ keys=intermittent randomness needs-src needs-src-jdk_javadoc groups=TEST.groups # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Use new module options useNewOptions=true 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/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/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/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/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/lib-test/TEST.ROOT b/test/lib-test/TEST.ROOT index 162e6e15ec2..5710e2e9528 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+2 # Allow querying of various System properties in @requires clauses requires.extraPropDefns = ../jtreg-ext/requires/VMProps.java 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); + } + } + } +}