diff --git a/.sai.json b/.sai.json index 546c355b91..d1fe05b8bd 100644 --- a/.sai.json +++ b/.sai.json @@ -65,6 +65,15 @@ "cd build && SAI_CPACK=\"-G DEB\" ${cpack}" ] }, + + "linux-debian13/x86_64-amd/gcc": { + "default": false, + "build": [ + "${cmake}" + ] + }, + + # "netbsd/aarch64BE-bcm2837-a53/gcc": { # "default": false, # "build": [ @@ -309,6 +318,11 @@ "platforms": "none, coverity/x86_64/gcc", "cpack": "export STAMP=`git log -1 --pretty=format:%h` && rm -f libwebsockets.tar.xz && tar cJvf libwebsockets.tar.xz cov-int && script -e -q -c \"cat /etc/coverity/secrets.sh | lws-minimal-http-client-post-form --h1 https://scan.coverity.com:443/builds?project=warmcat%2Flibwebsockets --form file=@libwebsockets.tar.xz --form version=${STAMP} --form 'description=lws qa'\" /dev/null", "branches": "coverity" + }, + + "qir": { + "cmake": "cd /opt/quic-interop-runner ; source venv/bin/activate ; rm -rf logs_* ; export QIR_LWS_BRANCH=main ; cd /opt/quic-interop-runner/lws ; if [ \"`docker ps -q`\" ] ; then docker kill $(docker ps -q) ; fi ; docker system prune -a --volumes -f && docker build --build-arg QIR_LWS_BRANCH=${QIR_LWS_BRANCH} -t lws-quic-interop . --no-cache && cd .. && python3 run.py -c lws -s lws --delete-successful-logs ; python3 run.py -p webtransport -c lws -s lws --delete-successful-logs -t handshake,transfer-unidirectional-receive,transfer-unidirectional-send,transfer-bidirectional-receive,transfer-bidirectional-send,transfer-datagram-receive,transfer-datagram-send", + "platforms": "none, linux-debian13/x86_64-amd/gcc" } # , diff --git a/CMakeLists-implied-options.txt b/CMakeLists-implied-options.txt index 6170f4675b..fca6ad5ecc 100644 --- a/CMakeLists-implied-options.txt +++ b/CMakeLists-implied-options.txt @@ -185,24 +185,40 @@ endif() if (LWS_WITH_WEBRTC_MIXER) set(LWS_WITH_WEBRTC 1) - set(LWS_WITH_ALSA 1 CACHE BOOL "Enable alsa audio example" FORCE) - set(LWS_WITH_OPUS 1 CACHE BOOL "Enable opus audio codec" FORCE) - set(LWS_WITH_GSTREAMER 1 CACHE BOOL "Enable gstreamer" FORCE) + if (NOT DEFINED LWS_WITH_ALSA) + set(LWS_WITH_ALSA 1 CACHE BOOL "Enable alsa audio example" FORCE) + endif() + if (NOT DEFINED LWS_WITH_OPUS) + set(LWS_WITH_OPUS 1 CACHE BOOL "Enable opus audio codec" FORCE) + endif() + if (NOT DEFINED LWS_WITH_GSTREAMER) + set(LWS_WITH_GSTREAMER 1 CACHE BOOL "Enable gstreamer" FORCE) + endif() endif() if (LWS_WITH_WEBRTC) set(LWS_WITH_UDP 1) set(LWS_WITH_DTLS 1) set(LWS_WITH_PLUGINS 1 CACHE BOOL "Enable plugins" FORCE) - set(LWS_WITH_V4L2 1) - set(LWS_WITH_LIBV4L2 1) - set(LWS_WITH_DRM 1) + if (NOT DEFINED LWS_WITH_V4L2) + set(LWS_WITH_V4L2 1) + endif() + if (NOT DEFINED LWS_WITH_LIBV4L2) + set(LWS_WITH_LIBV4L2 1) + endif() + if (NOT DEFINED LWS_WITH_DRM) + set(LWS_WITH_DRM 1) + endif() set(LWS_WITH_GENCRYPTO 1) set(LWS_WITH_JOSE 1) set(LWS_WITH_NETWORK 1) set(LWS_WITH_CLIENT 1) - set(LWS_WITH_ALSA 1) - set(LWS_WITH_OPUS 1) + if (NOT DEFINED LWS_WITH_ALSA) + set(LWS_WITH_ALSA 1) + endif() + if (NOT DEFINED LWS_WITH_OPUS) + set(LWS_WITH_OPUS 1) + endif() endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index 7065691645..ed9df1b266 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -680,6 +680,20 @@ set(LWS_INSTALL_INCLUDE_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installati set(LWS_INSTALL_EXAMPLES_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for example files") set(LWS_INSTALL_PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/share/libwebsockets-test-server/plugins" CACHE PATH "Installation directory for plugins") +if(IS_ABSOLUTE "${LWS_INSTALL_LIB_DIR}") + set(LWS_ABSOLUTE_INSTALL_LIBDIR "${LWS_INSTALL_LIB_DIR}") + set(PKG_CONFIG_LIBDIR "${LWS_INSTALL_LIB_DIR}") +else() + set(LWS_ABSOLUTE_INSTALL_LIBDIR "${CMAKE_INSTALL_PREFIX}/${LWS_INSTALL_LIB_DIR}") + set(PKG_CONFIG_LIBDIR "\${exec_prefix}/${LWS_INSTALL_LIB_DIR}") +endif() + +if(IS_ABSOLUTE "${LWS_INSTALL_INCLUDE_DIR}") + set(PKG_CONFIG_INCLUDEDIR "${LWS_INSTALL_INCLUDE_DIR}") +else() + set(PKG_CONFIG_INCLUDEDIR "\${prefix}/${LWS_INSTALL_INCLUDE_DIR}") +endif() + # if you gave LWS_WITH_MINIZ, point to MINIZ here if not found # automatically @@ -1483,7 +1497,7 @@ file(RELATIVE_PATH "${LWS_ABSOLUTE_INSTALL_INCLUDE_DIR}") # Calculate the relative directory from the cmake dir. if (DEFINED REL_INCLUDE_DIR) - set(LWS__INCLUDE_DIRS "${LWS_CMAKE_DIR}/${REL_INCLUDE_DIR}") + set(LWS__INCLUDE_DIRS "\${LWS_CMAKE_DIR}/${REL_INCLUDE_DIR}") endif() set(STRIPPED_LIB_LIST_AT_END "") diff --git a/README.md b/README.md index 4065041c3d..8ba49580bb 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ ** NEW features available on main ** - - Support for SChannel (windows native TLS, no need for OpenSSL build!), GnuTLS, and BearSSL added - - QUIC + H3 + Webtranspoer implementation, using aws-lc, wolfssl, boringssl, libressl, gnutls, and schannel (OpenSSL is h1/h2 -only, mbedtls can do it via a patch) + - Support for SChannel (windows native TLS, no need for OpenSSL build!), GnuTLS, openHiTLS and BearSSL added + - QUIC + H3 + Webtransport implementation, using aws-lc, wolfssl, boringssl, libressl, gnutls, and schannel (OpenSSL is h1/h2 -only; mbedtls can do it via a patch) |LWS version|Platform|Protocols|Default TLS| |---|---|---|---| @@ -33,7 +33,7 @@ quic/h3 is enabled for build by default... necessitating GnuTLS instead of OpenS | **openHiTLS** | **Yes** | **Yes** | **No** | **Yes** | **Yes** | **Yes** | Yes (not SRTP) | **Yes** | **Yes** | **Yes** | -\* *Note: 1) Upstream OpenSSL does not provide the necessary QUIC TLS API (`SSL_set_quic_method`) to act as a cryptographic engine for LWS's QUIC transport. If you need QUIC/HTTP3 support, we recommend using BoringSSL, GnuTLS, WolfSSL, or the `quictls` fork of OpenSSL.* +\* *Note: 1) Upstream OpenSSL does not provide the necessary QUIC TLS API (`SSL_set_quic_method`) to act as a cryptographic engine for LWS's QUIC transport. If you need QUIC/HTTP3 support, we recommend using BoringSSL or GnuTLS.* \* *Note: 2) openHiTLS does not provide the necessary QUIC TLS API * - DHT support built-in: `-DLWS_WITH_DHT=1` diff --git a/READMEs/README.lwsws.md b/READMEs/README.lwsws.md index 7702ff87f5..656c4e85f4 100644 --- a/READMEs/README.lwsws.md +++ b/READMEs/README.lwsws.md @@ -217,6 +217,16 @@ to be selected using "raw": "1" See also "apply-listen-accept" below. +## Care about redirecting the trailing / + +In lws the mount applies at the URL folder, so a mount at /myapp actually works starting from /myapp/. + +This means you often need to add an additional redirect mount to do the last step for the user who +may have typed .../myapp, to get them to .../myapp/ where the app actually takes over. + + "mountpoint": "/git", + "origin": ">https://warmcat.com/git/", + ## Lwsws Other vhost options - If the three options `host-ssl-cert`, `host-ssl-ca` and `host-ssl-key` are given, then the vhost supports SSL. @@ -479,7 +489,7 @@ Auth before the ws upgrade, this is also possible. In this case, the The mounts in lws allow you to stack up other plugins that run "before" the main mountpoint. There are two "interceptor plugins" provided which can be useful for this, -`lws_login` and `lws_captcha_ratelimit` +`lws_login` and `lws_captcha_ratelimit`, you can create your own based on these. To indicate you want to use an interceptor plugin for a mount, you add an "interceptor-path" entry to the original mount definition, pointing to the diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in index d06c38c7cb..b87241d304 100644 --- a/cmake/lws_config.h.in +++ b/cmake/lws_config.h.in @@ -7,7 +7,7 @@ #endif #define LWS_INSTALL_DATADIR "${CMAKE_INSTALL_PREFIX}/share" -#define LWS_INSTALL_LIBDIR "${CMAKE_INSTALL_PREFIX}/${LWS_INSTALL_LIB_DIR}" +#define LWS_INSTALL_LIBDIR "${LWS_ABSOLUTE_INSTALL_LIBDIR}" #define LWS_PLUGIN_DIR "${LWS_INSTALL_PLUGIN_DIR}" #define LWS_LIBRARY_VERSION_MAJOR ${LWS_LIBRARY_VERSION_MAJOR} #define LWS_LIBRARY_VERSION_MINOR ${LWS_LIBRARY_VERSION_MINOR} diff --git a/include/libwebsockets.h b/include/libwebsockets.h index 5cf39a6019..cdddc47818 100644 --- a/include/libwebsockets.h +++ b/include/libwebsockets.h @@ -869,6 +869,7 @@ lws_fx_string(const lws_fx_t *a, char *buf, size_t size); #include #if defined(LWS_ROLE_H3) #include +#include #endif #include #include diff --git a/include/libwebsockets/lws-client.h b/include/libwebsockets/lws-client.h index b7c572d69d..c96261b9c4 100644 --- a/include/libwebsockets/lws-client.h +++ b/include/libwebsockets/lws-client.h @@ -227,6 +227,10 @@ struct lws_client_connect_info { * to the client connection. */ + struct lws *quic_migrate_from_wsi; + /**< If set, this connection acts as a Make-Before-Break migration probe for + * the existing QUIC network connection in quic_migrate_from_wsi. */ + uint8_t priority; /**< 0 means normal priority... otherwise sets the IP priority on * packets coming from this connection, from 1 - 7. Setting 7 diff --git a/include/libwebsockets/lws-context-vhost.h b/include/libwebsockets/lws-context-vhost.h index 8be14a4d13..a6a0efa3d4 100644 --- a/include/libwebsockets/lws-context-vhost.h +++ b/include/libwebsockets/lws-context-vhost.h @@ -221,6 +221,10 @@ #define LWS_SERVER_OPTION_GLIB (1ll << 33) /**< (CTX) Use glib event loop */ +#define LWS_SERVER_OPTION_QUIC_FORCE_RETRY (1ll << 52) + /**< (VH) For QUIC, force the server to always send a Retry packet. + * Normally used only for testing. */ + #define LWS_SERVER_OPTION_H2_PRIOR_KNOWLEDGE (1ll << 34) /**< (VH) Tell the vhost to treat plain text http connections as * H2 with prior knowledge (no upgrade request involved) @@ -284,6 +288,12 @@ #define LWS_SERVER_OPTION_QUIC_PAD_CRYPTO (1ll << 49) /**< (VH) Pad the QUIC handshake crypto data to artificially hit anti-amplification limits */ +#define LWS_SERVER_OPTION_QUIC_EARLY_KEY_UPDATE (1ll << 51) + /**< CONTEXT: For testing. Trigger a QUIC early key update */ + +#define LWS_SERVER_OPTION_QUIC_LATEST_VERSION (1ll << 50) + /**< (CTX) Force client to initiate QUIC connections using the latest supported version (e.g. v2) */ + /****** add new things just above ---^ ******/ @@ -1102,7 +1112,19 @@ struct lws_context_creation_info { const struct lws_cc_ops *quic_cc_ops; /**< CONTEXT: QUIC congestion control algorithm ops to use. If NULL, defaults to &lws_cc_ops_newreno. */ + const char *quic_preferred_addresses; + /**< VHOST: Comma-separated list of IPv4 and/or IPv6 preferred addresses (e.g., "1.2.3.4:443,[2001:db8::1]:443") + * to send to the client during the handshake to facilitate Connection Migration. */ + + uint32_t quic_initial_cwnd; + /**< CONTEXT: 0 for default (10 * MTU), or the desired initial congestion window in bytes */ + + uint32_t quic_0rtt_max_size; + /**< CONTEXT: 0 for default (4096), or the desired max 0-RTT early data size */ + +#if !defined(__STRICT_ANSI__) void *_unused[1]; /**< dummy */ +#endif }; /** diff --git a/include/libwebsockets/lws-lecp.h b/include/libwebsockets/lws-lecp.h index 8133e4b148..76456bbf2b 100644 --- a/include/libwebsockets/lws-lecp.h +++ b/include/libwebsockets/lws-lecp.h @@ -290,6 +290,7 @@ struct lecp_ctx { uint8_t pst_sp; /* parsing stack head */ uint8_t outer_array; uint8_t cbor_pos; + uint8_t cbor_len; uint8_t literal_cbor_report; char present; /* temp for cb reason to use */ diff --git a/include/libwebsockets/lws-misc.h b/include/libwebsockets/lws-misc.h index f0cd02f37c..1c150ed776 100644 --- a/include/libwebsockets/lws-misc.h +++ b/include/libwebsockets/lws-misc.h @@ -712,6 +712,15 @@ lws_get_library_version(void); LWS_VISIBLE LWS_EXTERN void * lws_wsi_user(struct lws *wsi); +/** + * lws_ensure_user_space() - allocate user space for wsi if not already allocated + * \param wsi: lws connection + * + * returns 0 if user space exists or was allocated, else non-zero. + */ +LWS_VISIBLE LWS_EXTERN int +lws_ensure_user_space(struct lws *wsi); + /** * lws_wsi_tsi() - get the service thread index the wsi is bound to * \param wsi: lws connection diff --git a/include/libwebsockets/lws-quic.h b/include/libwebsockets/lws-quic.h index 717ba108f8..8580bd536a 100644 --- a/include/libwebsockets/lws-quic.h +++ b/include/libwebsockets/lws-quic.h @@ -159,6 +159,7 @@ struct lws_cc_ops { void (*on_sent)(struct lws *nwsi, size_t bytes); void (*on_ack)(struct lws *nwsi, size_t bytes_acked, lws_usec_t rtt); void (*on_loss)(struct lws *nwsi, size_t bytes_lost); + void (*on_discard)(struct lws *nwsi, size_t bytes_discarded); int (*can_send)(struct lws *nwsi, size_t bytes); lws_usec_t (*get_pacing_delay)(struct lws *nwsi, size_t bytes_to_send); }; diff --git a/include/libwebsockets/lws-webtransport.h b/include/libwebsockets/lws-webtransport.h index e593fd68b7..9aac088dd2 100644 --- a/include/libwebsockets/lws-webtransport.h +++ b/include/libwebsockets/lws-webtransport.h @@ -41,5 +41,14 @@ lws_wt_create_stream(struct lws *wsi_session, int unidi); LWS_VISIBLE LWS_EXTERN int lws_wt_is_session(struct lws *wsi); +LWS_VISIBLE LWS_EXTERN int +lws_wt_is_unidi(struct lws *wsi); + +LWS_VISIBLE LWS_EXTERN struct lws * +lws_wt_create_stream_from_child(struct lws *child_wsi, int unidi); + +LWS_VISIBLE LWS_EXTERN struct lws * +lws_wt_get_session_wsi(struct lws *wsi); + #endif /* LWS_ROLE_WT */ #endif /* _LWS_WEBTRANSPORT_H */ diff --git a/include/libwebsockets/lws-x509.h b/include/libwebsockets/lws-x509.h index d9946a352a..0f9074f07a 100644 --- a/include/libwebsockets/lws-x509.h +++ b/include/libwebsockets/lws-x509.h @@ -240,6 +240,20 @@ LWS_VISIBLE LWS_EXTERN int lws_x509_info(struct lws_x509_cert *x509, enum lws_tls_cert_info type, union lws_tls_cert_info_results *buf, size_t len); +/** + * lws_x509_cert_fingerprint() - Calculate the fingerprint of a certificate + * + * \param x509: the certificate to fingerprint + * \param type: the hash type (e.g., LWS_GENHASH_TYPE_SHA256) + * \param buf: buffer to receive the binary hash + * \param len: max length of the buffer + * + * Returns the hash length on success, or -1 on failure. + */ +LWS_VISIBLE LWS_EXTERN int +lws_x509_cert_fingerprint(struct lws_x509_cert *x509, int type, + uint8_t *buf, size_t len); + /** * lws_tls_peer_cert_info() - get information from the peer's TLS cert * diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index fedb0ed95f..1e748882d0 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -429,8 +429,8 @@ endif() file(WRITE "${PROJECT_BINARY_DIR}/libwebsockets.pc" "prefix=\"${CMAKE_INSTALL_PREFIX}\" exec_prefix=\${prefix} -libdir=\${exec_prefix}/${LWS_INSTALL_LIB_DIR} -includedir=\${prefix}/${LWS_INSTALL_INCLUDE_DIR} +libdir=${PKG_CONFIG_LIBDIR} +includedir=${PKG_CONFIG_INCLUDEDIR} Name: libwebsockets Description: Websockets server and client library @@ -451,8 +451,8 @@ endif() file(WRITE "${PROJECT_BINARY_DIR}/libwebsockets_static.pc" "prefix=\"${CMAKE_INSTALL_PREFIX}\" exec_prefix=\${prefix} -libdir=\${exec_prefix}/${LWS_INSTALL_LIB_DIR} -includedir=\${prefix}/${LWS_INSTALL_INCLUDE_DIR} +libdir=${PKG_CONFIG_LIBDIR} +includedir=${PKG_CONFIG_INCLUDEDIR} Name: libwebsockets_static Description: Websockets server and client static library diff --git a/lib/core-net/adopt.c b/lib/core-net/adopt.c index a797597098..34d5661ee5 100644 --- a/lib/core-net/adopt.c +++ b/lib/core-net/adopt.c @@ -805,6 +805,16 @@ lws_create_adopt_udp2(struct lws *wsi, const char *ads, if (sock.sockfd == LWS_SOCK_INVALID) goto resume; + { + int opt = 4 * 1024 * 1024; + if (setsockopt(sock.sockfd, SOL_SOCKET, SO_RCVBUF, (const char *)&opt, sizeof(opt)) < 0) { + lwsl_wsi_warn(wsi, "Failed to set SO_RCVBUF"); + } + if (setsockopt(sock.sockfd, SOL_SOCKET, SO_SNDBUF, (const char *)&opt, sizeof(opt)) < 0) { + lwsl_wsi_warn(wsi, "Failed to set SO_SNDBUF"); + } + } + if (lws_plat_apply_FD_CLOEXEC((int)sock.sockfd) || lws_plat_set_nonblocking(sock.sockfd)) { compatible_close(sock.sockfd); @@ -974,6 +984,16 @@ lws_create_adopt_udp2(struct lws *wsi, const char *ads, if (sock.sockfd == LWS_SOCK_INVALID) goto bail; + { + int opt = 4 * 1024 * 1024; + if (setsockopt(sock.sockfd, SOL_SOCKET, SO_RCVBUF, (const char *)&opt, sizeof(opt)) < 0) { + lwsl_wsi_warn(wsi, "Failed to set SO_RCVBUF"); + } + if (setsockopt(sock.sockfd, SOL_SOCKET, SO_SNDBUF, (const char *)&opt, sizeof(opt)) < 0) { + lwsl_wsi_warn(wsi, "Failed to set SO_SNDBUF"); + } + } + if (lws_plat_apply_FD_CLOEXEC((int)sock.sockfd) || lws_plat_set_nonblocking(sock.sockfd)) { compatible_close(sock.sockfd); diff --git a/lib/core-net/client/connect.c b/lib/core-net/client/connect.c index 0223acf6b3..ba48f6ba09 100644 --- a/lib/core-net/client/connect.c +++ b/lib/core-net/client/connect.c @@ -208,6 +208,10 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) wsi->client_proxy_onward = !!(i->ssl_connection & LCCSCF_SECSTREAM_PROXY_ONWARD); #endif +#if defined(LWS_ROLE_QUIC) + wsi->quic.migrate_from_wsi = i->quic_migrate_from_wsi; +#endif + #if defined(LWS_WITH_SYS_FAULT_INJECTION) wsi->fic.name = "wsi"; if (i->fic.fi_owner.count) @@ -250,6 +254,8 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) wsi->keep_warm_secs = 5; wsi->flags = i->ssl_connection; + if (i->context->options & LWS_SERVER_OPTION_ALLOW_EARLY_DATA) + wsi->flags |= LCCSCF_ALLOW_EARLY_DATA; wsi->c_pri = i->priority; @@ -347,7 +353,7 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) } #if defined(LWS_WITH_TLS) - wsi->tls.use_ssl = (unsigned int)i->ssl_connection; + wsi->tls.use_ssl = (unsigned int)wsi->flags; #else if (i->ssl_connection & LCCSCF_USE_SSL) { lwsl_wsi_err(wsi, "lws not configured for tls"); diff --git a/lib/core-net/client/connect3.c b/lib/core-net/client/connect3.c index ac0773b045..dfaf64c7c9 100644 --- a/lib/core-net/client/connect3.c +++ b/lib/core-net/client/connect3.c @@ -78,6 +78,25 @@ lws_client_h3_grace_cb(lws_sorted_usec_list_t *sul) { struct lws *wsi = lws_container_of(sul, struct lws, sul_h3_grace); + if (!wsi->role_ops || strcmp(wsi->role_ops->name, "quic") != 0) + return; + + int first_valid = -1; + for (int i = 0; i < wsi->parallel_count; i++) { + if (wsi->parallel_conns[i].is_valid) { + first_valid = i; + break; + } + } + + if (first_valid == -1 && !wsi->dns_sorted_list.count) { + /* There are no parallel TCP sockets, and no more DNS results to try! + * We cannot fallback to TCP. Do not abort the QUIC race, just + * let it run until the overall connect timeout expires. */ + lwsl_wsi_notice(wsi, "H3 grace timer expired, but no TCP fallback available. Keeping QUIC."); + return; + } + lwsl_wsi_notice(wsi, "H3 grace timer expired, abandoning QUIC race"); /* Mark H3 as FAILED in cache with 5s TTL */ @@ -91,41 +110,32 @@ lws_client_h3_grace_cb(lws_sorted_usec_list_t *sul) } /* Abort QUIC and revert to TCP */ - if (wsi->role_ops && strcmp(wsi->role_ops->name, "quic") == 0) { - if (lws_socket_is_valid(wsi->desc.sockfd)) { - struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; - lws_pt_lock(pt, __func__); - __remove_wsi_socket_from_fds(wsi); - lws_pt_unlock(pt); - compatible_close(wsi->desc.sockfd); - wsi->desc.sockfd = LWS_SOCK_INVALID; - } + if (lws_socket_is_valid(wsi->desc.sockfd)) { + struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; + lws_pt_lock(pt, __func__); + __remove_wsi_socket_from_fds(wsi); + lws_pt_unlock(pt); + compatible_close(wsi->desc.sockfd); + wsi->desc.sockfd = LWS_SOCK_INVALID; + } - if (lws_rops_fidx(wsi->role_ops, LWS_ROPS_close_kill_connection)) - lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_close_kill_connection).close_kill_connection(wsi, LWS_CLOSE_STATUS_NOSTATUS); + if (lws_rops_fidx(wsi->role_ops, LWS_ROPS_close_kill_connection)) + lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_close_kill_connection).close_kill_connection(wsi, LWS_CLOSE_STATUS_NOSTATUS); - /* Promote the first parallel TCP connection */ - int first_valid = -1; - for (int i = 0; i < wsi->parallel_count; i++) { - if (wsi->parallel_conns[i].is_valid) { - first_valid = i; - break; - } - } - if (first_valid != -1) { - const struct lws_role_ops *r = lws_role_by_name("h2"); - if (!r) r = lws_role_by_name("h1"); - if (r) { - lws_role_transition(wsi, LWSIFR_CLIENT, LRS_WAITING_CONNECT, r); - } - wsi->desc.sockfd = wsi->parallel_conns[first_valid].desc.sockfd; - wsi->position_in_fds_table = wsi->parallel_conns[first_valid].position_in_fds_table; - wsi->parallel_conns[first_valid].is_valid = 0; - /* We changed the primary fd, the event loop will trigger POLLOUT if it's connected */ - } else { - /* No TCP sockets survived? Fail connection. */ - lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL); + /* Promote the first parallel TCP connection */ + if (first_valid != -1) { + const struct lws_role_ops *r = lws_role_by_name("h2"); + if (!r) r = lws_role_by_name("h1"); + if (r) { + lws_role_transition(wsi, LWSIFR_CLIENT, LRS_WAITING_CONNECT, r); } + wsi->desc.sockfd = wsi->parallel_conns[first_valid].desc.sockfd; + wsi->position_in_fds_table = wsi->parallel_conns[first_valid].position_in_fds_table; + wsi->parallel_conns[first_valid].is_valid = 0; + /* We changed the primary fd, the event loop will trigger POLLOUT if it's connected */ + } else { + /* No TCP sockets survived? Fail connection. */ + lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL); } } @@ -271,6 +281,16 @@ lws_remove_parallel_fd_safely(struct lws *wsi, int pidx) int saved_pos = wsi->position_in_fds_table; lws_sock_file_fd_type saved_fd = wsi->desc; + /* + * An attempt that already failed (eg, synchronous connect() failure + * like EHOSTUNREACH on an unroutable AAAA result) was invalidated and + * its socket closed at the failure site; its fds-table position was + * never assigned. Removing it again would use the bogus position to + * remove an unrelated fds slot and double-close a possibly-reused fd. + */ + if (!wsi->parallel_conns[pidx].is_valid || hole_pos == LWS_NO_FDS_POS) + return; + wsi->desc.sockfd = wsi->parallel_conns[pidx].desc.sockfd; wsi->position_in_fds_table = hole_pos; @@ -477,6 +497,7 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, promote_parallel_fd(wsi, pidx); /* close all remaining parallel */ for (m = 0; m < wsi->parallel_count; m++) + if (wsi->parallel_conns[m].is_valid) /* A parallel socket failed. Just close it and remove from fds */ lws_remove_parallel_fd_safely(wsi, m); wsi->parallel_count = 0; @@ -592,8 +613,10 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, lws_dll2_remove(&curr->list); wsi->sa46_peer = curr->dest; #if defined(LWS_WITH_UDP) - if (wsi->udp) + if (wsi->udp) { wsi->udp->sa46 = curr->dest; + sa46_sockport(&wsi->udp->sa46, htons(port)); + } #endif #if defined(LWS_WITH_ROUTING) wsi->peer_route_uidx = curr->uidx; @@ -646,6 +669,15 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, want_udp = (wsi->role_ops && !strcmp(wsi->role_ops->name, "quic") && !is_parallel); #endif new_fd = socket(wsi->sa46_peer.sa4.sin_family, want_udp ? SOCK_DGRAM : SOCK_STREAM, 0); + if (lws_socket_is_valid(new_fd) && want_udp) { + int opt = 4 * 1024 * 1024; + if (setsockopt(new_fd, SOL_SOCKET, SO_RCVBUF, (const char *)&opt, sizeof(opt)) < 0) { + lwsl_wsi_warn(wsi, "Failed to set SO_RCVBUF"); + } + if (setsockopt(new_fd, SOL_SOCKET, SO_SNDBUF, (const char *)&opt, sizeof(opt)) < 0) { + lwsl_wsi_warn(wsi, "Failed to set SO_SNDBUF"); + } + } } if (!lws_socket_is_valid(new_fd)) { diff --git a/lib/core-net/client/connect4.c b/lib/core-net/client/connect4.c index 8d7eb750c9..cef4e434eb 100644 --- a/lib/core-net/client/connect4.c +++ b/lib/core-net/client/connect4.c @@ -135,7 +135,7 @@ lws_client_connect_4_established(struct lws *wsi, struct lws *wsi_piggyback, send_hs: -#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) +#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) || defined(LWS_ROLE_H3) if (wsi_piggyback && !lws_dll2_is_detached(&wsi->dll2_cli_txn_queue)) { /* @@ -250,7 +250,11 @@ lws_client_connect_4_established(struct lws *wsi, struct lws *wsi_piggyback, #if defined(LWS_ROLE_QUIC) if ((meth && !strcmp(meth, "QUIC")) || !strcmp(wsi->role_ops->name, "quic")) { - lwsi_set_state(wsi, LRS_WAITING_SSL); + if (wsi->quic.migrate_from_wsi) { + lwsi_set_state(wsi, LRS_ESTABLISHED); + } else { + lwsi_set_state(wsi, LRS_WAITING_SSL); + } wsi->hdr_parsing_completed = 1; lws_callback_on_writable(wsi); return wsi; diff --git a/lib/core-net/close.c b/lib/core-net/close.c index 246a672af6..725fd4cd6a 100644 --- a/lib/core-net/close.c +++ b/lib/core-net/close.c @@ -507,7 +507,14 @@ __lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason, #if defined(LWS_WITH_CLIENT) #if defined(LWS_WITH_TLS_SESSIONS) && defined(LWS_WITH_GNUTLS) - lws_tls_session_new_gnutls(wsi); + /* + * For multiplexed connections (H3/H2), the TLS session belongs + * to the network-level WSI, not to individual stream WSIs. Don't + * perform the (expensive) session ticket snapshot on every mux + * substream close — only on the actual network wsi. + */ + if (!wsi->client_mux_substream) + lws_tls_session_new_gnutls(wsi); #endif lws_free_set_NULL(wsi->cli_hostname_copy); diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h index 2898a5026f..4e55efd908 100644 --- a/lib/core-net/private-lib-core-net.h +++ b/lib/core-net/private-lib-core-net.h @@ -540,6 +540,8 @@ struct lws_vhost { uint64_t options; + const char *quic_preferred_addresses; + struct lws_context *context; struct lws_vhost *vhost_next; @@ -1358,6 +1360,9 @@ lws_service_flag_pending(struct lws_context *context, int tsi); int lws_has_buffered_out(struct lws *wsi); +int +lws_has_unsent_buffered_out(struct lws *wsi); + lws_handling_result_t LWS_WARN_UNUSED_RESULT lws_ws_client_rx_sm(struct lws *wsi, unsigned char c); diff --git a/lib/core-net/service.c b/lib/core-net/service.c index f957f0d7b1..9f69c5612d 100644 --- a/lib/core-net/service.c +++ b/lib/core-net/service.c @@ -148,7 +148,7 @@ lws_handle_POLLOUT_event(struct lws *wsi, struct lws_pollfd *pollfd) #if defined(LWS_WITH_CLIENT) /* Intercept POLLOUT for parallel sockets if we are racing H3 */ - if (pollfd && pollfd->fd != wsi->desc.sockfd) { + if (pollfd && wsi->parallel_count > 0 && pollfd->fd != wsi->desc.sockfd) { lws_client_connect_3_connect(wsi, NULL, NULL, 0, pollfd); return 0; } @@ -676,9 +676,9 @@ lws_service_flag_pending(struct lws_context *context, int tsi) struct lws *wsi = lws_container_of(d, struct lws, dll_buflist); if (!lws_is_flowcontrolled(wsi) && - lwsi_state(wsi) != LRS_DEFERRING_ACTION && - lwsi_state(wsi) != LRS_AWAITING_FILE_READ && - lwsi_state(wsi) != LRS_AWAITING_SSL_ACCEPT) { + lwsi_state(wsi) != LRS_DEFERRING_ACTION && + lwsi_state(wsi) != LRS_AWAITING_FILE_READ && + lwsi_state(wsi) != LRS_AWAITING_SSL_ACCEPT) { forced = 1; break; } @@ -898,17 +898,7 @@ _lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, case LWS_HPI_RET_PLEASE_CLOSE_ME: //lwsl_notice("%s: %s pollin says please close me\n", __func__, // wsi->role_ops->name); -close_and_handled_l: - lwsl_wsi_debug(wsi, "Close and handled"); - lws_close_free_wsi(wsi, LWS_CLOSE_STATUS_NOSTATUS, - "close_and_handled"); - /* - * pollfd may point to something else after the close - * due to pollfd swapping scheme on delete on some platforms - * we can't clear revents now because it'd be the wrong guy's - * revents - */ - return 1; + goto close_and_handled_l; default: assert(0); } @@ -938,6 +928,18 @@ _lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, } #endif return 0; + +close_and_handled_l: + lwsl_wsi_debug(wsi, "Close and handled"); + lws_close_free_wsi(wsi, LWS_CLOSE_STATUS_NOSTATUS, + "close_and_handled"); + /* + * pollfd may point to something else after the close + * due to pollfd swapping scheme on delete on some platforms + * we can't clear revents now because it'd be the wrong guy's + * revents + */ + return 1; } int diff --git a/lib/core-net/socks5-client.c b/lib/core-net/socks5-client.c index 3788d2363c..2fa7147554 100644 --- a/lib/core-net/socks5-client.c +++ b/lib/core-net/socks5-client.c @@ -323,33 +323,12 @@ lws_socks5c_handle_state(struct lws *wsi, struct lws_pollfd *pollfd, lwsl_wsi_client(wsi, "SOCKS password OK, sending connect"); if (lws_socks5c_generate_msg(wsi, SOCKS_MSG_CONNECT, &len)) { -socks_send_msg_fail_l: - *pcce = "socks gen msg fail"; - return LW5CHS_RET_BAIL3; + goto socks_send_msg_fail_l; } conn_mode = LRS_WAITING_SOCKS_CONNECT_REPLY; pending_timeout = PENDING_TIMEOUT_AWAITING_SOCKS_CONNECT_REPLY; -socks_send_l: - // lwsl_hexdump_notice(pt->serv_buf, len); - n = (int)send(wsi->desc.sockfd, (char *)pt->serv_buf, - LWS_POSIX_LENGTH_CAST(len), MSG_NOSIGNAL); - if (n < 0) { - lwsl_wsi_debug(wsi, "ERROR writing to socks proxy"); - *pcce = "socks write fail"; - return LW5CHS_RET_BAIL3; - } - - lws_set_timeout(wsi, (enum pending_timeout)pending_timeout, - (int)wsi->a.context->timeout_secs); - lwsi_set_state(wsi, (lws_wsi_state_t)conn_mode); - break; - -socks_reply_fail_l: - lwsl_wsi_err(wsi, "socks reply: v%d, err %d", - pt->serv_buf[0], pt->serv_buf[1]); - *pcce = "socks reply fail"; - return LW5CHS_RET_BAIL3; + goto socks_send_l; case LRS_WAITING_SOCKS_CONNECT_REPLY: if (pt->serv_buf[0] != SOCKS_VERSION_5 || @@ -377,4 +356,29 @@ lws_socks5c_handle_state(struct lws *wsi, struct lws_pollfd *pollfd, } return LW5CHS_RET_NOTHING; + +socks_send_l: + // lwsl_hexdump_notice(pt->serv_buf, len); + n = (int)send(wsi->desc.sockfd, (char *)pt->serv_buf, + LWS_POSIX_LENGTH_CAST(len), MSG_NOSIGNAL); + if (n < 0) { + lwsl_wsi_debug(wsi, "ERROR writing to socks proxy"); + *pcce = "socks write fail"; + return LW5CHS_RET_BAIL3; + } + + lws_set_timeout(wsi, (enum pending_timeout)pending_timeout, + (int)wsi->a.context->timeout_secs); + lwsi_set_state(wsi, (lws_wsi_state_t)conn_mode); + return LW5CHS_RET_NOTHING; + +socks_send_msg_fail_l: + *pcce = "socks gen msg fail"; + return LW5CHS_RET_BAIL3; + +socks_reply_fail_l: + lwsl_wsi_err(wsi, "socks reply: v%d, err %d", + pt->serv_buf[0], pt->serv_buf[1]); + *pcce = "socks reply fail"; + return LW5CHS_RET_BAIL3; } diff --git a/lib/core-net/vhost.c b/lib/core-net/vhost.c index f5eea5048b..c451a3a641 100644 --- a/lib/core-net/vhost.c +++ b/lib/core-net/vhost.c @@ -835,8 +835,9 @@ lws_create_vhost(struct lws_context *context, vh->count_protocols++) ; - vh->options = info->options; - vh->pvo = info->pvo; + vh->options = info->options; + vh->quic_preferred_addresses = info->quic_preferred_addresses; + vh->pvo = info->pvo; #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) vh->headers = info->headers; #endif @@ -879,8 +880,8 @@ lws_create_vhost(struct lws_context *context, if (info->tls1_3_plus_cipher_list) vh->tls.cfg_tls1_3_plus_cipher_list = lws_strdup(info->tls1_3_plus_cipher_list); #if defined(LWS_WITH_CLIENT) - if (info->client_ssl_cipher_list) - vh->tls.cfg_tls_client_cipher_list = lws_strdup(info->client_ssl_cipher_list); + if (info->client_ssl_cipher_list) + vh->tls.cfg_tls_client_cipher_list = lws_strdup(info->client_ssl_cipher_list); #endif if (info->tls_ciphers_iana) vh->tls.cfg_tls_ciphers_iana = lws_strdup(info->tls_ciphers_iana); @@ -2101,7 +2102,7 @@ lws_vhost_active_conns(struct lws *wsi, struct lws **nwsi, const char *adsin) * h2: if in usable state already: just use it without * going through the queue */ - if (w->client_h2_alpn && w->client_mux_migrated && + if (lwsi_role_h2(w) && w->client_h2_alpn && w->client_mux_migrated && (lwsi_state(w) == LRS_H2_WAITING_TO_SEND_HEADERS || lwsi_state(w) == LRS_ESTABLISHED || lwsi_state(w) == LRS_IDLING)) { @@ -2109,10 +2110,28 @@ lws_vhost_active_conns(struct lws *wsi, struct lws **nwsi, const char *adsin) lwsl_wsi_info(w, "just join h2 directly 0x%x", lwsi_state(w)); - if (lwsi_state(w) == LRS_IDLING) + if (lwsi_state(w) == LRS_IDLING) { _lws_generic_transaction_completed_active_conn(&w, 0); - //lwsi_set_state(w, LRS_H1C_ISSUE_HANDSHAKE2); + /* + * The connection was kept warm in + * LRS_IDLING, which does not carry + * LWSIFS_POCB, so its POLLOUT is never + * serviced (lwsi_state_can_handle_POLLOUT() + * is false). If we adopt the new stream + * while the network wsi is still IDLING, the + * child-walking POLLOUT loop never runs, the + * new stream's HEADERS are never sent + * (lws_h2_client_handshake() is never + * reached) and its response would not be read + * either. Put it back into the same + * LRS_ESTABLISHED state it uses while actively + * muxing, and drop the keep-warm idle timeout + * since it is no longer idle. + */ + lwsi_set_state(w, LRS_ESTABLISHED); + lws_set_timeout(w, NO_PENDING_TIMEOUT, 0); + } wsi->client_h2_alpn = 1; lws_wsi_h2_adopt(w, wsi); @@ -2130,7 +2149,7 @@ lws_vhost_active_conns(struct lws *wsi, struct lws **nwsi, const char *adsin) * h3: if in usable state already: just use it without * going through the queue */ - if (lwsi_role_h3(w) && w->client_h2_alpn && w->client_mux_migrated && + if ((w->role_ops && !strcmp(w->role_ops->name, "quic")) && w->client_h2_alpn && w->client_mux_migrated && (lwsi_state(w) == LRS_H2_WAITING_TO_SEND_HEADERS || lwsi_state(w) == LRS_ESTABLISHED || lwsi_state(w) == LRS_IDLING)) { @@ -2138,9 +2157,18 @@ lws_vhost_active_conns(struct lws *wsi, struct lws **nwsi, const char *adsin) lwsl_wsi_info(w, "just join h3 directly 0x%x", lwsi_state(w)); - if (lwsi_state(w) == LRS_IDLING) + + if (lwsi_state(w) == LRS_IDLING) { _lws_generic_transaction_completed_active_conn(&w, 0); + /* See the h2 branch above: a revived idle + * mux connection must leave LRS_IDLING so its + * POLLOUT is serviced and the new stream's + * headers get sent. */ + lwsi_set_state(w, LRS_ESTABLISHED); + lws_set_timeout(w, NO_PENDING_TIMEOUT, 0); + } + wsi->client_h2_alpn = 1; if (lws_wsi_h3_adopt(w, wsi)) { lws_vhost_unlock(wsi->a.vhost); /* } ---------- */ diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c index f5e2f3592a..02f403ee28 100644 --- a/lib/core-net/wsi.c +++ b/lib/core-net/wsi.c @@ -527,7 +527,7 @@ const struct lws_protocols *lws_get_protocol(struct lws *wsi) { return wsi->a.protocol; } -int lws_ensure_user_space(struct lws *wsi) { +LWS_VISIBLE int lws_ensure_user_space(struct lws *wsi) { if (!wsi->a.protocol) return 0; @@ -608,9 +608,8 @@ int lws_has_buffered_out(struct lws *wsi) { lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->pending_tx[i].head) { struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); if (((f->type & 0xf8) == LWS_QUIC_FT_STREAM && f->stream_id == sid) || - (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED && f->stream_id == sid) || - f->type == LWS_QUIC_FT_DATA_BLOCKED) { - lwsl_notice("lws_has_buffered_out: %s has pending_tx stream/blocked frame on sid %llu\n", lws_wsi_tag(wsi), (unsigned long long)sid); + (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED && f->stream_id == sid)) { + lwsl_info("lws_has_buffered_out: %s has pending_tx stream/blocked frame on sid %llu\n", lws_wsi_tag(wsi), (unsigned long long)sid); return 1; } } lws_end_foreach_dll_safe(d, d1); @@ -618,9 +617,45 @@ int lws_has_buffered_out(struct lws *wsi) { lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->in_flight[i].head) { struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); if (((f->type & 0xf8) == LWS_QUIC_FT_STREAM && f->stream_id == sid) || - (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED && f->stream_id == sid) || - f->type == LWS_QUIC_FT_DATA_BLOCKED) { - lwsl_notice("lws_has_buffered_out: %s has in_flight stream/blocked frame on sid %llu\n", lws_wsi_tag(wsi), (unsigned long long)sid); + (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED && f->stream_id == sid)) { + lwsl_info("lws_has_buffered_out: %s has in_flight stream/blocked frame on sid %llu\n", lws_wsi_tag(wsi), (unsigned long long)sid); + return 1; + } + } lws_end_foreach_dll_safe(d, d1); + } + } + } +#endif + + return 0; +} + +int lws_has_unsent_buffered_out(struct lws *wsi) { + if (wsi->buflist_out) + return 1; + +#if defined(LWS_ROLE_H2) + { + struct lws *nwsi = lws_get_network_wsi(wsi); + + if (nwsi && nwsi->buflist_out) + return 1; + } +#endif + +#if defined(LWS_ROLE_QUIC) + if (wsi->quic.qs) { + struct lws *nwsi = lws_get_network_wsi(wsi); + struct lws_quic_netconn *qn = nwsi ? nwsi->quic.qn : NULL; + if (qn) { + uint64_t sid = wsi->quic.qs->stream_id; + int i; + + for (i = 0; i < LWS_QUIC_LEVEL_COUNT; i++) { + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->pending_tx[i].head) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + if (((f->type & 0xf8) == LWS_QUIC_FT_STREAM && f->stream_id == sid) || + (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED && f->stream_id == sid)) { return 1; } } lws_end_foreach_dll_safe(d, d1); @@ -1411,6 +1446,15 @@ void lws_wsi_mux_insert(struct lws *wsi, struct lws *parent_wsi, if (!wsi->role_ops) wsi->role_ops = parent_wsi->role_ops; +#if defined(LWS_WITH_PEER_LIMITS) + if (parent_wsi->peer && !wsi->peer) { + wsi->peer = parent_wsi->peer; + lws_context_lock(wsi->a.context, "mux peer child adopt"); + wsi->peer->count_wsi++; + lws_context_unlock(wsi->a.context); + } +#endif + /* new guy's sibling is whoever was the first child before */ wsi->mux.sibling_list = parent_wsi->mux.child_list; @@ -1511,7 +1555,7 @@ int lws_wsi_mux_mark_parents_needing_writeable(struct lws *wsi) { wsi2 = wsi; while (wsi2) { wsi2->mux.requested_POLLOUT = 1; - lwsl_wsi_info(wsi2, "sid %u, pending writable", wsi2->mux.my_sid); + lwsl_wsi_debug(wsi2, "sid %u, pending writable", wsi2->mux.my_sid); wsi2 = wsi2->mux.parent_wsi; } @@ -1555,23 +1599,24 @@ struct lws *lws_wsi_mux_move_child_to_tail(struct lws **wsi2) { int lws_wsi_mux_action_pending_writeable_reqs(struct lws *wsi) { struct lws *w = wsi->mux.child_list; + struct lws *nwsi = lws_get_network_wsi(wsi); if (wsi->mux.requested_POLLOUT) { - if (lws_change_pollfd(wsi, 0, LWS_POLLOUT)) + if (lws_change_pollfd(nwsi, 0, LWS_POLLOUT)) return -1; return 0; } while (w) { if (w->mux.requested_POLLOUT) { - if (lws_change_pollfd(wsi, 0, LWS_POLLOUT)) + if (lws_change_pollfd(nwsi, 0, LWS_POLLOUT)) return -1; return 0; } w = w->mux.sibling_list; } - if (lws_change_pollfd(wsi, LWS_POLLOUT, 0)) + if (lws_change_pollfd(nwsi, LWS_POLLOUT, 0)) return -1; return 0; @@ -1650,6 +1695,8 @@ int lws_wsi_mux_apply_queue(struct lws *wsi) { lwsl_wsi_notice(wsi, "evaluating queued conn %s (state 0x%x, par role %s)", lws_wsi_tag(w), lwsi_state(w), wsi->role_ops ? wsi->role_ops->name : "none"); + lwsl_notice("AGY-MUX: parent role=%s, child state=0x%x (expected 0x%x)\n", + wsi->role_ops ? wsi->role_ops->name : "none", lwsi_state(w), LRS_H2_WAITING_TO_SEND_HEADERS); #if defined(LWS_ROLE_H2) if (lwsi_role_h2(wsi) && @@ -1669,7 +1716,7 @@ int lws_wsi_mux_apply_queue(struct lws *wsi) { #endif #if defined(LWS_ROLE_H3) - if ((wsi->role_ops && !strcmp(wsi->role_ops->name, "quic")) && + if ((wsi->role_ops && (!strcmp(wsi->role_ops->name, "quic") || !strcmp(wsi->role_ops->name, "h3"))) && lwsi_state(w) == LRS_H2_WAITING_TO_SEND_HEADERS) { if (!lws_wsi_h3_can_adopt(wsi)) { diff --git a/lib/core/context.c b/lib/core/context.c index b17ee42fe4..18a5c3d39e 100644 --- a/lib/core/context.c +++ b/lib/core/context.c @@ -722,6 +722,8 @@ lws_create_context(const struct lws_context_creation_info *info) context->quic_tx_credit_cb = info->quic_tx_credit_cb; context->quic_cc_ops = info->quic_cc_ops; + context->quic_initial_cwnd = info->quic_initial_cwnd; + context->quic_0rtt_max_size = info->quic_0rtt_max_size; context->pt_serv_buf_size = (unsigned int)s1; context->protocols_copy = info->protocols; diff --git a/lib/core/libwebsockets.c b/lib/core/libwebsockets.c index fed5020bca..92a8482228 100644 --- a/lib/core/libwebsockets.c +++ b/lib/core/libwebsockets.c @@ -1602,6 +1602,11 @@ static const struct lws_switches builtins[] = { { "--h3", "Force HTTP/3 (client)" }, { "--cpd-bypass", "Bypass captive portal detect" }, { "--quic-pad-crypto", "Pad QUIC Handshake Crypto for tests" }, + { "--quic-only-latest", "Force QUIC to use the latest supported version (e.g. v2)" }, + { "--quic-force-retry", "Force QUIC to always send a Retry packet" }, + { "--0rtt", "Allow QUIC 0-RTT early data" }, + { "--quic-initial-cwnd", "Initial QUIC congestion window (cwnd) in bytes" }, + { "--0rtt-max-size", "Max QUIC 0-RTT early data size in bytes" }, { "-h", "Print this help" }, { "--help", "Print this help" }, }; @@ -1620,6 +1625,11 @@ enum opts { OPT_H3, OPT_CPD_BYPASS, OPT_QUIC_PAD_CRYPTO, + OPT_QUIC_ONLY_LATEST, + OPT_QUIC_FORCE_RETRY, + OPT_0RTT, + OPT_QUIC_INITIAL_CWND, + OPT_0RTT_MAX_SIZE, OPT_HELP1, OPT_HELP2, }; @@ -1897,6 +1907,21 @@ lws_cmdline_option_handle_builtin(int argc, const char **argv, case OPT_QUIC_PAD_CRYPTO: info->options |= LWS_SERVER_OPTION_QUIC_PAD_CRYPTO; break; + case OPT_QUIC_ONLY_LATEST: + info->options |= LWS_SERVER_OPTION_QUIC_LATEST_VERSION; + break; + case OPT_QUIC_FORCE_RETRY: + info->options |= LWS_SERVER_OPTION_QUIC_FORCE_RETRY; + break; + case OPT_0RTT: + info->options |= LWS_SERVER_OPTION_ALLOW_EARLY_DATA; + break; + case OPT_QUIC_INITIAL_CWND: + info->quic_initial_cwnd = (uint32_t)atoll(p); + break; + case OPT_0RTT_MAX_SIZE: + info->quic_0rtt_max_size = (uint32_t)atoll(p); + break; case OPT_HELP1: case OPT_HELP2: diff --git a/lib/core/private-lib-core.h b/lib/core/private-lib-core.h index bba4b8b086..146ae9a307 100644 --- a/lib/core/private-lib-core.h +++ b/lib/core/private-lib-core.h @@ -443,7 +443,7 @@ typedef enum { LWS_H3_STATE_FAILED_IGNORE } lws_h3_state_t; -#define LWS_QUIC_GRACE_DEFAULT_US 30000000 +#define LWS_QUIC_GRACE_DEFAULT_US 2000000 #define LWS_QUIC_GRACE_MARGIN_US 200000 typedef struct { @@ -866,6 +866,8 @@ struct lws_context { lws_quic_tx_credit_cb_t quic_tx_credit_cb; const struct lws_cc_ops *quic_cc_ops; + uint32_t quic_initial_cwnd; + uint32_t quic_0rtt_max_size; uint8_t quic_retry_secret[16]; }; diff --git a/lib/cose/cose_key.c b/lib/cose/cose_key.c index 45fbd6a924..0d024aae08 100644 --- a/lib/cose/cose_key.c +++ b/lib/cose/cose_key.c @@ -328,10 +328,19 @@ cb_cose_key(struct lecp_ctx *ctx, char reason) case LECPCB_OBJECT_START: if (cps->ck) break; - goto ak_l; + cps->ck = lws_zalloc(sizeof(*cps->ck), __func__); + if (!cps->ck) + goto bail; + cps->cose_state = 0; + cps->meta_idx = -1; + cps->gencrypto_eidx = -1; + cps->seen_count = 0; + + if (cps->pkey_set) + lws_dll2_add_tail(&cps->ck->list, cps->pkey_set); + break; case LECPCB_ARRAY_ITEM_START: if (cps->pkey_set && ctx->pst[ctx->pst_sp].ppos == 2) { - ak_l: cps->ck = lws_zalloc(sizeof(*cps->ck), __func__); if (!cps->ck) goto bail; diff --git a/lib/cose/cose_sign.c b/lib/cose/cose_sign.c index c268c85f69..7169575ae5 100644 --- a/lib/cose/cose_sign.c +++ b/lib/cose/cose_sign.c @@ -456,11 +456,10 @@ lws_cose_sign_payload_chunk(struct lws_cose_sign_context *csc, lws_lec_printf(&lec, "{1:%lld}", (long long)csc->alg->cose_alg); - lws_lec_init(&lec1, lb, sizeof(lb)); - lws_lec_int(&lec1, LWS_CBOR_MAJTYP_BSTR, 0, - lec.used); - if (lws_lec_printf(csc->info.lec, "{1:%lld}", - (long long)csc->alg->cose_alg) != LWS_LECPCTX_RET_FINISHED) + lws_lec_scratch(&lec); + + if (lws_lec_printf(csc->info.lec, "%.*b", + (int)lec.used, lec.start) != LWS_LECPCTX_RET_FINISHED) /* coverity */ return 0; break; diff --git a/lib/cose/cose_sign_alg.c b/lib/cose/cose_sign_alg.c index a9c4124242..7267660d67 100644 --- a/lib/cose/cose_sign_alg.c +++ b/lib/cose/cose_sign_alg.c @@ -41,6 +41,8 @@ lws_cose_sign_alg_create(struct lws_context *cx, const lws_cose_key_t *ck, alg->cose_alg = cose_alg; alg->cose_key = ck; + int type = 0; + switch (cose_alg) { /* ECDSA algs */ @@ -49,18 +51,74 @@ lws_cose_sign_alg_create(struct lws_context *cx, const lws_cose_key_t *ck, crv = "P-256"; gh = LWS_GENHASH_TYPE_SHA256; alg->keybits = 256; - goto ecdsa_l; + type = 1; + break; case LWSCOSE_WKAECDSA_ALG_ES384: /* ECDSA w/ SHA-384 */ crv = "P-384"; gh = LWS_GENHASH_TYPE_SHA384; alg->keybits = 384; - goto ecdsa_l; + type = 1; + break; case LWSCOSE_WKAECDSA_ALG_ES512: /* ECDSA w/ SHA-512 */ crv = "P-521"; gh = LWS_GENHASH_TYPE_SHA512; alg->keybits = 521; -ecdsa_l: + type = 1; + break; + + case LWSCOSE_WKAEDDSA_ALG_EDDSA: + type = 2; + break; + + + /* HMAC algs */ + + case LWSCOSE_WKAHMAC_256_64: + ghm = LWS_GENHMAC_TYPE_SHA256; + alg->keybits = 64; + type = 3; + break; + case LWSCOSE_WKAHMAC_256_256: + ghm = LWS_GENHMAC_TYPE_SHA256; + alg->keybits = 256; + type = 3; + break; + case LWSCOSE_WKAHMAC_384_384: + ghm = LWS_GENHMAC_TYPE_SHA384; + alg->keybits = 384; + type = 3; + break; + case LWSCOSE_WKAHMAC_512_512: + ghm = LWS_GENHMAC_TYPE_SHA512; + alg->keybits = 512; + type = 3; + break; + + /* RSASSA algs */ + + case LWSCOSE_WKARSA_ALG_RS256: + gh = LWS_GENHASH_TYPE_SHA256; + type = 4; + break; + + case LWSCOSE_WKARSA_ALG_RS384: + gh = LWS_GENHASH_TYPE_SHA384; + type = 4; + break; + + case LWSCOSE_WKARSA_ALG_RS512: + gh = LWS_GENHASH_TYPE_SHA512; + type = 4; + break; + + default: + lwsl_warn("%s: unsupported alg %lld\n", __func__, + (long long)cose_alg); + goto bail_hmac; + } + switch (type) { + case 1: /* ECDSA */ /* the key is good for this? */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_EC2, cose_alg, @@ -80,10 +138,9 @@ lws_cose_sign_alg_create(struct lws_context *cx, const lws_cose_key_t *ck, lwsl_notice("%s: ec key import fail\n", __func__); goto bail_ecdsa2; } - break; - case LWSCOSE_WKAEDDSA_ALG_EDDSA: + case 2: /* EDDSA */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_OKP, cose_alg, op, NULL)) goto bail_ecdsa; @@ -92,29 +149,9 @@ lws_cose_sign_alg_create(struct lws_context *cx, const lws_cose_key_t *ck, if (lws_geneddsa_set_key(&alg->u.ecdsactx, ck->e)) goto bail_ecdsa2; - break; - - /* HMAC algs */ - - case LWSCOSE_WKAHMAC_256_64: - ghm = LWS_GENHMAC_TYPE_SHA256; - alg->keybits = 64; - goto hmac_l; - case LWSCOSE_WKAHMAC_256_256: - ghm = LWS_GENHMAC_TYPE_SHA256; - alg->keybits = 256; - goto hmac_l; - case LWSCOSE_WKAHMAC_384_384: - ghm = LWS_GENHMAC_TYPE_SHA384; - alg->keybits = 384; - goto hmac_l; - case LWSCOSE_WKAHMAC_512_512: - ghm = LWS_GENHMAC_TYPE_SHA512; - alg->keybits = 512; - -hmac_l: + case 3: /* HMAC */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_SYMMETRIC, cose_alg, op, NULL)) goto bail_hmac; @@ -122,23 +159,9 @@ lws_cose_sign_alg_create(struct lws_context *cx, const lws_cose_key_t *ck, ke = &ck->e[LWS_GENCRYPTO_OCT_KEYEL_K]; if (lws_genhmac_init(&alg->u.hmacctx, ghm, ke->buf, ke->len)) goto bail_hmac; - break; - /* RSASSA algs */ - - case LWSCOSE_WKARSA_ALG_RS256: - gh = LWS_GENHASH_TYPE_SHA256; - goto rsassa_l; - - case LWSCOSE_WKARSA_ALG_RS384: - gh = LWS_GENHASH_TYPE_SHA384; - goto rsassa_l; - - case LWSCOSE_WKARSA_ALG_RS512: - gh = LWS_GENHASH_TYPE_SHA512; - -rsassa_l: + case 4: /* RSASSA */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_RSA, cose_alg, op, NULL)) goto bail_hmac; @@ -154,11 +177,6 @@ lws_cose_sign_alg_create(struct lws_context *cx, const lws_cose_key_t *ck, goto bail_hmac; } break; - - default: - lwsl_warn("%s: unsupported alg %lld\n", __func__, - (long long)cose_alg); - goto bail_hmac; } return alg; @@ -252,13 +270,17 @@ lws_cose_sign_alg_complete(lws_cose_sig_alg_t *alg) case LWSCOSE_WKAEDDSA_ALG_EDDSA: { int len; - if (!alg->failed && - (len = lws_geneddsa_hash_sign_jws(&alg->u.ecdsactx, alg->eddsa_in, + if (!alg->failed) { + len = lws_geneddsa_hash_sign_jws(&alg->u.ecdsactx, alg->eddsa_in, alg->eddsa_in_len, alg->rhash, - sizeof(alg->rhash))) >= 0) - alg->rhash_len = len; - else + sizeof(alg->rhash)); + if (len >= 0) + alg->rhash_len = len; + else + alg->failed = 1; + } else { alg->failed = 1; + } lws_genec_destroy(&alg->u.ecdsactx); lws_free_set_NULL(alg->eddsa_in); diff --git a/lib/cose/cose_validate.c b/lib/cose/cose_validate.c index abb483381d..d2ce6be938 100644 --- a/lib/cose/cose_validate.c +++ b/lib/cose/cose_validate.c @@ -509,12 +509,8 @@ cb_cose_sig(struct lecp_ctx *ctx, char reason) goto unexpected_tag_l; break; case SIGTYPE_MAC: - if (ctx->item.u.u64 != LWSCOAP_CONTENTFORMAT_COSE_MAC) { -unexpected_tag_l: - lwsl_warn("%s: unexpected tag %d\n", __func__, - (int)ctx->item.u.u64); - goto bail; - } + if (ctx->item.u.u64 != LWSCOAP_CONTENTFORMAT_COSE_MAC) + goto unexpected_tag_l; break; } @@ -947,19 +943,24 @@ cb_cose_sig(struct lecp_ctx *ctx, char reason) case ST_OUTER_UNPROTECTED: sl = &cps->st[cps->sp]; hi = ph_index(cps); - if (sl->ph_pos[hi] + 3 + ctx->cbor_pos > + if (sl->ph_pos[hi] + 3 + ctx->cbor_len > (int)sizeof(sl->ph[hi]) - 3) /* more protected cbor than we can handle */ goto bail; memcpy(sl->ph[hi] + 3 + sl->ph_pos[hi], ctx->cbor, - ctx->cbor_pos); - sl->ph_pos[hi] += ctx->cbor_pos; + ctx->cbor_len); + sl->ph_pos[hi] += ctx->cbor_len; break; } } return 0; +unexpected_tag_l: + lwsl_warn("%s: unexpected tag %d\n", __func__, + (int)ctx->item.u.u64); + goto bail; + bail: return -1; diff --git a/lib/cose/cose_validate_alg.c b/lib/cose/cose_validate_alg.c index d7cb9b612e..87945b32aa 100644 --- a/lib/cose/cose_validate_alg.c +++ b/lib/cose/cose_validate_alg.c @@ -41,6 +41,8 @@ lws_cose_val_alg_create(struct lws_context *cx, lws_cose_key_t *ck, alg->cose_alg = cose_alg; alg->cose_key = ck; + int type = 0; + switch (cose_alg) { /* ECDSA algs */ @@ -49,18 +51,73 @@ lws_cose_val_alg_create(struct lws_context *cx, lws_cose_key_t *ck, crv = "P-256"; gh = LWS_GENHASH_TYPE_SHA256; alg->keybits = 256; - goto ecdsa_l; + type = 1; + break; case LWSCOSE_WKAECDSA_ALG_ES384: /* ECDSA w/ SHA-384 */ crv = "P-384"; gh = LWS_GENHASH_TYPE_SHA384; alg->keybits = 384; - goto ecdsa_l; + type = 1; + break; case LWSCOSE_WKAECDSA_ALG_ES512: /* ECDSA w/ SHA-512 */ crv = "P-521"; gh = LWS_GENHASH_TYPE_SHA512; alg->keybits = 521; -ecdsa_l: + type = 1; + break; + case LWSCOSE_WKAEDDSA_ALG_EDDSA: + type = 2; + break; + + /* HMAC algs */ + + case LWSCOSE_WKAHMAC_256_64: + ghm = LWS_GENHMAC_TYPE_SHA256; + alg->keybits = 64; + type = 3; + break; + case LWSCOSE_WKAHMAC_256_256: + ghm = LWS_GENHMAC_TYPE_SHA256; + alg->keybits = 256; + type = 3; + break; + case LWSCOSE_WKAHMAC_384_384: + ghm = LWS_GENHMAC_TYPE_SHA384; + alg->keybits = 384; + type = 3; + break; + case LWSCOSE_WKAHMAC_512_512: + ghm = LWS_GENHMAC_TYPE_SHA512; + alg->keybits = 512; + type = 3; + break; + + /* RSASSA algs */ + + case LWSCOSE_WKARSA_ALG_RS256: + gh = LWS_GENHASH_TYPE_SHA256; + type = 4; + break; + + case LWSCOSE_WKARSA_ALG_RS384: + gh = LWS_GENHASH_TYPE_SHA384; + type = 4; + break; + + case LWSCOSE_WKARSA_ALG_RS512: + gh = LWS_GENHASH_TYPE_SHA512; + type = 4; + break; + + default: + lwsl_warn("%s: unsupported alg %lld\n", __func__, + (long long)cose_alg); + goto bail_hmac; + } + + switch (type) { + case 1: /* ECDSA */ /* the key is good for this? */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_EC2, cose_alg, @@ -80,10 +137,9 @@ lws_cose_val_alg_create(struct lws_context *cx, lws_cose_key_t *ck, lwsl_notice("%s: ec key import fail\n", __func__); goto bail_ecdsa2; } - break; - case LWSCOSE_WKAEDDSA_ALG_EDDSA: + case 2: /* EDDSA */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_OKP, cose_alg, op, NULL)) goto bail_ecdsa; @@ -92,28 +148,9 @@ lws_cose_val_alg_create(struct lws_context *cx, lws_cose_key_t *ck, if (lws_geneddsa_set_key(&alg->u.ecdsactx, ck->e)) goto bail_ecdsa2; - break; - /* HMAC algs */ - - case LWSCOSE_WKAHMAC_256_64: - ghm = LWS_GENHMAC_TYPE_SHA256; - alg->keybits = 64; - goto hmac_l; - case LWSCOSE_WKAHMAC_256_256: - ghm = LWS_GENHMAC_TYPE_SHA256; - alg->keybits = 256; - goto hmac_l; - case LWSCOSE_WKAHMAC_384_384: - ghm = LWS_GENHMAC_TYPE_SHA384; - alg->keybits = 384; - goto hmac_l; - case LWSCOSE_WKAHMAC_512_512: - ghm = LWS_GENHMAC_TYPE_SHA512; - alg->keybits = 512; - -hmac_l: + case 3: /* HMAC */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_SYMMETRIC, cose_alg, op, NULL)) goto bail_hmac; @@ -121,23 +158,9 @@ lws_cose_val_alg_create(struct lws_context *cx, lws_cose_key_t *ck, ke = &ck->e[LWS_GENCRYPTO_OCT_KEYEL_K]; if (lws_genhmac_init(&alg->u.hmacctx, ghm, ke->buf, ke->len)) goto bail_hmac; - break; - /* RSASSA algs */ - - case LWSCOSE_WKARSA_ALG_RS256: - gh = LWS_GENHASH_TYPE_SHA256; - goto rsassa_l; - - case LWSCOSE_WKARSA_ALG_RS384: - gh = LWS_GENHASH_TYPE_SHA384; - goto rsassa_l; - - case LWSCOSE_WKARSA_ALG_RS512: - gh = LWS_GENHASH_TYPE_SHA512; - -rsassa_l: + case 4: /* RSASSA */ if (lws_cose_key_checks(ck, LWSCOSE_WKKTV_RSA, cose_alg, op, NULL)) goto bail_hmac; @@ -152,11 +175,6 @@ lws_cose_val_alg_create(struct lws_context *cx, lws_cose_key_t *ck, goto bail_ecdsa1; } break; - - default: - lwsl_warn("%s: unsupported alg %lld\n", __func__, - (long long)cose_alg); - goto bail_hmac; } return alg; diff --git a/lib/event-libs/libevent/libevent.c b/lib/event-libs/libevent/libevent.c index 4ea3a101a9..e275f8c300 100644 --- a/lib/event-libs/libevent/libevent.c +++ b/lib/event-libs/libevent/libevent.c @@ -466,9 +466,11 @@ elops_destroy_context2_event(struct lws_context *context) event_base_loopexit(ptpr->io_loop, NULL); continue; } - while (budget-- && - (m = event_base_loop(ptpr->io_loop, EVLOOP_NONBLOCK))) - ; + while (budget--) { + m = event_base_loop(ptpr->io_loop, EVLOOP_NONBLOCK); + if (!m) + break; + } lwsl_cx_info(context, "event_base_free"); diff --git a/lib/event-libs/libuv/libuv.c b/lib/event-libs/libuv/libuv.c index c02687e1c9..7bb9aa0249 100644 --- a/lib/event-libs/libuv/libuv.c +++ b/lib/event-libs/libuv/libuv.c @@ -381,9 +381,12 @@ elops_destroy_context1_uv(struct lws_context *context) if (!pt->event_loop_foreign && pt_to_priv_uv(pt)->io_loop) { - while (budget-- && (m = uv_run(pt_to_priv_uv(pt)->io_loop, - UV_RUN_NOWAIT))) - ; + while (budget--) { + m = uv_run(pt_to_priv_uv(pt)->io_loop, + UV_RUN_NOWAIT); + if (!m) + break; + } if (m) lwsl_cx_info(context, "tsi %d: unclosed", n); @@ -528,6 +531,14 @@ elops_accept_uv(struct lws *wsi) ((uv_handle_t *)w_read->pwatcher)->data = (void *)wsi; + /* + * A fresh poll watcher has just been attached to this wsi. If the wsi + * is being reused (eg, http->https redirect via lws_client_reset()), + * told_event_loop_closed may still be latched from the previous close. + * Clear it now that a new handle is live. + */ + wsi->told_event_loop_closed = 0; + ptpriv->extant_handles++; lwsl_wsi_debug(wsi, "thr %d: sa left %d: dyn left: %d", diff --git a/lib/misc/lecp.c b/lib/misc/lecp.c index 13e6ba476b..edf084b13f 100644 --- a/lib/misc/lecp.c +++ b/lib/misc/lecp.c @@ -325,6 +325,7 @@ report_raw_cbor(struct lecp_ctx *ctx) if (!ctx->cbor_pos) return 0; + ctx->cbor_len = ctx->cbor_pos; ctx->cbor_pos = 0; /* reset BEFORE callback */ if (pst->cb(ctx, LECPCB_LITERAL_CBOR)) diff --git a/lib/plat/unix/unix-sockets.c b/lib/plat/unix/unix-sockets.c index 7042d1150b..f34a266ea1 100644 --- a/lib/plat/unix/unix-sockets.c +++ b/lib/plat/unix/unix-sockets.c @@ -68,6 +68,14 @@ lws_send_pipe_choked(struct lws *wsi) #if defined(LWS_WITH_HTTP2) wsi_eff = lws_get_network_wsi(wsi); + + /* + * ws-over-h2: whole DATA frames parked on the stream awaiting h2 + * tx credit (see lws_h2_frame_write) mean the pipe is choked for + * this stream even though the network wsi could accept bytes. + */ + if (wsi_eff != wsi && lws_has_buffered_out(wsi)) + return 1; #else wsi_eff = wsi; #endif diff --git a/lib/plat/windows/windows-sockets.c b/lib/plat/windows/windows-sockets.c index 1a737a7350..306e0fbe4e 100644 --- a/lib/plat/windows/windows-sockets.c +++ b/lib/plat/windows/windows-sockets.c @@ -42,6 +42,14 @@ lws_send_pipe_choked(struct lws *wsi) #if defined(LWS_WITH_HTTP2) wsi_eff = lws_get_network_wsi(wsi); + + /* + * ws-over-h2: whole DATA frames parked on the stream awaiting h2 + * tx credit (see lws_h2_frame_write) mean the pipe is choked for + * this stream even though the network wsi could accept bytes. + */ + if (wsi_eff != wsi && lws_has_buffered_out(wsi)) + return 1; #else wsi_eff = wsi; #endif diff --git a/lib/roles/h1/ops-h1.c b/lib/roles/h1/ops-h1.c index e4c058d023..52e4248988 100644 --- a/lib/roles/h1/ops-h1.c +++ b/lib/roles/h1/ops-h1.c @@ -108,6 +108,17 @@ lws_read_h1(struct lws *wsi, unsigned char *buf, lws_filepos_t len) return lws_ptr_diff(buf, oldbuf); case LRS_ISSUING_FILE: return lws_ptr_diff(buf, oldbuf); + case LRS_DOING_TRANSACTION: + if (!lwsi_role_h1(wsi)) + break; + /* + * Consume and discard spurious pipelined data + * while we are actively sending a response. + * This avoids CPU spins from stashing it, and + * allows the current transaction to complete. + */ + buf += len; + return lws_ptr_diff(buf, oldbuf); case LRS_DISCARD_BODY: case LRS_BODY: wsi->http.rx_content_remain = diff --git a/lib/roles/h2/http2.c b/lib/roles/h2/http2.c index 39c33dc426..48d2d925e9 100644 --- a/lib/roles/h2/http2.c +++ b/lib/roles/h2/http2.c @@ -339,6 +339,15 @@ lws_wsi_h2_adopt(struct lws *parent_wsi, struct lws *wsi) wsi->seen_nonpseudoheader = 0; #if defined(LWS_WITH_CLIENT) wsi->client_mux_substream = 1; + /* + * A reused client stream is a mux substream just like the first one. + * Without this, HPACK skips capturing custom (non-indexed) response + * headers into the ah unknown-header list (that capture is gated on + * mux_substream), so a pooled client's 2nd+ request drops custom + * response headers and leaves a stale list that lws_hdr_custom_* + * subsequently walks out of bounds. + */ + wsi->mux_substream = 1; #endif wsi->h2.initialized = 1; @@ -437,8 +446,17 @@ lws_pps_schedule(struct lws *wsi, struct lws_h2_protocol_send *pps) return; } + if (pps->type != LWS_H2_PPS_GOAWAY && h2n->pps_count >= 50) { + lwsl_warn("%s: too many pending protocol sends (%u), dropping conn\n", + __func__, (unsigned int)h2n->pps_count); + lws_free(pps); + lws_h2_goaway(nwsi, H2_ERR_ENHANCE_YOUR_CALM, "too many pending pps"); + return; + } + pps->next = h2n->pps; h2n->pps = pps; + h2n->pps_count++; lws_rx_flow_control(wsi, LWS_RXFLOW_REASON_APPLIES_DISABLE | LWS_RXFLOW_REASON_H2_PPS_PENDING); lws_callback_on_writable(wsi); @@ -686,6 +704,33 @@ int lws_h2_frame_write(struct lws *wsi, int type, int flags, sid, len, (int)wsi->txc.tx_cr, (int)nwsi->txc.tx_cr); if (type == LWS_H2_FRAME_TYPE_DATA) { +#if defined(LWS_ROLE_WS) + if (wsi->h23_stream_carries_ws && + (wsi->buflist_out || + lws_h2_tx_cr_get(wsi) < (int)len)) { + /* + * ws-over-h2 (RFC 8441): not enough h2 flow-control + * credit for this DATA frame (or earlier frames are + * already parked and must keep their order). Sending + * DATA past the peer's advertised window is a + * connection-fatal FLOW_CONTROL_ERROR, and unlike the + * http file-serving path the ws user API has no seat + * at the flow-control table -- so park the whole + * frame (header included) on the stream's buflist and + * send it from the POLLOUT servicing loop once + * WINDOW_UPDATE restores credit (which re-arms every + * mux child). Credit is consumed at drain time. + */ + if (lws_buflist_append_segment(&wsi->buflist_out, + &buf[-LWS_H2_FRAME_HEADER_LENGTH], + len + LWS_H2_FRAME_HEADER_LENGTH) < 0) + return -1; + + lws_callback_on_writable(wsi); + + return (int)len; + } +#endif if (wsi->txc.tx_cr < (int)len) lwsl_info("%s: %s: sending payload len %d" @@ -705,6 +750,108 @@ int lws_h2_frame_write(struct lws *wsi, int type, int flags, return n; } +#if defined(LWS_ROLE_WS) +/* + * Send as much parked ws-over-h2 DATA (see lws_h2_frame_write) as the + * stream's tx credit allows, oldest first. Each parked buflist segment is one + * complete h2 frame: the 9-byte header (carrying the payload length) plus the + * payload. h2 DATA is a byte stream, so a frame larger than the available + * credit is SPLIT: a chunk goes out under a fresh header and the stored + * header is rewritten in place for the remainder -- the peer's stream window + * (SETTINGS_INITIAL_WINDOW_SIZE) may be permanently smaller than a parked + * frame, so waiting for whole-frame credit can deadlock. Flags (e.g. + * END_STREAM) are only sent with a frame's final chunk. Returns <0 on a + * fatal write error; leftover bytes stay parked until the next WINDOW_UPDATE. + */ +int +lws_h2_ws_drain_parked_tx(struct lws *nwsi, struct lws *wsi) +{ + uint8_t out[LWS_H2_FRAME_HEADER_LENGTH + 4096]; + + /* + * Test the stream's own buflist directly: lws_has_buffered_out() + * additionally reports the nwsi's buffer, which fills up whenever + * lws_issue_raw() below takes a partial socket write and must not + * keep us looping after our own parked frames are gone. + */ + while (wsi->buflist_out) { + uint8_t *seg = NULL; + size_t sl = lws_buflist_next_segment_len(&wsi->buflist_out, + &seg); + unsigned int plen, chunk, rem; + uint8_t type, flags, sid[4]; + int cr; + + if (!sl || !seg || sl < LWS_H2_FRAME_HEADER_LENGTH) + return -1; + + plen = ((unsigned int)seg[0] << 16) | + ((unsigned int)seg[1] << 8) | seg[2]; + type = seg[3]; + flags = seg[4]; + memcpy(sid, &seg[5], 4); + + if (sl < LWS_H2_FRAME_HEADER_LENGTH + plen) + return -1; + + cr = lws_h2_tx_cr_get(wsi); + if (plen && cr <= 0) + /* still skint; WINDOW_UPDATE will re-arm us */ + return 0; + + chunk = plen; + if (chunk > (unsigned int)cr) + chunk = (unsigned int)cr; + if (chunk > sizeof(out) - LWS_H2_FRAME_HEADER_LENGTH) + chunk = sizeof(out) - LWS_H2_FRAME_HEADER_LENGTH; + + out[0] = (uint8_t)(chunk >> 16); + out[1] = (uint8_t)(chunk >> 8); + out[2] = (uint8_t)chunk; + out[3] = type; + out[4] = chunk == plen ? flags : 0; + memcpy(&out[5], sid, 4); + memcpy(&out[LWS_H2_FRAME_HEADER_LENGTH], + &seg[LWS_H2_FRAME_HEADER_LENGTH], chunk); + + lws_h2_tx_cr_consume(wsi, (int)chunk); + + /* + * lws_issue_raw() either sends the bytes or copies any + * remainder onto nwsi's own buflist, so the parked bytes can + * be released either way. + */ + if (lws_issue_raw(nwsi, out, + LWS_H2_FRAME_HEADER_LENGTH + chunk) < 0) + return -1; + + if (chunk == plen) { + lws_buflist_use_segment(&wsi->buflist_out, + LWS_H2_FRAME_HEADER_LENGTH + plen); + continue; + } + + /* + * Partial send: stamp a header for the remainder over the + * last 9 already-consumed bytes (the sent payload ended at + * seg[chunk + 9], so seg[chunk..chunk+8] only covers spent + * header/payload bytes for any chunk >= 1), then advance the + * segment onto it. + */ + rem = plen - chunk; + seg[chunk + 0] = (uint8_t)(rem >> 16); + seg[chunk + 1] = (uint8_t)(rem >> 8); + seg[chunk + 2] = (uint8_t)rem; + seg[chunk + 3] = type; + seg[chunk + 4] = flags; + memcpy(&seg[chunk + 5], sid, 4); + lws_buflist_use_segment(&wsi->buflist_out, chunk); + } + + return 0; +} +#endif + static void lws_h2_set_bin(struct lws *wsi, int n, unsigned char *buf) { *buf++ = (uint8_t)(n >> 8); @@ -741,6 +888,9 @@ int lws_h2_do_pps_send(struct lws *wsi) if (!pps) return 1; + if (h2n->pps_count) + h2n->pps_count--; + lwsl_info("%s: %s: %d\n", __func__, lws_wsi_tag(wsi), pps->type); switch (pps->type) { @@ -1529,6 +1679,19 @@ lws_h2_parse_end_of_frame(struct lws *wsi) #endif h2n->swsi->client_mux_substream = 1; h2n->swsi->client_h2_alpn = 1; +#if defined(LWS_ROLE_WS) + /* + * If the original client ask was a ws connection, the + * migrated stream carries it (RFC 8441 extended + * CONNECT); without this the h2 client handshake + * issues a plain GET and the ws ask is lost. + */ + if (wsi->ws) { + h2n->swsi->ws = wsi->ws; + wsi->ws = NULL; + h2n->swsi->do_ws = 1; + } +#endif #if defined(LWS_WITH_CLIENT) h2n->swsi->flags = wsi->flags; #if defined(LWS_WITH_CONMON) @@ -2007,6 +2170,11 @@ lws_h2_parse_end_of_frame(struct lws *wsi) lwsl_info("LWS_H2_FRAME_TYPE_RST_STREAM: sid %u: reason 0x%x\n", (unsigned int)h2n->sid, (unsigned int)h2n->hpack_e_dep); + if (h2n->swsi) { + lws_close_free_wsi(h2n->swsi, LWS_CLOSE_STATUS_NOSTATUS, + "peer RST_STREAM"); + h2n->swsi = NULL; + } break; case LWS_H2_FRAME_TYPE_COUNT: /* IGNORING FRAME */ @@ -2263,7 +2431,15 @@ lws_h2_parser(struct lws *wsi, unsigned char *in, lws_filepos_t _inlen, "\n", n); } #if defined(LWS_WITH_CLIENT) - if (h2n->swsi->client_mux_substream) { + /* + * A client stream carrying ws (RFC 8441) is + * NOT http body: its DATA is ws framing that + * must go through the ws parser via the + * lws_read_h1() path below, like the server + * side of the same situation. + */ + if (h2n->swsi->client_mux_substream && + !h2n->swsi->h23_stream_carries_ws) { if (!h2n->swsi->a.protocol) { lwsl_err("%s: %p doesn't have protocol\n", __func__, lws_wsi_tag(h2n->swsi)); @@ -2649,27 +2825,6 @@ lws_h2_client_handshake(struct lws *wsi) (unsigned char *)path, n, &p, end)) goto fail_length; -#if defined(LWS_ROLE_WS) - if (wsi->do_ws) { - const char *prot = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_ORIGIN); - - if (lws_add_http_header_by_token(wsi, WSI_TOKEN_VERSION, - (unsigned char *)"13", 2, &p, end)) - goto fail_length; - - if (!prot && wsi->stash && wsi->stash->cis[CIS_PROTOCOL]) - prot = wsi->stash->cis[CIS_PROTOCOL]; - - if (prot) { - if (lws_add_http_header_by_token(wsi, WSI_TOKEN_PROTOCOL, - (unsigned char *)prot, (int)strlen(prot), &p, end)) - goto fail_length; - } - - wsi->h23_stream_carries_ws = 1; - } -#endif - n = lws_hdr_total_length(wsi, _WSI_TOKEN_CLIENT_HOST); simp = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_HOST); if (!n && wsi->stash && wsi->stash->cis[CIS_ADDRESS]) { @@ -2690,6 +2845,68 @@ lws_h2_client_handshake(struct lws *wsi) (unsigned char *)simp, n, &p, end)) goto fail_length; +#if defined(LWS_ROLE_WS) + if (wsi->do_ws) { + /* + * These are regular headers, so they must come after every + * pseudo-header (:authority is the last one above), or strict + * peers fail the stream with "pseudoheader after normal hdrs". + * The requested subprotocol list rides in the same header as + * for h1 upgrades (RFC 8441 Sect 5). + */ + const char *prot = lws_hdr_simple_ptr(wsi, + _WSI_TOKEN_CLIENT_SENT_PROTOCOLS); + + if (lws_add_http_header_by_token(wsi, WSI_TOKEN_VERSION, + (unsigned char *)"13", 2, &p, end)) + goto fail_length; + + if (!prot && wsi->stash && wsi->stash->cis[CIS_PROTOCOL]) + prot = wsi->stash->cis[CIS_PROTOCOL]; + + if (prot) { + if (lws_add_http_header_by_token(wsi, WSI_TOKEN_PROTOCOL, + (unsigned char *)prot, (int)strlen(prot), &p, end)) + goto fail_length; + } + +#if !defined(LWS_WITHOUT_EXTENSIONS) + { + /* + * Offer the vhost's extensions the same way the h1 + * upgrade does (RFC 8441 Sect 5 carries the ws + * headers unchanged): without this the client can + * never negotiate eg, permessage-deflate over h2. + */ + const struct lws_extension *ext = + wsi->a.vhost->ws.extensions; + char eb[256]; + int el = 0; + + while (ext && ext->callback) { + if (wsi->a.vhost->protocols[0].callback(wsi, + LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED, + wsi->user_space, (char *)ext->name, 0)) { + ext++; + continue; + } + el += lws_snprintf(eb + (size_t)el, + sizeof(eb) - (size_t)el, + "%s%s", el ? "," : "", + ext->client_offer); + ext++; + } + if (el && + lws_add_http_header_by_token(wsi, + WSI_TOKEN_EXTENSIONS, + (unsigned char *)eb, el, &p, end)) + goto fail_length; + } +#endif + + wsi->h23_stream_carries_ws = 1; + } +#endif if (wsi->flags & LCCSCF_HTTP_MULTIPART_MIME) { p1 = lws_http_multipart_headers(wsi, p); @@ -2758,6 +2975,13 @@ lws_h2_client_handshake(struct lws *wsi) lws_h2_state(wsi, LWS_H2_STATE_OPEN); lwsi_set_state(wsi, LRS_ESTABLISHED); + if (wsi->mux.my_sid == 1) { + lws_start_foreach_ll(struct lws *, w1, nwsi->mux.child_list) { + if (w1 != wsi && lwsi_state(w1) == LRS_H2_WAITING_TO_SEND_HEADERS) + lws_callback_on_writable(w1); + } lws_end_foreach_ll(w1, mux.sibling_list); + } + if (wsi->flags & LCCSCF_HTTP_MULTIPART_MIME) lws_callback_on_writable(wsi); diff --git a/lib/roles/h2/ops-h2.c b/lib/roles/h2/ops-h2.c index 111e48091b..640005fc8a 100644 --- a/lib/roles/h2/ops-h2.c +++ b/lib/roles/h2/ops-h2.c @@ -801,7 +801,12 @@ rops_close_kill_connection_h2(struct lws *wsi, enum lws_close_status reason) if (wsi->mux.parent_wsi->h2.h2n && wsi->mux.parent_wsi->h2.h2n->swsi == wsi) { + struct lws *nwsi = wsi->mux.parent_wsi; wsi->mux.parent_wsi->h2.h2n->swsi = NULL; + lws_start_foreach_ll(struct lws *, sibling, nwsi->mux.child_list) { + if (sibling != wsi && lwsi_state(sibling) == LRS_H2_WAITING_TO_SEND_HEADERS) + lws_callback_on_writable(sibling); + } lws_end_foreach_ll(sibling, mux.sibling_list); } lws_wsi_mux_sibling_disconnect(wsi); @@ -1084,6 +1089,42 @@ rops_perform_user_POLLOUT_h2(struct lws *wsi) /* priority 1: post compression-transform buffered output */ +#if defined(LWS_ROLE_WS) + if (w->h23_stream_carries_ws && w->buflist_out) { + /* + * ws-over-h2: whole DATA frames parked by + * lws_h2_frame_write() waiting for tx credit. Gate + * and re-arm on the stream's OWN buflist, not + * lws_has_buffered_out(): that also reports the + * nwsi's buffer (eg, another stream's partial socket + * write), which must neither pull us into the drain + * with nothing parked nor block re-arming the user + * once our own frames are gone. If nothing of ours + * is parked, fall through to normal servicing. + */ + + if (lws_h2_ws_drain_parked_tx(wsi, w) < 0) { + lwsl_info("%s signalling to close\n", + __func__); + lws_close_free_wsi(w, + LWS_CLOSE_STATUS_NOSTATUS, + "h2 end stream 1"); + wa = &wsi->mux.child_list; + goto next_child; + } + if (!w->buflist_out) + /* fully drained: let the user write */ + lws_callback_on_writable(w); + /* + * else still skint: stay quiet, the + * WINDOW_UPDATE handler re-arms every child + */ + wa = &wsi->mux.child_list; + goto next_child; + } +#endif + /* ... for both ws-over-h2 and h2, deal with partial at nwsi */ + if (lws_has_buffered_out(w)) { lwsl_debug("%s: completing partial\n", __func__); if (lws_issue_raw(w, NULL, 0) < 0) { @@ -1098,6 +1139,33 @@ rops_perform_user_POLLOUT_h2(struct lws *wsi) goto next_child; } +#if defined(LWS_ROLE_WS) && !defined(LWS_WITHOUT_EXTENSIONS) + /* + * ws-over-h2: tx path extension with more to send (eg, + * permessage-deflate whose compressed output exceeded its + * chunk buffer). The h1 path services this from + * rops_handle_POLLOUT_ws() priority 5; this loop is the ONLY + * POLLOUT servicing an encapsulated child ever gets, so it + * must do the same -- while tx_draining_ext is set, + * lws_send_pipe_choked() reports choked, so the user + * callback will never write again and the connection wedges + * for good. + */ + if (lwsi_role_ws(w) && lwsi_state(w) == LRS_ESTABLISHED && + w->ws && w->ws->tx_draining_ext) { + if (lws_write(w, NULL, 0, LWS_WRITE_CONTINUATION) < 0) { + lwsl_info("%s signalling to close\n", __func__); + lws_close_free_wsi(w, LWS_CLOSE_STATUS_NOSTATUS, + "h2 ws ext drain"); + wa = &wsi->mux.child_list; + goto next_child; + } + lws_callback_on_writable(w); + wa = &wsi->mux.child_list; + goto next_child; + } +#endif + /* priority 2: pre compression-transform buffered output */ #if defined(LWS_WITH_HTTP_STREAM_COMPRESSION) diff --git a/lib/roles/h2/private-lib-roles-h2.h b/lib/roles/h2/private-lib-roles-h2.h index c41c36772e..5fa716e308 100644 --- a/lib/roles/h2/private-lib-roles-h2.h +++ b/lib/roles/h2/private-lib-roles-h2.h @@ -251,6 +251,7 @@ struct lws_h2_netconn { char goaway_str[32]; /* for rx */ struct lws *swsi; struct lws_h2_protocol_send *pps; /* linked list */ + uint32_t pps_count; enum http2_hpack_state hpack; enum http2_hpack_type hpack_type; @@ -330,6 +331,10 @@ lws_h2_do_pps_send(struct lws *wsi); int lws_h2_frame_write(struct lws *wsi, int type, int flags, unsigned int sid, unsigned int len, unsigned char *buf); +#if defined(LWS_ROLE_WS) +int +lws_h2_ws_drain_parked_tx(struct lws *nwsi, struct lws *wsi); +#endif struct lws * lws_wsi_mux_from_id(struct lws *wsi, unsigned int sid); int diff --git a/lib/roles/h3/ops-h3.c b/lib/roles/h3/ops-h3.c index f5735bde1d..2d15dc18ba 100644 --- a/lib/roles/h3/ops-h3.c +++ b/lib/roles/h3/ops-h3.c @@ -58,6 +58,10 @@ lws_h3_client_handshake(struct lws *wsi) if (wsi->do_ws) meth = "CONNECT"; +#if defined(LWS_ROLE_WT) + else if (wsi->a.protocol && !strcmp(wsi->a.protocol->name, "webtransport")) + meth = "CONNECT"; +#endif else if (!meth) meth = "GET"; @@ -75,6 +79,13 @@ lws_h3_client_handshake(struct lws *wsi) (unsigned char *)"websocket", 9, &p, end)) return -1; } +#if defined(LWS_ROLE_WT) + else if (wsi->a.protocol && !strcmp(wsi->a.protocol->name, "webtransport")) { + if (lws_add_http3_header_by_token(wsi, WSI_TOKEN_COLON_PROTOCOL, + (unsigned char *)"webtransport", 12, &p, end)) + return -1; + } +#endif if (lws_add_http3_header_by_token(wsi, WSI_TOKEN_HTTP_COLON_SCHEME, (unsigned char *)"https", 5, &p, end)) @@ -167,8 +178,40 @@ lws_h3_client_handshake(struct lws *wsi) static int rops_perform_user_POLLOUT_h3(struct lws *wsi) { +#if defined(LWS_WITH_HTTP2) + if (wsi->h2.pending_status_body) { + int n = lws_write(wsi, (uint8_t *)wsi->h2.pending_status_body + + LWS_PRE, + strlen(wsi->h2.pending_status_body + + LWS_PRE), LWS_WRITE_HTTP_FINAL); + (void)n; + lws_free_set_NULL(wsi->h2.pending_status_body); + lwsl_wsi_notice(wsi, "closing stream after sending pending status body"); + return -1; + } +#endif + lwsl_wsi_info(wsi, "rops_perform_user_POLLOUT_h3: entry, state=%d", lwsi_state(wsi)); + /* + * For QUIC/H3, lws_has_buffered_out() also checks QUIC pending_tx + * for stream frames. But those are drained by the QUIC POLLOUT + * handler, not by lws_issue_raw(). Only check buflist_out here — + * blocking on QUIC pending_tx prevents the H3 state machine from + * ever progressing past this point for client streams whose HEADERS + * STREAM frames are still in the QUIC TX queue. + */ + if (wsi->buflist_out) { + lwsl_wsi_debug(wsi, "%s: completing partial", __func__); + if (lws_issue_raw(wsi, NULL, 0) < 0) { + lwsl_wsi_info(wsi, "%s signalling to close", __func__); + wsi->socket_is_permanently_unusable = 1; + return -1; + } + if (wsi->buflist_out) + return 0; + } + #if defined(LWS_WITH_SERVER) if (wsi->http.deferred_transaction_completed) { if (!lws_has_buffered_out(wsi)) { @@ -177,6 +220,9 @@ rops_perform_user_POLLOUT_h3(struct lws *wsi) wsi->socket_is_permanently_unusable = 1; return -1; } + } else { + lws_callback_on_writable(wsi); + wsi->mux.requested_POLLOUT = 1; } return 0; } @@ -187,6 +233,10 @@ rops_perform_user_POLLOUT_h3(struct lws *wsi) wsi->socket_is_permanently_unusable = 1; return -1; } + if (lws_has_unsent_buffered_out(wsi)) { + lws_callback_on_writable(wsi); + wsi->mux.requested_POLLOUT = 1; + } return 0; } @@ -244,6 +294,16 @@ rops_perform_user_POLLOUT_h3(struct lws *wsi) tx_credit(wsi, LWSTXCR_US_TO_PEER, 0); } if (lws_wsi_txc_check_skint(&wsi->txc, usable_credit)) { + /* + * No TX credit — either QUIC peer flow control is + * exhausted, or the pending_tx buffer throttle (64KB cap + * in rops_tx_credit_quic) is full. Call + * lws_callback_on_writable() which for QUIC will invoke + * lws_wsi_mux_mark_parents_needing_writeable(), propagating + * requested_POLLOUT up the full parent chain to nwsi so the + * post-ACK wakeup loop can find and restart us. + */ + lws_callback_on_writable(wsi); return 0; } @@ -261,15 +321,8 @@ rops_perform_user_POLLOUT_h3(struct lws *wsi) return -1; } if (!n) { - int32_t usable_credit2 = wsi->txc.tx_cr; - if (lws_rops_fidx(wsi->role_ops, LWS_ROPS_tx_credit)) { - usable_credit2 = lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_tx_credit). - tx_credit(wsi, LWSTXCR_US_TO_PEER, 0); - } - if (usable_credit2 > 0) { - lws_callback_on_writable(wsi); - wsi->mux.requested_POLLOUT = 1; - } + lws_callback_on_writable(wsi); + wsi->mux.requested_POLLOUT = 1; } return 0; @@ -324,9 +377,14 @@ rops_perform_user_POLLOUT_h3(struct lws *wsi) lwsl_debug("H3_TRACE: wsi %p lws_http_action failed, returning %d\n", wsi, n); return -1; } - if (n > 0) { + if (n > 0 +#if defined(LWS_WITH_HTTP2) + && !wsi->h2.pending_status_body +#endif + ) { lwsl_wsi_notice(wsi, "closing stream after h3 action completed (%d)", n); - return -1; + lwsi_set_state(wsi, LRS_FLUSHING_BEFORE_CLOSE); + return 0; } lwsl_debug("H3_TRACE: wsi %p lws_http_action returned 0 (success)\n", wsi); @@ -334,6 +392,12 @@ rops_perform_user_POLLOUT_h3(struct lws *wsi) lws_header_table_detach(wsi, 0); lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0); + /* We are returning 0 because the file might be read synchronously and FIN queued, + * but not sent yet. We need to preserve requested_POLLOUT so it gets polled + * again and transitions properly. */ + lws_callback_on_writable(wsi); + wsi->mux.requested_POLLOUT = 1; + return 0; } #endif @@ -498,6 +562,28 @@ lws_h3_qpack_header_cb(void *user, int name_idx, const char *name, size_t name_l return -1; } } else { +#if defined(LWS_WITH_CUSTOM_HEADERS) + struct allocated_headers *ah = wsi->http.ah; + if (ah && name && name_len > 0 && ah->pos + 8 + name_len + value_len < (unsigned int)wsi->a.context->max_http_header_data) { + uint32_t unk_pos = ah->pos; + + lws_ser_wu16be((uint8_t *)&ah->data[unk_pos + 0], (uint16_t)name_len); + lws_ser_wu16be((uint8_t *)&ah->data[unk_pos + 2], (uint16_t)value_len); + lws_ser_wu32be((uint8_t *)&ah->data[unk_pos + 4], 0); + + memcpy(&ah->data[unk_pos + 8], name, name_len); + memcpy(&ah->data[unk_pos + 8 + name_len], value, value_len); + + if (!ah->unk_ll_head) + ah->unk_ll_head = unk_pos; + if (ah->unk_ll_tail) + lws_ser_wu32be((uint8_t *)&ah->data[ah->unk_ll_tail + 4], unk_pos); + ah->unk_ll_tail = unk_pos; + + ah->pos += (ah_data_idx_t)(8 + name_len + value_len); + lwsl_wsi_notice(wsi, "Added H3 custom header: %.*s = %.*s", (int)name_len, name, (int)value_len, value); + } else +#endif lwsl_wsi_debug(wsi, "Ignoring unknown header: %s", name ? name : "unknown"); } @@ -716,6 +802,131 @@ lws_h3_rx_stream_data(struct lws *wsi, const uint8_t *buf, size_t len) // lwsl_notice("H3 RX: %d bytes\n", (int)len); // lwsl_hexdump_notice(buf, len); +#if defined(LWS_ROLE_WT) + extern const struct lws_role_ops role_ops_wt; + struct lws *nwsi = lws_get_quic_network_wsi(wsi); + int has_wt_session = 0; + if (nwsi) { + struct lws *child = nwsi->mux.child_list; + while (child) { + if (child->wt.is_session) { + has_wt_session = 1; + break; + } + child = child->mux.sibling_list; + } + } + + if (has_wt_session) { + if (wsi->quic.qs && wsi->quic.qs->is_unidirectional) { + uint64_t type = 0; + size_t consumed_type = 0; + + if (!wsi->h3.type_set) { + consumed_type = lws_quic_parse_varint(buf, len, &type); + if (!consumed_type) return 0; /* Need more data */ + + if (type == 0x54) { + wsi->h3.stream_type = 0x54; + wsi->h3.type_set = 1; + } + } else if (wsi->h3.stream_type == 0x54) { + type = 0x54; + } + + if (type == 0x54) { + /* We need to parse the Session ID next */ + uint64_t session_id = 0; + size_t consumed_sid = lws_quic_parse_varint(buf + consumed_type, len - consumed_type, &session_id); + if (!consumed_sid) return 0; /* Need more data */ + + /* Find matching WT session */ + struct lws *session_wsi = NULL; + if (nwsi) { + struct lws *child = nwsi->mux.child_list; + while (child) { + if (child->wt.is_session && (child->mux.my_sid / 4 == session_id)) { + session_wsi = child; + break; + } + child = child->mux.sibling_list; + } + } + + if (session_wsi) { + lwsl_notice("Transitioning client-initiated uni stream to WT (session ID %llu)\n", (unsigned long long)session_id); + lws_role_transition(wsi, lwsi_role_client(wsi) ? LWSIFR_CLIENT : LWSIFR_SERVER, LRS_ESTABLISHED, &role_ops_wt); + wsi->wt.is_unidi = 1; + wsi->wt.is_session = 0; + if (lws_bind_protocol(wsi, session_wsi->a.protocol, __func__)) + return 1; + + if (wsi->a.protocol && wsi->a.protocol->callback) { + wsi->a.protocol->callback(wsi, LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED, wsi->user_space, NULL, 0); + } + + size_t total_consumed = consumed_type + consumed_sid; + if (len > total_consumed && wsi->a.protocol && wsi->a.protocol->callback) { + wsi->a.protocol->callback(wsi, LWS_CALLBACK_RECEIVE, wsi->user_space, (void *)(buf + total_consumed), len - total_consumed); + } + return 0; + } else { + lwsl_err("WT session WSI not found for session ID %llu\n", (unsigned long long)session_id); + return 1; + } + } + } else if (wsi->quic.qs && !wsi->quic.qs->is_unidirectional && !wsi->h3.type_set) { + uint64_t type = 0; + size_t consumed_type = lws_quic_parse_varint(buf, len, &type); + if (!consumed_type) return 0; /* Need more data */ + + if (type == 0x41) { + uint64_t session_id = 0; + size_t consumed_sid = lws_quic_parse_varint(buf + consumed_type, len - consumed_type, &session_id); + if (!consumed_sid) return 0; /* Need more data */ + + /* Check if it matches an active WT session */ + struct lws *session_wsi = NULL; + if (nwsi) { + struct lws *child = nwsi->mux.child_list; + while (child) { + if (child->wt.is_session && (child->mux.my_sid / 4 == session_id)) { + session_wsi = child; + break; + } + child = child->mux.sibling_list; + } + } + + if (session_wsi) { + lwsl_notice("Transitioning client-initiated bidi stream to WT (session ID %llu)\n", (unsigned long long)session_id); + lws_role_transition(wsi, lwsi_role_client(wsi) ? LWSIFR_CLIENT : LWSIFR_SERVER, LRS_ESTABLISHED, &role_ops_wt); + wsi->wt.is_unidi = 0; + wsi->wt.is_session = 0; + if (lws_bind_protocol(wsi, session_wsi->a.protocol, __func__)) + return 1; + + if (wsi->a.protocol && wsi->a.protocol->callback) { + wsi->a.protocol->callback(wsi, LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED, wsi->user_space, NULL, 0); + } + + size_t total_consumed = consumed_type + consumed_sid; + if (len > total_consumed && wsi->a.protocol && wsi->a.protocol->callback) { + wsi->a.protocol->callback(wsi, LWS_CALLBACK_RECEIVE, wsi->user_space, (void *)(buf + total_consumed), len - total_consumed); + } + return 0; + } else { + lwsl_err("WT session WSI not found for session ID %llu\n", (unsigned long long)session_id); + return 1; + } + } else { + /* Not a WT stream, mark type_set to avoid parsing again */ + wsi->h3.type_set = 1; + } + } + } +#endif + /* If it's unidirectional and we don't know the type yet */ if (wsi->quic.qs && wsi->quic.qs->is_unidirectional && !wsi->h3.type_set) { uint64_t type; @@ -726,7 +937,12 @@ lws_h3_rx_stream_data(struct lws *wsi, const uint8_t *buf, size_t len) wsi->h3.stream_type = (uint8_t)type; wsi->h3.type_set = 1; if (type == 0x02) { - wsi->h3.qpack_dec_state.state = LQP_DEC_INSTRUCTION; + if (!wsi->h3.qpack_dec_state) { + wsi->h3.qpack_dec_state = lws_zalloc(sizeof(*wsi->h3.qpack_dec_state), "qpack dec state"); + if (!wsi->h3.qpack_dec_state) + return 1; + } + wsi->h3.qpack_dec_state->state = LQP_DEC_INSTRUCTION; } buf += consumed; len -= consumed; @@ -751,10 +967,17 @@ lws_h3_rx_stream_data(struct lws *wsi, const uint8_t *buf, size_t len) struct lws_qpack_context *ctx = wsi->h3.h3n ? &wsi->h3.h3n->qpack_dec_ctx : NULL; lwsl_wsi_info(wsi, "LWS_H3_RX_STREAM_DATA: Encoder Stream payload received, len=%d", (int)len); - if (lws_qpack_decode_encoder_stream(&wsi->h3.qpack_dec_state, ctx, buf, len)) { + if (!wsi->h3.qpack_dec_state) { + wsi->h3.qpack_dec_state = lws_zalloc(sizeof(*wsi->h3.qpack_dec_state), "qpack dec state"); + if (!wsi->h3.qpack_dec_state) + return 1; + wsi->h3.qpack_dec_state->state = LQP_DEC_INSTRUCTION; + } + + if (lws_qpack_decode_encoder_stream(wsi->h3.qpack_dec_state, ctx, buf, len)) { struct lws *nwsi = lws_get_quic_network_wsi(wsi); lwsl_err("ERROR: QPACK_ENCODER_STREAM_ERROR!!!!\n"); -lws_quic_enter_closing_state(nwsi, LWS_QPACK_ENCODER_STREAM_ERROR, 0, 1); + lws_quic_enter_closing_state(nwsi, LWS_QPACK_ENCODER_STREAM_ERROR, 0, 1); return 1; } buf += len; len = 0; @@ -894,7 +1117,12 @@ lws_quic_enter_closing_state(nwsi, LWS_QPACK_ENCODER_STREAM_ERROR, 0, 1); if (!wsi->quic.qs || !wsi->quic.qs->is_unidirectional) { if (wsi->h3.rx_frame_type == 0x01) { /* HEADERS */ struct lws_qpack_context *ctx = wsi->h3.h3n ? &wsi->h3.h3n->qpack_dec_ctx : NULL; - if (lws_qpack_decode_header_block(&wsi->h3.qpack_dec_state, ctx, buf, chunk, lws_h3_qpack_header_cb, wsi)) { + if (!wsi->h3.qpack_dec_state) { + wsi->h3.qpack_dec_state = lws_zalloc(sizeof(*wsi->h3.qpack_dec_state), "qpack dec state"); + if (!wsi->h3.qpack_dec_state) + return 1; + } + if (lws_qpack_decode_header_block(wsi->h3.qpack_dec_state, ctx, buf, chunk, lws_h3_qpack_header_cb, wsi)) { struct lws *nwsi = lws_get_quic_network_wsi(wsi); lws_quic_enter_closing_state(nwsi, LWS_QPACK_DECOMPRESSION_FAILED, 0, 1); return 1; @@ -1002,7 +1230,8 @@ lws_quic_enter_closing_state(nwsi, LWS_QPACK_ENCODER_STREAM_ERROR, 0, 1); len -= chunk; /* Replenish flow control window */ - // lwsl_notice("H3 RX: Replenishing %d bytes", (int)chunk); lws_wsi_tx_credit(wsi, LWSTXCR_PEER_TO_US, (int)chunk); + // lwsl_notice("H3 RX: Replenishing %d bytes", (int)chunk); + lws_wsi_tx_credit(wsi, LWSTXCR_PEER_TO_US, (int)chunk); if (wsi->h3.rx_frame_payload_read == wsi->h3.rx_frame_len) { if (wsi->h3.stream_type == 0x00 && wsi->h3.rx_frame_type == 0x04) { @@ -1146,6 +1375,10 @@ lws_quic_enter_closing_state(nwsi, LWS_QPACK_ENCODER_STREAM_ERROR, 0, 1); } #endif } + if (wsi->h3.rx_frame_type == 0x01 && wsi->h3.qpack_dec_state) { + lws_free(wsi->h3.qpack_dec_state); + wsi->h3.qpack_dec_state = NULL; + } wsi->h3.rx_frame_state = 0; /* Next frame */ } } @@ -1199,18 +1432,23 @@ rops_alpn_negotiated_h3(struct lws *wsi, const char *alpn) wsi->h3.h3n = nwsi->h3.h3n; wsi->h3.qpack_tx_encoder = nwsi->h3.qpack_tx_encoder; - /* If we are the network wsi, we must notify our children! */ - if (wsi == nwsi) { + /* Notify all children and update their h3n */ + { struct lws *child = nwsi->mux.child_list; lwsl_wsi_info(wsi, "H3 ALPN Negotiated, child_list=%p", child); while (child) { lwsl_wsi_info(child, "H3 ALPN child state=%d", lwsi_state(child)); + child->h3.h3n = nwsi->h3.h3n; + child->h3.qpack_tx_encoder = nwsi->h3.qpack_tx_encoder; + if (lwsi_state(child) == LRS_UNCONNECTED || lwsi_state(child) == LRS_WAITING_CONNECT) { lwsl_wsi_info(child, "H3 ALPN Negotiated, transitioning child"); lws_role_transition(child, lwsi_role_client(nwsi) ? LWSIFR_CLIENT : LWSIFR_SERVER, LRS_H2_WAITING_TO_SEND_HEADERS, &role_ops_h3); - child->h3.h3n = nwsi->h3.h3n; - child->h3.qpack_tx_encoder = nwsi->h3.qpack_tx_encoder; lws_callback_on_writable(child); + } else if (child->role_ops && !strcmp(child->role_ops->name, "quic")) { + /* Server-side peer-initiated stream adopted during 0-RTT! Transition it to H3 now. */ + lwsl_wsi_info(child, "H3 ALPN Negotiated, transitioning 0-RTT child to H3"); + lws_role_transition(child, lwsi_role_client(nwsi) ? LWSIFR_CLIENT : LWSIFR_SERVER, LRS_ESTABLISHED, &role_ops_h3); } child = child->mux.sibling_list; } @@ -1224,6 +1462,11 @@ rops_close_kill_connection_h3(struct lws *wsi, enum lws_close_status reason) { lws_quic_stream_cleanup(wsi); + if (wsi->h3.qpack_dec_state) { + lws_free(wsi->h3.qpack_dec_state); + wsi->h3.qpack_dec_state = NULL; + } + if (wsi->mux.parent_wsi) lws_wsi_mux_sibling_disconnect(wsi); @@ -1328,8 +1571,12 @@ rops_write_role_protocol_h3(struct lws *wsi, unsigned char *buf, size_t len, { struct lws *nwsi = lws_get_quic_network_wsi(wsi); - if (nwsi && lws_rops_fidx(nwsi->role_ops, LWS_ROPS_write_role_protocol)) { - n = lws_rops_func_fidx(nwsi->role_ops, LWS_ROPS_write_role_protocol). + const struct lws_role_ops *role = nwsi ? nwsi->role_ops : NULL; + if (role && !strcmp(role->name, "h3")) { + role = lws_role_by_name("quic"); + } + if (nwsi && role && lws_rops_fidx(role, LWS_ROPS_write_role_protocol)) { + n = lws_rops_func_fidx(role, LWS_ROPS_write_role_protocol). write_role_protocol(wsi, (is_http || is_headers) ? pre : buf, len, wp); if (n <= 0) return n; @@ -1353,9 +1600,8 @@ rops_callback_on_writable_h3(struct lws *wsi) { struct lws *nwsi = lws_get_quic_network_wsi(wsi); - if (wsi->mux.requested_POLLOUT) { + if (wsi->mux.requested_POLLOUT) lwsl_debug("already pending writable\n"); - } lws_wsi_mux_mark_parents_needing_writeable(wsi); @@ -1405,11 +1651,163 @@ rops_check_upgrades_h3(struct lws *wsi) #if defined(LWS_ROLE_WT) lwsl_info("Upgrade h3 to wt\n"); extern const struct lws_role_ops role_ops_wt; + unsigned char response_buf[LWS_PRE + 4096], *rp = response_buf + LWS_PRE, *end = response_buf + sizeof(response_buf); + char draft[32]; + char client_protos[256]; + char negotiated[64] = ""; + int draft_len, cp_len; + lws_mux_mark_immortal(wsi); lws_metrics_tag_wsi_add(wsi, "upg", "wt_over_h3"); + /* Construct HTTP/3 response headers for CONNECT upgrade to WebTransport */ + if (lws_add_http_header_status(wsi, 200, &rp, end)) + return LWS_UPG_RET_BAIL; + + /* Copy and send back the draft version */ + draft_len = lws_hdr_custom_copy(wsi, draft, sizeof(draft) - 1, + "sec-webtransport-http3-draft", 28); + if (draft_len > 0) { + draft[draft_len] = '\0'; + if (lws_add_http_header_by_name(wsi, + (const unsigned char *)"sec-webtransport-http3-draft:", + (const unsigned char *)draft, draft_len, &rp, end)) + return LWS_UPG_RET_BAIL; + } else { + /* Check if client sent sec-webtransport-http3-draft02 */ + char draft02_val[16]; + int d02_len = lws_hdr_custom_copy(wsi, draft02_val, sizeof(draft02_val) - 1, + "sec-webtransport-http3-draft02", 30); + if (d02_len > 0) { + if (lws_add_http_header_by_name(wsi, + (const unsigned char *)"sec-webtransport-http3-draft02:", + (const unsigned char *)"1", 1, &rp, end)) + return LWS_UPG_RET_BAIL; + } else { + /* Default to draft02 if not sent */ + if (lws_add_http_header_by_name(wsi, + (const unsigned char *)"sec-webtransport-http3-draft:", + (const unsigned char *)"draft02", 7, &rp, end)) + return LWS_UPG_RET_BAIL; + } + } + + /* Subprotocol negotiation */ + cp_len = lws_hdr_custom_copy(wsi, client_protos, sizeof(client_protos) - 1, + "wt-available-protocols", 22); + if (cp_len > 0) { + const char *env_protocols = getenv("PROTOCOLS_SERVER"); + client_protos[cp_len] = '\0'; + if (!env_protocols) + env_protocols = getenv("PROTOCOLS"); + + struct lws_tokenize ts; + lws_tokenize_init(&ts, client_protos, LWS_TOKENIZE_F_COMMA_SEP_LIST | + LWS_TOKENIZE_F_MINUS_NONTERM); + ts.len = (unsigned int)cp_len; + lws_tokenize_elem e; + + do { + e = lws_tokenize(&ts); + if (e == LWS_TOKZE_TOKEN || e == LWS_TOKZE_QUOTED_STRING) { + char name[64]; + if (!lws_tokenize_cstr(&ts, name, sizeof(name))) { + if (env_protocols) { + struct lws_tokenize ts_srv; + lws_tokenize_init(&ts_srv, env_protocols, LWS_TOKENIZE_F_MINUS_NONTERM); + lws_tokenize_elem e_srv; + do { + e_srv = lws_tokenize(&ts_srv); + if (e_srv == LWS_TOKZE_TOKEN || e_srv == LWS_TOKZE_QUOTED_STRING) { + char srv_name[64]; + if (!lws_tokenize_cstr(&ts_srv, srv_name, sizeof(srv_name))) { + if (strcmp(name, srv_name) == 0) { + lws_strncpy(negotiated, name, sizeof(negotiated)); + break; + } + } + } + } while (e_srv > 0); + } else { + /* No env protocols filter - select if loaded on this vhost */ + if (lws_vhost_name_to_protocol(wsi->a.vhost, name)) { + lws_strncpy(negotiated, name, sizeof(negotiated)); + break; + } + } + } + } + if (negotiated[0]) + break; + } while (e > 0); + } + + if (!negotiated[0]) { + char *uri_ptr = lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_COLON_PATH); + int uri_len = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_COLON_PATH); + lwsl_notice("H3 WT Upgrade: path '%.*s'\n", uri_len, uri_ptr ? uri_ptr : "NULL"); + if (uri_ptr && uri_len > 0) { + const struct lws_http_mount *hit = lws_find_mount(wsi, uri_ptr, uri_len); + if (hit) { + lwsl_notice("H3 WT Upgrade: matched mount '%s', origin '%s', protocol '%s'\n", + hit->mountpoint, hit->origin ? hit->origin : "NULL", + hit->protocol ? hit->protocol : "NULL"); + const char *name = hit->origin; + if (hit->protocol) + name = hit->protocol; + else if (!strncmp(name, "callback://", 11)) + name += 11; + + lws_strncpy(negotiated, name, sizeof(negotiated)); + } else { + lwsl_notice("H3 WT Upgrade: no mount matched path\n"); + } + } + } + + const struct lws_protocols *prot = NULL; + if (negotiated[0]) { + prot = lws_vhost_name_to_protocol(wsi->a.vhost, negotiated); + } + if (!prot) { + prot = lws_vhost_name_to_protocol(wsi->a.vhost, "webtransport-shared-world"); + } + if (!prot) { + int n = wsi->a.vhost->default_protocol_index; + if (n < wsi->a.vhost->count_protocols) { + prot = &wsi->a.vhost->protocols[n]; + } + } + + if (prot) { + if (lws_bind_protocol(wsi, prot, __func__)) + return LWS_UPG_RET_BAIL; + lwsl_notice("H3 WT Upgrade: bound to protocol '%s'\n", prot->name); + + if (negotiated[0]) { + char wt_prot_val[128]; + int wpl = lws_snprintf(wt_prot_val, sizeof(wt_prot_val), "\"%s\"", prot->name); + if (lws_add_http_header_by_name(wsi, + (const unsigned char *)"wt-protocol:", + (const unsigned char *)wt_prot_val, wpl, &rp, end)) + return LWS_UPG_RET_BAIL; + } + } else { + lwsl_notice("H3 WT Upgrade: no WebTransport protocol found on vhost\n"); + } + + if (lws_finalize_http_header(wsi, &rp, end)) + return LWS_UPG_RET_BAIL; + + if (lws_write(wsi, response_buf + LWS_PRE, lws_ptr_diff_size_t(rp, response_buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0) + return LWS_UPG_RET_BAIL; + /* Switch role to WebTransport */ lws_role_transition(wsi, LWSIFR_SERVER, LRS_ESTABLISHED, &role_ops_wt); + wsi->wt.is_session = 1; + + if (lws_ensure_user_space(wsi)) + return LWS_UPG_RET_BAIL; if (wsi->a.protocol && wsi->a.protocol->callback) { if (wsi->a.protocol->callback(wsi, LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED, wsi->user_space, NULL, 0)) @@ -1512,8 +1910,13 @@ lws_wsi_h3_can_adopt(struct lws *parent_wsi) if (next_id == 0) next_id = 4; - if (next_id / 4 >= qn->max_streams_bidi_remote) - return 0; /* limit reached */ + if (next_id / 4 >= qn->max_streams_bidi_remote) { + /* Allow if we are doing 0-RTT but haven't received/cached the peer's limit */ + if (qn->early_data_status == LWS_0RTT_STATUS_ATTEMPTED && !qn->max_streams_bidi_remote) + return 1; + + return 0; /* limit reached */ + } return 1; } @@ -1565,6 +1968,9 @@ lws_wsi_h3_adopt(struct lws *parent_wsi, struct lws *wsi) wsi->client_h2_alpn = 1; #endif + if (!qn->is_server && qn->early_data_status == LWS_0RTT_STATUS_ATTEMPTED) + wsi->quic.qs->opted_into_early_data = 1; + lws_wsi_mux_insert(wsi, nwsi, (unsigned int)sid); /* Initialize flow control credits */ diff --git a/lib/roles/h3/private-lib-roles-h3.h b/lib/roles/h3/private-lib-roles-h3.h index f53b7ef9ff..7b1481118f 100644 --- a/lib/roles/h3/private-lib-roles-h3.h +++ b/lib/roles/h3/private-lib-roles-h3.h @@ -91,7 +91,7 @@ struct lws_h3_netconn { struct _lws_h3_related { struct lws_h3_netconn *h3n; /* malloc'd for root net conn */ struct lws_qpack_tx_encoder *qpack_tx_encoder; - struct lws_qpack_stream_state qpack_dec_state; + struct lws_qpack_stream_state *qpack_dec_state; uint8_t h3_state; uint8_t stream_type; uint8_t type_set:1; diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index b5facb884b..73c2f80725 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -1061,6 +1061,16 @@ lws_client_interpret_server_handshake(struct lws *wsi) if (!wsi->do_ws) { /* we are being an http client... */ +#if defined(LWS_ROLE_WT) + if (wsi->a.protocol && !strcmp(wsi->a.protocol->name, "webtransport")) { + extern const struct lws_role_ops role_ops_wt; + lwsl_debug("%s: %s: transitioning to WebTransport client\n", + __func__, lws_wsi_tag(wsi)); + lws_role_transition(wsi, LWSIFR_CLIENT, + LRS_ESTABLISHED, &role_ops_wt); + wsi->wt.is_session = 1; + } else +#endif #if defined(LWS_ROLE_H2) if (wsi->client_h2_alpn || wsi->client_mux_substream) { lwsl_debug("%s: %s: transitioning to mux client\n", diff --git a/lib/roles/http/parsers.c b/lib/roles/http/parsers.c index 4053a244e1..5e13a5c02d 100644 --- a/lib/roles/http/parsers.c +++ b/lib/roles/http/parsers.c @@ -618,7 +618,7 @@ lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen) ll = wsi->http.ah->unk_ll_head; while (ll) { - if (ll >= wsi->http.ah->data_length) + if (ll + UHO_NAME >= wsi->http.ah->data_length) return -1; if (nlen == lws_ser_ru16be( (uint8_t *)&wsi->http.ah->data[ll + UHO_NLEN]) && @@ -646,7 +646,7 @@ lws_hdr_custom_copy(struct lws *wsi, char *dst, int len, const char *name, ll = wsi->http.ah->unk_ll_head; while (ll) { - if (ll >= wsi->http.ah->data_length) + if (ll + UHO_NAME >= wsi->http.ah->data_length) return -1; if (nlen == lws_ser_ru16be( (uint8_t *)&wsi->http.ah->data[ll + UHO_NLEN]) && @@ -678,7 +678,7 @@ lws_hdr_custom_name_foreach(struct lws *wsi, lws_hdr_custom_fe_cb_t cb, ll = wsi->http.ah->unk_ll_head; while (ll) { - if (ll >= wsi->http.ah->data_length) + if (ll + UHO_NAME >= wsi->http.ah->data_length) return -1; cb(&wsi->http.ah->data[ll + UHO_NAME], diff --git a/lib/roles/http/server/lejp-conf.c b/lib/roles/http/server/lejp-conf.c index dfb8a6e1ee..b96b7bd072 100644 --- a/lib/roles/http/server/lejp-conf.c +++ b/lib/roles/http/server/lejp-conf.c @@ -51,6 +51,9 @@ static const char * const paths_global[] = { "global.rlimit-nofile", "global.cpd-bypass", "global.quic-pad-crypto", + "global.allow-early-data", + "global.quic-only-latest", + "global.quic-early-key-update", }; enum lejp_global_paths { @@ -73,6 +76,9 @@ enum lejp_global_paths { LWJPGP_FD_LIMIT_PT, LEJPGP_CPD_BYPASS, LEJPGP_QUIC_PAD_CRYPTO, + LEJPGP_ALLOW_EARLY_DATA, + LEJPGP_QUIC_ONLY_LATEST, + LEJPGP_QUIC_EARLY_KEY_UPDATE, }; static const char * const paths_vhosts[] = { @@ -168,6 +174,7 @@ static const char * const paths_vhosts[] = { "vhosts[].dht[]", #endif "vhosts[].quic-mtu", + "vhosts[].quic-preferred-addresses", }; enum lejp_vhost_paths { @@ -265,6 +272,7 @@ enum lejp_vhost_paths { LEJPVP_DHT, #endif LEJPVP_QUIC_MTU, + LEJPVP_QUIC_PREFERRED_ADDRESSES, }; #define MAX_PLUGIN_DIRS 10 @@ -405,6 +413,18 @@ lejp_globals_cb(struct lejp_ctx *ctx, char reason) if (arg_to_bool(ctx->buf)) a->info->options |= LWS_SERVER_OPTION_QUIC_PAD_CRYPTO; return 0; + case LEJPGP_ALLOW_EARLY_DATA: + if (arg_to_bool(ctx->buf)) + a->info->options |= LWS_SERVER_OPTION_ALLOW_EARLY_DATA; + return 0; + case LEJPGP_QUIC_ONLY_LATEST: + if (arg_to_bool(ctx->buf)) + a->info->options |= LWS_SERVER_OPTION_QUIC_LATEST_VERSION; + return 0; + case LEJPGP_QUIC_EARLY_KEY_UPDATE: + if (arg_to_bool(ctx->buf)) + a->info->options |= LWS_SERVER_OPTION_QUIC_EARLY_KEY_UPDATE; + return 0; case LEJPGP_SERVER_STRING: #if defined(LWS_WITH_SERVER) a->info->server_string = a->p; @@ -498,6 +518,8 @@ lejp_vhosts_cb(struct lejp_ctx *ctx, char reason) LWS_SERVER_OPTION_UV_NO_SIGSEGV_SIGFPE_SPIN | LWS_SERVER_OPTION_LIBEVENT | LWS_SERVER_OPTION_QUIC_PAD_CRYPTO | + LWS_SERVER_OPTION_ALLOW_EARLY_DATA | + LWS_SERVER_OPTION_QUIC_EARLY_KEY_UPDATE | LWS_SERVER_OPTION_LIBEV ); #if defined(LWS_WITH_SERVER) @@ -1174,6 +1196,10 @@ lejp_vhosts_cb(struct lejp_ctx *ctx, char reason) case LEJPVP_QUIC_MTU: a->info->quic_mtu = (uint32_t)atoi(ctx->buf); return 0; + case LEJPVP_QUIC_PREFERRED_ADDRESSES: + a->info->quic_preferred_addresses = a->p; + lwsl_notice("Parsed quic-preferred-addresses: %s\n", a->p); + break; #endif case LEJPVP_LISTEN_ACCEPT_ROLE: diff --git a/lib/roles/http/server/server.c b/lib/roles/http/server/server.c index b61eaf452f..4703eab409 100644 --- a/lib/roles/http/server/server.c +++ b/lib/roles/http/server/server.c @@ -787,7 +787,7 @@ lws_http_serve(struct lws *wsi, char *uri, const char *origin, #endif int spin = 0; #endif - char path[256], sym[2048]; + char path[1024], sym[2048]; unsigned char *p = (unsigned char *)sym + 32 + LWS_PRE, *start = p; unsigned char *end = p + sizeof(sym) - 32 - LWS_PRE; #if !defined(WIN32) && !defined(LWS_PLAT_FREERTOS) @@ -1506,8 +1506,14 @@ lws_http_proxy_start(struct lws *wsi, const struct lws_http_mount *hit, pslash + 1, uri_ptr + hit->mountpoint_len) - 1; lws_clean_url(rpath); n = (int)strlen(rpath); - if (n && rpath[n - 1] == '/') - n--; + { + int orig_had_slash = 0; + int u_len = (int)strlen(uri_ptr); + if (u_len && uri_ptr[u_len - 1] == '/') + orig_had_slash = 1; + if (!orig_had_slash && n && rpath[n - 1] == '/') + n--; + } na = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_URI_ARGS); if (na) { @@ -1714,6 +1720,7 @@ lws_http_redirect_hit(struct lws_context_per_thread *pt, struct lws *wsi, if ((hit->mountpoint_len > 1 || (hit->origin_protocol == LWSMPRO_REDIR_HTTP || hit->origin_protocol == LWSMPRO_REDIR_HTTPS)) && + (uri_ptr[0] && uri_ptr[strlen(uri_ptr) - 1] != '/') && (*s != '/' || (hit->origin_protocol == LWSMPRO_REDIR_HTTP || hit->origin_protocol == LWSMPRO_REDIR_HTTPS)) && @@ -1743,30 +1750,29 @@ lws_http_redirect_hit(struct lws_context_per_thread *pt, struct lws *wsi, oprot[hit->origin_protocol & 1], hit->origin); } else { - if (!lws_hdr_total_length(wsi, WSI_TOKEN_HOST)) { + const char *host_hdr = lws_hdr_simple_ptr(wsi, WSI_TOKEN_HOST); #if defined(LWS_ROLE_H2) || defined(LWS_ROLE_H3) - if (!lws_hdr_total_length(wsi, - WSI_TOKEN_HTTP_COLON_AUTHORITY)) -#endif - goto bail_nuke_ah; -#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_H3) - n = lws_snprintf((char *)end, 256, - "%s%s%s/", oprot[!!lws_is_ssl(wsi)], - lws_hdr_simple_ptr(wsi, - WSI_TOKEN_HTTP_COLON_AUTHORITY), - uri_ptr); + if (!host_hdr) { + host_hdr = lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_COLON_AUTHORITY); + } #endif - } else + if (!host_hdr) + goto bail_nuke_ah; + + if (host_hdr[0] == '+' || strchr(host_hdr, '/')) { + n = lws_snprintf((char *)end, 256, "%s/", uri_ptr); + } else { n = lws_snprintf((char *)end, 256, "%s%s%s/", oprot[!!lws_is_ssl(wsi)], - lws_hdr_simple_ptr(wsi, WSI_TOKEN_HOST), - uri_ptr); + host_hdr, uri_ptr); + } } // lwsl_notice("%s: redirecting to '%s' (vhost port %d, peer %s)\n", // __func__, (char *)end, wsi->a.vhost->listen_port, peer_buf); lws_clean_url((char *)end); + n = (int)strlen((char *)end); n = lws_http_redirect(wsi, HTTP_STATUS_MOVED_PERMANENTLY, end, n, &p, end); if ((int)n < 0) @@ -3604,11 +3610,16 @@ int lws_serve_http_file_fragment(struct lws *wsi) if (!txc) { /* - * We shouldn't've been able to get the - * WRITEABLE if we are skint + * tx credit is 0. This can happen because the + * QUIC pending_tx buffer throttle (64KB cap) kicked + * in *between* the POLLOUT handler's check and here. + * lws_callback_on_writable propagates requested_POLLOUT + * up the full parent chain so the post-ACK wakeup loop + * can restart us once the buffer drains. */ lwsl_notice("%s: %s: no tx credit\n", __func__, lws_wsi_tag(wsi)); + lws_callback_on_writable(wsi); return 0; } diff --git a/lib/roles/mqtt/mqtt.c b/lib/roles/mqtt/mqtt.c index 85e9840155..e743c38122 100644 --- a/lib/roles/mqtt/mqtt.c +++ b/lib/roles/mqtt/mqtt.c @@ -1278,8 +1278,7 @@ _lws_mqtt_rx_parser(struct lws *wsi, lws_mqtt_parser_t *par, * Otherwise consume the properties before * completing the command */ - lws_mqtt_vbi_init(&par->vbit); - par->state = LMQCPP_PUBACK_VH_PKT_ID; + par->state = LMQCPP_EAT_PROPERTIES_AND_COMPLETE; break; default: lwsl_notice("%s: puback pr bad vbi\n", __func__); diff --git a/lib/roles/quic/crypto-quic.c b/lib/roles/quic/crypto-quic.c index 35f9aef7a4..4e4ab87677 100644 --- a/lib/roles/quic/crypto-quic.c +++ b/lib/roles/quic/crypto-quic.c @@ -25,6 +25,8 @@ #include "private-lib-core.h" #include "roles/quic/private-lib-roles-quic.h" +extern const struct lws_role_ops role_ops_h3; + static const uint8_t quic_v1_initial_salt[20] = { 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, @@ -93,7 +95,7 @@ lws_quic_derive_initial_keys(struct lws *wsi, const struct lws_quic_cid *dcid) if (!k) return -1; - if (wsi->quic.qn->version == LWS_QUIC_VERSION_2) { + if (wsi->quic.qn->original_version == LWS_QUIC_VERSION_2) { salt = quic_v2_initial_salt; salt_len = sizeof(quic_v2_initial_salt); } else { @@ -166,16 +168,23 @@ lws_quic_initiate_key_update(struct lws *wsi) qn = nwsi->quic.qn; /* We only update keys if the handshake is done and we have APP keys */ - if (!qn->handshake_done || !qn->keys[LWS_QUIC_LEVEL_APP]) + if (!qn->handshake_done || !qn->keys[LWS_QUIC_LEVEL_APP]) { + lwsl_wsi_notice(wsi, "QUIC Key Update failed: handshake_done=%d, app_keys=%p", + qn->handshake_done, qn->keys[LWS_QUIC_LEVEL_APP]); return -1; + } /* If an update is already pending, wait for it to be echoed/completed */ - if (qn->key_update_pending) + if (qn->key_update_pending) { + lwsl_wsi_notice(wsi, "QUIC Key Update ignored: pending"); return -1; + } /* Derive the new TX keys */ - if (lws_quic_update_keys(qn->keys[LWS_QUIC_LEVEL_APP], 0)) + if (lws_quic_update_keys(qn->keys[LWS_QUIC_LEVEL_APP], 0)) { + lwsl_wsi_err(wsi, "QUIC Key Update failed: lws_quic_update_keys error"); return -1; + } /* Flip the TX key phase bit */ qn->tx_key_phase ^= 1; @@ -186,6 +195,8 @@ lws_quic_initiate_key_update(struct lws *wsi) /* Reset the packet counter for AEAD limits */ qn->tx_packets_since_update = 0; + lwsl_wsi_notice(wsi, "QUIC TX: Key Update Initiated! tx_key_phase is now %d", qn->tx_key_phase); + return 0; } @@ -213,22 +224,27 @@ lws_quic_set_keys(struct lws *wsi, enum lws_tls_quic_secret_type type, const uin case LWS_TLS_QUIC_SECRET_CLIENT_EARLY: level = LWS_QUIC_LEVEL_EARLY; is_rx = qn->is_server ? 1 : 0; + lwsl_notice("lws_quic_set_keys: CLIENT_EARLY (is_rx=%d, len=%d)\n", is_rx, (int)secret_len); break; case LWS_TLS_QUIC_SECRET_CLIENT_HANDSHAKE: level = LWS_QUIC_LEVEL_HANDSHAKE; is_rx = qn->is_server ? 1 : 0; + lwsl_notice("lws_quic_set_keys: CLIENT_HANDSHAKE (is_rx=%d, len=%d)\n", is_rx, (int)secret_len); break; case LWS_TLS_QUIC_SECRET_SERVER_HANDSHAKE: level = LWS_QUIC_LEVEL_HANDSHAKE; is_rx = qn->is_server ? 0 : 1; + lwsl_notice("lws_quic_set_keys: SERVER_HANDSHAKE (is_rx=%d, len=%d)\n", is_rx, (int)secret_len); break; case LWS_TLS_QUIC_SECRET_CLIENT_APPLICATION: level = LWS_QUIC_LEVEL_APP; is_rx = qn->is_server ? 1 : 0; + lwsl_notice("lws_quic_set_keys: CLIENT_APP (is_rx=%d, len=%d)\n", is_rx, (int)secret_len); break; case LWS_TLS_QUIC_SECRET_SERVER_APPLICATION: level = LWS_QUIC_LEVEL_APP; is_rx = qn->is_server ? 0 : 1; + lwsl_notice("lws_quic_set_keys: SERVER_APP (is_rx=%d, len=%d)\n", is_rx, (int)secret_len); break; default: return -1; @@ -238,6 +254,11 @@ lws_quic_set_keys(struct lws *wsi, enum lws_tls_quic_secret_type type, const uin k = lws_zalloc(sizeof(*k), "quic_keys"); if (!k) return -1; qn->keys[level] = k; + + /* Inherit packet number spaces between 0-RTT and 1-RTT */ + if (level == LWS_QUIC_LEVEL_APP && qn->keys[LWS_QUIC_LEVEL_EARLY]) { + k->pn_tx = qn->keys[LWS_QUIC_LEVEL_EARLY]->pn_tx; + } } else { k = qn->keys[level]; } @@ -262,27 +283,73 @@ lws_quic_set_keys(struct lws *wsi, enum lws_tls_quic_secret_type type, const uin /* For simplicity, we just mark valid and rely on tx/rx logic */ k->valid = 1; +#if defined(LWS_WITH_CLIENT) /* If this is the client deriving the early secret, check if the stream opts in */ if (type == LWS_TLS_QUIC_SECRET_CLIENT_EARLY && !qn->is_server) { qn->early_data_status = LWS_0RTT_STATUS_ATTEMPTED; - /* The initial stream is currently also the network wsi, because ALPN - * hasn't migrated it yet. If it returns 1, it opts into 0-RTT. */ - if (wsi->a.protocol && wsi->a.protocol->callback) { - int ret = wsi->a.protocol->callback(wsi, - LWS_CALLBACK_CLIENT_ESTABLISHED_EARLY, - wsi->user_space, NULL, 0); - if (ret == 1) { - lwsl_wsi_notice(wsi, "Stream %s opted into 0-RTT", lws_wsi_tag(wsi)); - if (wsi->quic.qs) - wsi->quic.qs->opted_into_early_data = 1; - lws_callback_on_writable(wsi); - } else { - lwsl_wsi_notice(wsi, "Stream %s ignored 0-RTT", lws_wsi_tag(wsi)); + /* Trigger ALPN migration immediately to create nwsi and transition wsi to h3 */ + lws_role_call_alpn_negotiated(wsi, "h3"); + + struct lws *nwsi = lws_get_network_wsi(wsi); + if (nwsi) { + lws_wsi_mux_apply_queue(nwsi); + struct lws *w = nwsi->mux.child_list; + while (w) { + if (w->a.protocol && w->a.protocol->callback) { + int ret = w->a.protocol->callback(w, + LWS_CALLBACK_CLIENT_ESTABLISHED_EARLY, + w->user_space, NULL, 0); + if (ret == 1) { + lwsl_wsi_notice(w, "Stream %s opted into 0-RTT", lws_wsi_tag(w)); + + if (!w->quic.qs) { + w->quic.qs = lws_zalloc(sizeof(*w->quic.qs), "quic stream"); + if (w->quic.qs) { + w->quic.qs->wsi = w; + w->quic.qs->stream_id = (w == wsi) ? 0 : w->mux.my_sid; + w->quic.qs->rx_max_data = LWS_QUIC_DEFAULT_WINDOW; + w->quic.qs->advertised_rx_max_data = LWS_QUIC_DEFAULT_WINDOW; + w->quic.qs->rx_window_size = LWS_QUIC_DEFAULT_WINDOW; + w->quic.qs->last_rx_update_us = lws_now_usecs(); + } + } + + lws_role_transition(w, LWSIFR_CLIENT, LRS_H2_WAITING_TO_SEND_HEADERS, &role_ops_h3); + + if (w->quic.qs) + w->quic.qs->opted_into_early_data = 1; + + lws_callback_on_writable(w); + } else { + lwsl_wsi_notice(w, "Stream %s ignored 0-RTT", lws_wsi_tag(w)); + } + } + w = w->mux.sibling_list; } } - } else if (type == LWS_TLS_QUIC_SECRET_CLIENT_EARLY && qn->is_server) { + } else +#endif + if (type == LWS_TLS_QUIC_SECRET_CLIENT_EARLY && qn->is_server) { qn->early_data_status = LWS_0RTT_STATUS_ACCEPTED; + + /* On server, migrate connection to H3 immediately to support 0-RTT stream adoption */ + const unsigned char *prot = NULL; + unsigned int plen = 0; +#if defined(LWS_WITH_GNUTLS) + gnutls_datum_t dt; + if (gnutls_alpn_get_selected_protocol(wsi->tls.ssl, &dt) >= 0) { + prot = dt.data; + plen = dt.size; + } +#endif + if (plen) { + lws_strncpy(wsi->alpn, (const char *)prot, plen + 1); + } else { + lws_strncpy(wsi->alpn, "h3", sizeof(wsi->alpn)); + } + lwsl_wsi_notice(wsi, "QUIC Server 0-RTT ALPN: %s", wsi->alpn); + lws_role_call_alpn_negotiated(wsi, wsi->alpn); } return 0; @@ -300,6 +367,7 @@ lws_quic_keys_destroy(struct lws_quic_keys *keys) gnutls_aead_cipher_deinit((gnutls_aead_cipher_hd_t)keys->aead_tx); #endif + lws_explicit_bzero(keys, sizeof(*keys)); lws_free(keys); } @@ -311,26 +379,38 @@ lws_quic_update_keys(struct lws_quic_keys *k, int is_rx) size_t key_len = (k->cipher_type == 0) ? 16 : 32; if (is_rx) { - enum lws_genhmac_types hash_type = (k->secret_len == 48) ? LWS_GENHMAC_TYPE_SHA384 : LWS_GENHMAC_TYPE_SHA256; - if (lws_genhkdf_expand_label(hash_type, k->secret_rx, k->secret_len, "quic ku", NULL, 0, new_secret, k->secret_len)) return -1; - memcpy(k->secret_rx, new_secret, k->secret_len); - - if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic key", NULL, 0, k->key_aead_rx, key_len)) return -1; - if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic iv", NULL, 0, k->iv_rx, 12)) return -1; - - k->el_aead_rx.buf = k->key_aead_rx; - k->el_aead_rx.len = (uint32_t)key_len; - } else { - enum lws_genhmac_types hash_type = (k->secret_len == 48) ? LWS_GENHMAC_TYPE_SHA384 : LWS_GENHMAC_TYPE_SHA256; - if (lws_genhkdf_expand_label(hash_type, k->secret_tx, k->secret_len, "quic ku", NULL, 0, new_secret, k->secret_len)) return -1; - memcpy(k->secret_tx, new_secret, k->secret_len); - - if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic key", NULL, 0, k->key_aead_tx, key_len)) return -1; - if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic iv", NULL, 0, k->iv_tx, 12)) return -1; - - k->el_aead_tx.buf = k->key_aead_tx; - k->el_aead_tx.len = (uint32_t)key_len; - } + enum lws_genhmac_types hash_type = (k->secret_len == 48) ? LWS_GENHMAC_TYPE_SHA384 : LWS_GENHMAC_TYPE_SHA256; + if (lws_genhkdf_expand_label(hash_type, k->secret_rx, k->secret_len, "quic ku", NULL, 0, new_secret, k->secret_len)) return -1; + memcpy(k->secret_rx, new_secret, k->secret_len); + + if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic key", NULL, 0, k->key_aead_rx, key_len)) return -1; + if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic iv", NULL, 0, k->iv_rx, 12)) return -1; + + k->el_aead_rx.buf = k->key_aead_rx; + k->el_aead_rx.len = (uint32_t)key_len; +#if defined(LWS_WITH_GNUTLS) + if (k->aead_rx) { + gnutls_aead_cipher_deinit((gnutls_aead_cipher_hd_t)k->aead_rx); + k->aead_rx = NULL; + } +#endif + } else { + enum lws_genhmac_types hash_type = (k->secret_len == 48) ? LWS_GENHMAC_TYPE_SHA384 : LWS_GENHMAC_TYPE_SHA256; + if (lws_genhkdf_expand_label(hash_type, k->secret_tx, k->secret_len, "quic ku", NULL, 0, new_secret, k->secret_len)) return -1; + memcpy(k->secret_tx, new_secret, k->secret_len); + + if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic key", NULL, 0, k->key_aead_tx, key_len)) return -1; + if (lws_genhkdf_expand_label(hash_type, new_secret, k->secret_len, "quic iv", NULL, 0, k->iv_tx, 12)) return -1; + + k->el_aead_tx.buf = k->key_aead_tx; + k->el_aead_tx.len = (uint32_t)key_len; +#if defined(LWS_WITH_GNUTLS) + if (k->aead_tx) { + gnutls_aead_cipher_deinit((gnutls_aead_cipher_hd_t)k->aead_tx); + k->aead_tx = NULL; + } +#endif + } lws_explicit_bzero(new_secret, sizeof(new_secret)); return 0; @@ -344,16 +424,19 @@ lws_quic_unmask_header(struct lws_quic_keys *keys, uint8_t *packet, size_t packe uint8_t pn_len; if (sample_offset + 16 > packet_len) - return -1; /* Truncated packet */ + { lwsl_notice("unmask: Truncated\n"); return -1; } memcpy(sample, &packet[sample_offset], 16); if (keys->cipher_type == 0 || keys->cipher_type == 2) { /* AES-GCM uses AES-ECB for Header Protection */ struct lws_genaes_ctx hp; - if (lws_genaes_create(&hp, LWS_GAESO_ENC, LWS_GAESM_ECB, &keys->el_hp_rx, LWS_GAESP_NO_PADDING, NULL)) - return -1; + if (lws_genaes_create(&hp, LWS_GAESO_ENC, LWS_GAESM_ECB, &keys->el_hp_rx, LWS_GAESP_NO_PADDING, NULL)) { + lwsl_notice("unmask: genaes_create failed, el_hp_rx.len=%d, cipher_type=%d, valid=%d, el_aead_rx.len=%d\n", (int)keys->el_hp_rx.len, (int)keys->cipher_type, (int)keys->valid, (int)keys->el_aead_rx.len); + return -1; + } if (lws_genaes_crypt(&hp, sample, 16, mask, NULL, NULL, NULL, 0)) { + lwsl_notice("unmask: genaes_crypt failed\n"); lws_genaes_destroy(&hp, NULL, 0); return -1; } @@ -431,7 +514,7 @@ lws_quic_decrypt_payload(struct lws_quic_keys *keys, uint8_t *packet, size_t pac size_t ct_len = payload_len + 16; size_t pt_len = payload_len; - uint8_t tmp[2048]; + uint8_t tmp[4096]; if (ct_len > sizeof(tmp)) return -1; memcpy(tmp, &packet[payload_offset], ct_len); @@ -521,7 +604,7 @@ lws_quic_encrypt_payload(struct lws_quic_keys *keys, uint8_t *packet, size_t pac hd = (gnutls_aead_cipher_hd_t)keys->aead_tx; size_t ct_len = payload_len + 16; - uint8_t tmp[2048]; + uint8_t tmp[4096]; if (payload_len > sizeof(tmp)) return -1; memcpy(tmp, &packet[payload_offset], payload_len); @@ -643,52 +726,104 @@ lws_quic_encrypt_payload(struct lws_quic_keys *keys, uint8_t *packet, size_t pac int lws_tls_quic_rx_crypto(struct lws *wsi, int level, const uint8_t *buf, size_t len) { - uint8_t *out = lws_malloc(32768, "quic rx crypto"); - size_t out_len = 32768; + struct lws *orig_wsi = wsi; int n; - if (!out) - return -1; - if (len > 0 && wsi->quic.qn) { - lwsl_wsi_notice(wsi, "lws_tls_quic_rx_crypto: level %d, len %zu, buf[0]=%d", level, len, buf[0]); - lwsl_hexdump_notice(buf, len > 32 ? 32 : len); - size_t i = 0; - while (i < len) { - if (wsi->quic.qn->crypto_rx_expected_msg_len[level] > 0) { - size_t consume = wsi->quic.qn->crypto_rx_expected_msg_len[level]; - if (consume > len - i) - consume = len - i; - wsi->quic.qn->crypto_rx_expected_msg_len[level] -= consume; - i += consume; - continue; + if (wsi->quic.qn->crypto_rx_buf_len[level] + len > 262144) { + lwsl_wsi_err(wsi, "QUIC: CRYPTO reassembly buffer size limit exceeded on level %d", level); + return -1; + } + if (wsi->quic.qn->crypto_rx_buf_len[level] > 0) { + uint8_t *new_buf = lws_realloc(wsi->quic.qn->crypto_rx_buf[level], + wsi->quic.qn->crypto_rx_buf_len[level] + len, + "crypto rx buf"); + if (!new_buf) { + return -1; } + memcpy(new_buf + wsi->quic.qn->crypto_rx_buf_len[level], buf, len); + wsi->quic.qn->crypto_rx_buf[level] = new_buf; + wsi->quic.qn->crypto_rx_buf_len[level] += len; - /* We are at the start of a new Handshake message! */ - uint8_t type = buf[i]; - lwsl_wsi_notice(wsi, "QUIC RX CRYPTO: checking message type %d at offset %zu", type, i); + buf = wsi->quic.qn->crypto_rx_buf[level]; + len = wsi->quic.qn->crypto_rx_buf_len[level]; + } + + size_t scan = 0; + size_t complete_len = 0; + while (scan < len) { + if (len - scan < 4) { + break; + } + + uint8_t type = buf[scan]; if (type == 24 || type == 5) { lwsl_wsi_notice(wsi, "QUIC RX CRYPTO: Illegal TLS Handshake type %d", type); lws_quic_enter_closing_state(wsi, 0x0100 + 10 /* unexpected_message */, 0, 0); - lws_free(out); return -1; } - - if (i + 3 >= len) { - /* Fragmented header - extremely rare in h3spec, but we'd need to handle it. - * For now, assume headers are not fragmented across chunks. */ + + uint32_t msg_len = ((uint32_t)buf[scan+1] << 16) | ((uint32_t)buf[scan+2] << 8) | buf[scan+3]; + if (scan + 4 + msg_len > len) { break; } - uint32_t msg_len = ((uint32_t)buf[i+1] << 16) | ((uint32_t)buf[i+2] << 8) | buf[i+3]; - wsi->quic.qn->crypto_rx_expected_msg_len[level] = msg_len; - lwsl_wsi_notice(wsi, "QUIC RX CRYPTO: Expecting %u bytes for message type %d", msg_len, type); - i += 4; + scan += 4 + msg_len; + complete_len = scan; + } + + if (complete_len == 0) { + if (wsi->quic.qn->crypto_rx_buf_len[level] == 0) { + uint8_t *new_buf = lws_malloc(len, "crypto rx buf"); + if (!new_buf) { + return -1; + } + memcpy(new_buf, buf, len); + wsi->quic.qn->crypto_rx_buf[level] = new_buf; + wsi->quic.qn->crypto_rx_buf_len[level] = len; + } + return 0; + } + + n = lws_tls_quic_advance_handshake(wsi, level, buf, complete_len, NULL, NULL); + + { + struct lws *nwsi = lws_get_quic_network_wsi(wsi); + if (nwsi) wsi = nwsi; + } + + if (n < 0) { + goto error_handling; } - } - n = lws_tls_quic_advance_handshake(wsi, level, buf, len, out, &out_len); + size_t remainder = len - complete_len; + if (remainder > 0) { + if (wsi->quic.qn->crypto_rx_buf_len[level] == 0) { + uint8_t *new_buf = lws_malloc(remainder, "crypto rx buf"); + if (new_buf) { + memcpy(new_buf, buf + complete_len, remainder); + wsi->quic.qn->crypto_rx_buf[level] = new_buf; + } + } else { + memmove(wsi->quic.qn->crypto_rx_buf[level], buf + complete_len, remainder); + } + wsi->quic.qn->crypto_rx_buf_len[level] = remainder; + } else { + if (wsi->quic.qn->crypto_rx_buf_len[level] > 0) { + lws_free(wsi->quic.qn->crypto_rx_buf[level]); + wsi->quic.qn->crypto_rx_buf[level] = NULL; + wsi->quic.qn->crypto_rx_buf_len[level] = 0; + } + } + } else { + n = lws_tls_quic_advance_handshake(wsi, level, buf, len, NULL, NULL); + { + struct lws *nwsi = lws_get_quic_network_wsi(wsi); + if (nwsi) wsi = nwsi; + } + } +error_handling: if (n < 0) { #if defined(LWS_WITH_GNUTLS) int alert_level = 0; @@ -713,15 +848,9 @@ lws_tls_quic_rx_crypto(struct lws *wsi, int level, const uint8_t *buf, size_t le lws_quic_enter_closing_state(wsi, 0x0100 + 10 /* unexpected_message fallback */, 0, 0); } #endif - lws_free(out); return -1; } - if (out_len > 0) { - /* Pass the generated TX CRYPTO data back to the QUIC transport queues */ - lws_tls_quic_tx_crypto_cb(wsi, level, out, out_len); - } - lwsl_wsi_debug(wsi, "lws_tls_quic_advance_handshake returned %d, tp_parsed=%d", n, wsi->quic.qn ? wsi->quic.qn->tp_parsed : -1); @@ -734,16 +863,14 @@ lws_tls_quic_rx_crypto(struct lws *wsi, int level, const uint8_t *buf, size_t le if (lws_quic_parse_transport_parameters(wsi, peer_tp, peer_tp_len) < 0) { lwsl_wsi_err(wsi, "QUIC transport parameters validation failed"); lws_quic_enter_closing_state(wsi, LWS_QUIC_ERR_TRANSPORT_PARAMETER_ERROR, 0, 0); - lws_free(out); return -1; } } else { lwsl_wsi_debug(wsi, "lws_tls_quic_get_transport_parameters returned non-zero or NULL"); } - if (wsi->quic.qn->is_server && out_len > 0 && !wsi->quic.qn->tp_parsed) { + if (wsi->quic.qn->is_server && wsi->quic.qn->crypto_tx_offset[LWS_QUIC_LEVEL_INITIAL] > 0 && !wsi->quic.qn->tp_parsed) { lwsl_wsi_err(wsi, "QUIC Peer provided no transport parameters in ClientHello!"); lws_quic_enter_closing_state(wsi, 0x0100 + 109 /* missing_extension */, 0, 0); - lws_free(out); return -1; } } @@ -754,7 +881,6 @@ lws_tls_quic_rx_crypto(struct lws *wsi, int level, const uint8_t *buf, size_t le if (!wsi->quic.qn->tp_parsed) { lwsl_wsi_err(wsi, "QUIC Peer provided no transport parameters!"); lws_quic_enter_closing_state(wsi, 0x0100 + 109 /* missing_extension */, 0, 0); - lws_free(out); return -1; } @@ -801,28 +927,26 @@ lws_tls_quic_rx_crypto(struct lws *wsi, int level, const uint8_t *buf, size_t le lwsl_wsi_notice(wsi, "QUIC ALPN negotiated: %s", wsi->alpn); lws_role_call_alpn_negotiated(wsi, wsi->alpn); } else if (wsi->alpn[0]) { - lwsl_wsi_notice(wsi, "QUIC ALPN already negotiated: %s", wsi->alpn); - lws_role_call_alpn_negotiated(wsi, wsi->alpn); + lwsl_wsi_notice(wsi, "QUIC ALPN already negotiated: %s", wsi->alpn); + lws_role_call_alpn_negotiated(wsi, wsi->alpn); } else { lwsl_wsi_err(wsi, "QUIC requires ALPN, but none was negotiated!"); lws_quic_enter_closing_state(wsi, 0x0100 + 120 /* no_application_protocol */, 0, 0); - lws_free(out); return -1; } } #endif - if (wsi->role_ops) { - enum lws_callback_reasons cb = (enum lws_callback_reasons)wsi->role_ops->adoption_cb[lwsi_role_server(wsi)]; - if (cb && wsi->a.protocol && wsi->a.protocol->callback) { - wsi->a.protocol->callback(wsi, cb, wsi->user_space, NULL, 0); + if (orig_wsi->role_ops) { + enum lws_callback_reasons cb = (enum lws_callback_reasons)orig_wsi->role_ops->adoption_cb[lwsi_role_server(orig_wsi)]; + if (cb && orig_wsi->a.protocol && orig_wsi->a.protocol->callback) { + orig_wsi->a.protocol->callback(orig_wsi, cb, orig_wsi->user_space, NULL, 0); } } - lws_callback_on_writable(wsi); + lws_callback_on_writable(orig_wsi); } - lws_free(out); return 0; } @@ -939,6 +1063,7 @@ lws_quic_create_retry_token(struct lws *wsi, uint8_t pt[256]; size_t pt_len = 0; uint8_t nonce[12]; + uint64_t now = (uint64_t)lws_now_usecs(); lws_get_random(wsi->a.context, nonce, 12); @@ -954,6 +1079,17 @@ lws_quic_create_retry_token(struct lws *wsi, memcpy(&pt[pt_len], client_ip, ip_len); pt_len += ip_len; + if (pt_len + 8 > sizeof(pt)) + return -1; + pt[pt_len++] = (uint8_t)(now >> 56); + pt[pt_len++] = (uint8_t)(now >> 48); + pt[pt_len++] = (uint8_t)(now >> 40); + pt[pt_len++] = (uint8_t)(now >> 32); + pt[pt_len++] = (uint8_t)(now >> 24); + pt[pt_len++] = (uint8_t)(now >> 16); + pt[pt_len++] = (uint8_t)(now >> 8); + pt[pt_len++] = (uint8_t)now; + struct lws_gencrypto_keyelem keys[1]; keys[0].buf = wsi->a.context->quic_retry_secret; keys[0].len = 16; @@ -1027,6 +1163,23 @@ lws_quic_validate_retry_token(struct lws *wsi, const uint8_t *token, size_t toke if (pt[p++] != ip_len) return -1; if (p + ip_len > ct_len || p + ip_len > sizeof(pt)) return -1; if (ip_len && memcmp(pt + p, client_ip, ip_len)) return -1; + p += ip_len; + + if (p + 8 > ct_len || p + 8 > sizeof(pt)) return -1; + uint64_t token_time = ((uint64_t)pt[p] << 56) | + ((uint64_t)pt[p+1] << 48) | + ((uint64_t)pt[p+2] << 40) | + ((uint64_t)pt[p+3] << 32) | + ((uint64_t)pt[p+4] << 24) | + ((uint64_t)pt[p+5] << 16) | + ((uint64_t)pt[p+6] << 8) | + pt[p+7]; + + uint64_t now = (uint64_t)lws_now_usecs(); + if (now < token_time || now - token_time > 15ULL * 1000000ULL) { + lwsl_wsi_notice(wsi, "QUIC: Retry token expired. age = %lld us", (long long)(now - token_time)); + return -1; + } return 0; } diff --git a/lib/roles/quic/ops-quic-cc-cubic.c b/lib/roles/quic/ops-quic-cc-cubic.c index 4edf5a5b46..52547e9c90 100644 --- a/lib/roles/quic/ops-quic-cc-cubic.c +++ b/lib/roles/quic/ops-quic-cc-cubic.c @@ -76,7 +76,10 @@ cubic_init(struct lws *nwsi) st = (struct lws_quic_cc_cubic *)qn->cc_state; /* RFC 9002: Initial Window */ - st->cwnd = 10 * mtu; + if (nwsi->a.context->quic_initial_cwnd) + st->cwnd = nwsi->a.context->quic_initial_cwnd; + else + st->cwnd = 10 * mtu; st->ssthresh = (size_t)-1; /* Infinity */ st->bytes_in_flight = 0; st->congestion_recovery_start_time = 0; @@ -89,7 +92,7 @@ cubic_init(struct lws *nwsi) st->k = 0; st->is_in_fast_convergence = 0; - lwsl_cx_info(nwsi->a.context, "QUIC CUBIC: init cwnd=%zu, mtu=%u", st->cwnd, mtu); + lwsl_notice("QUIC CUBIC: init cwnd=%zu, mtu=%u", st->cwnd, mtu); } static void @@ -237,7 +240,12 @@ cubic_can_send(struct lws *nwsi, size_t bytes) if (!st) return 0; - return (st->bytes_in_flight + bytes <= st->cwnd); + int ok = (st->bytes_in_flight + bytes <= st->cwnd); + if (!ok) { + lwsl_notice("AGY-DEBUG: cubic_can_send failed: bytes_in_flight=%zu, bytes=%zu, cwnd=%zu\n", + st->bytes_in_flight, bytes, st->cwnd); + } + return ok; } static lws_usec_t @@ -255,32 +263,44 @@ cubic_get_pacing_delay(struct lws *nwsi, size_t bytes_to_send) lws_usec_t now = lws_now_usecs(); lws_usec_t elapsed = now - st->last_pacing_time; - st->last_pacing_time = now; /* Replenish credit based on elapsed time: R = cwnd / srtt */ size_t credit_added = (size_t)(((uint64_t)elapsed * (uint64_t)st->cwnd) / (uint64_t)rtt); st->pacing_credit += credit_added; - /* Cap credit to max burst to prevent micro-bursts (e.g. 10 packets) */ - size_t max_burst = 10 * (lws_get_vhost(nwsi)->quic_mtu ? lws_get_vhost(nwsi)->quic_mtu : 1280); + /* + * Cap credit to max burst (64 packets) to prevent very large bursts + * while still allowing good throughput for large concurrent stream + * queues (e.g. 1999 streams in the multiplexing test). + */ + size_t max_burst = 64 * (lws_get_vhost(nwsi)->quic_mtu ? lws_get_vhost(nwsi)->quic_mtu : 1280); if (st->pacing_credit > max_burst) st->pacing_credit = max_burst; if (st->pacing_credit >= bytes_to_send) { - /* We have enough credit to send this packet */ + /* + * Enough credit to send this packet: consume credit and + * record the time only when we actually allow a send. + * See newreno equivalent for the rationale. + */ + st->last_pacing_time = now; return 0; } - /* Not enough credit. Calculate how long it will take to earn the missing credit. */ + /* Not enough credit. Calculate how long to wait for the missing credit. */ size_t missing_credit = bytes_to_send - st->pacing_credit; delay_us = (lws_usec_t)(((uint64_t)missing_credit * (uint64_t)rtt) / (uint64_t)(st->cwnd ? st->cwnd : 1)); if (delay_us == 0) delay_us = 1; + /* Do NOT update last_pacing_time here: credit already earned in + * pacing_credit is preserved for the next call. */ + return delay_us; } + const struct lws_cc_ops lws_cc_ops_cubic = { .init = cubic_init, .on_sent = cubic_on_sent, diff --git a/lib/roles/quic/ops-quic-cc-newreno.c b/lib/roles/quic/ops-quic-cc-newreno.c index cb24a8d0ff..3d59a28e3d 100644 --- a/lib/roles/quic/ops-quic-cc-newreno.c +++ b/lib/roles/quic/ops-quic-cc-newreno.c @@ -16,15 +16,6 @@ #include "private-lib-core.h" #include "private-lib-roles-quic.h" -struct lws_quic_cc_newreno { - size_t cwnd; - size_t ssthresh; - size_t bytes_in_flight; - lws_usec_t congestion_recovery_start_time; - - lws_usec_t last_pacing_time; - size_t pacing_credit; -}; static void newreno_init(struct lws *nwsi) @@ -43,14 +34,17 @@ newreno_init(struct lws *nwsi) st = (struct lws_quic_cc_newreno *)qn->cc_state; /* RFC 9002: Initial Window */ - st->cwnd = 10 * mtu; + if (nwsi->a.context->quic_initial_cwnd) + st->cwnd = nwsi->a.context->quic_initial_cwnd; + else + st->cwnd = 10 * mtu; st->ssthresh = (size_t)-1; /* Infinity */ st->bytes_in_flight = 0; st->congestion_recovery_start_time = 0; st->last_pacing_time = lws_now_usecs(); st->pacing_credit = st->cwnd; /* initial burst allowed */ - lwsl_cx_info(nwsi->a.context, "QUIC NewReno: init cwnd=%zu, mtu=%u", st->cwnd, mtu); + lwsl_notice("QUIC NewReno: init cwnd=%zu, mtu=%u", st->cwnd, mtu); } static void @@ -105,6 +99,20 @@ newreno_on_ack(struct lws *nwsi, size_t bytes_acked, lws_usec_t rtt) } } +static void +newreno_on_discard(struct lws *nwsi, size_t bytes_discarded) +{ + struct lws_quic_netconn *qn = nwsi->quic.qn; + struct lws_quic_cc_newreno *st = (struct lws_quic_cc_newreno *)qn->cc_state; + + if (!st) return; + + if (st->bytes_in_flight >= bytes_discarded) + st->bytes_in_flight -= bytes_discarded; + else + st->bytes_in_flight = 0; +} + static void newreno_on_loss(struct lws *nwsi, size_t bytes_lost) { @@ -147,7 +155,12 @@ newreno_can_send(struct lws *nwsi, size_t bytes) if (!st) return 0; - return (st->bytes_in_flight + bytes <= st->cwnd); + int ok = (st->bytes_in_flight + bytes <= st->cwnd); + if (!ok) { + lwsl_notice("AGY-DEBUG: newreno_can_send failed: bytes_in_flight=%zu, bytes=%zu, cwnd=%zu\n", + st->bytes_in_flight, bytes, st->cwnd); + } + return ok; } static lws_usec_t @@ -165,29 +178,44 @@ newreno_get_pacing_delay(struct lws *nwsi, size_t bytes_to_send) lws_usec_t now = lws_now_usecs(); lws_usec_t elapsed = now - st->last_pacing_time; - st->last_pacing_time = now; /* Replenish credit based on elapsed time: R = cwnd / srtt */ size_t credit_added = (size_t)(((uint64_t)elapsed * (uint64_t)st->cwnd) / (uint64_t)rtt); st->pacing_credit += credit_added; - /* Cap credit to max burst to prevent micro-bursts (e.g. 10 packets) */ - size_t max_burst = 10 * (lws_get_vhost(nwsi)->quic_mtu ? lws_get_vhost(nwsi)->quic_mtu : 1280); + /* + * Cap credit to max burst (64 packets) to prevent very large bursts + * while still allowing good throughput for large concurrent stream + * queues (e.g. 1999 streams in the multiplexing test). + */ + size_t max_burst = 64 * (lws_get_vhost(nwsi)->quic_mtu ? lws_get_vhost(nwsi)->quic_mtu : 1280); if (st->pacing_credit > max_burst) st->pacing_credit = max_burst; if (st->pacing_credit >= bytes_to_send) { - /* We have enough credit to send this packet */ + /* + * Enough credit to send this packet: consume credit and + * record the time only when we actually allow a send. + * This is critical: if we updated last_pacing_time even + * when blocking, the next call after a short timer would + * compute elapsed ≈ 0 → near-zero credit added → endless + * tight-loop stall. + */ + st->last_pacing_time = now; return 0; } - /* Not enough credit. Calculate how long it will take to earn the missing credit. */ + /* Not enough credit. Calculate how long to wait for the missing credit. */ size_t missing_credit = bytes_to_send - st->pacing_credit; delay_us = (lws_usec_t)(((uint64_t)missing_credit * (uint64_t)rtt) / (uint64_t)st->cwnd); if (delay_us == 0) delay_us = 1; + /* Do NOT update last_pacing_time here: the credit already earned + * (credit_added) is preserved in st->pacing_credit, and next call + * the elapsed time from *now* will be added on top of it. */ + return delay_us; } @@ -196,6 +224,7 @@ const struct lws_cc_ops lws_cc_ops_newreno = { .on_sent = newreno_on_sent, .on_ack = newreno_on_ack, .on_loss = newreno_on_loss, + .on_discard = newreno_on_discard, .can_send = newreno_can_send, .get_pacing_delay = newreno_get_pacing_delay, }; diff --git a/lib/roles/quic/ops-quic.c b/lib/roles/quic/ops-quic.c index 7bec9283cb..d75da0ea7d 100644 --- a/lib/roles/quic/ops-quic.c +++ b/lib/roles/quic/ops-quic.c @@ -29,6 +29,24 @@ static int quic_secret_cb(struct lws *wsi, enum lws_tls_quic_secret_type type, const uint8_t *secret, size_t secret_len); +void +lws_quic_queue_path_challenge(struct lws *nwsi) +{ + if (!nwsi || !nwsi->quic.qn) return; + struct lws_quic_tx_frame *f_pc = lws_zalloc(sizeof(*f_pc) + 8, "quic path_chall"); + if (f_pc) { + f_pc->type = LWS_QUIC_FT_PATH_CHALLENGE; + f_pc->len = 8; + f_pc->data = (uint8_t *)&f_pc[1]; + lws_get_random(nwsi->a.context, f_pc->data, 8); + memcpy(nwsi->quic.qn->path_challenge, f_pc->data, 8); + nwsi->quic.qn->path_challenge_pending = 1; + + lws_dll2_add_tail(&f_pc->list, &nwsi->quic.qn->pending_tx[LWS_QUIC_LEVEL_APP]); + lws_callback_on_writable(nwsi); + } +} + static void lws_quic_pacer_cb(lws_sorted_usec_list_t *sul) { @@ -49,34 +67,54 @@ lws_quic_pto_cb(lws_sorted_usec_list_t *sul) return; } - qn->pto_probe_needed = 1; + qn->pto_probe_needed = 2; #if (_LWS_ENABLED_LOGS & LLL_INFO) LWS_RATELIMIT_DEFINE_STATIC(rl); lwsl_ratelimit_info(&rl, 1000000, "QUIC PTO Timer Fired! Forcing POLLOUT for retransmission sweep\n"); #endif int sent_ping = 0; - /* Add a PING frame to force a packet out if we don't have pending tx */ - for (int i = 0; i < LWS_QUIC_LEVEL_COUNT; i++) { + size_t total_bytes_lost = 0; + /* Aggressively retransmit Initial/Handshake frames (RFC 9002 6.2.4) */ + for (int i = 0; i < LWS_QUIC_LEVEL_APP; i++) { if (qn->in_flight[i].count) { - struct lws_quic_tx_frame *ping = lws_zalloc(sizeof(*ping), "pto ping"); - if (ping) { - ping->type = LWS_QUIC_FT_PING; - lws_dll2_add_tail(&ping->list, &qn->pending_tx[i]); - lwsl_wsi_notice(qn->nwsi, "QUIC PTO: Enqueued PING for level %d", i); - } + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->in_flight[i].head) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + lws_dll2_remove(d); + total_bytes_lost += f->wire_len; + f->wire_len = 0; /* CRITICAL: Reset wire_len */ + lws_dll2_add_tail(&f->list, &qn->pending_tx[i]); + } lws_end_foreach_dll_safe(d, d1); + lwsl_wsi_notice(qn->nwsi, "QUIC PTO: Retransmitting Initial/Handshake for level %d", i); sent_ping = 1; - break; } } - if (!sent_ping && !qn->is_server && !qn->handshake_done) { + if (total_bytes_lost && qn->cc_ops && qn->cc_ops->on_loss) + qn->cc_ops->on_loss(qn->nwsi, total_bytes_lost); + + /* For App Data, just send a PING to elicit an ACK */ + if (!sent_ping && qn->in_flight[LWS_QUIC_LEVEL_APP].count) { + struct lws_quic_tx_frame *ping = lws_zalloc(sizeof(*ping), "pto ping"); + if (ping) { + ping->type = LWS_QUIC_FT_PING; + /* Add at HEAD so the PING is serialized first, before data frames + * fill the MTU and cause pto_probe_needed to decrement prematurely */ + lws_dll2_add_head(&ping->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); + lwsl_wsi_notice(qn->nwsi, "QUIC PTO: Enqueued PING for level 3"); + qn->pto_probe_needed = 1; + } + sent_ping = 1; + } + + if (!sent_ping && !qn->handshake_done) { int target_level = LWS_QUIC_LEVEL_INITIAL; if (qn->keys[LWS_QUIC_LEVEL_HANDSHAKE]) target_level = LWS_QUIC_LEVEL_HANDSHAKE; struct lws_quic_tx_frame *ping = lws_zalloc(sizeof(*ping), "pto ping"); if (ping) { ping->type = LWS_QUIC_FT_PING; lws_dll2_add_tail(&ping->list, &qn->pending_tx[target_level]); - lwsl_wsi_notice(qn->nwsi, "QUIC PTO: Enqueued PING for level %d (no in-flight, client handshake incomplete)", target_level); + lwsl_wsi_notice(qn->nwsi, "QUIC PTO: Enqueued PING for level %d (no in-flight, handshake incomplete)", target_level); + qn->pto_probe_needed = 1; } sent_ping = 1; } @@ -92,7 +130,7 @@ lws_quic_pto_cb(lws_sorted_usec_list_t *sul) } } - if (any_in_flight || (!qn->is_server && !qn->handshake_done)) { + if (any_in_flight || !qn->handshake_done) { lws_usec_t pto_base = qn->smoothed_rtt ? (qn->smoothed_rtt + (4 * qn->rttvar) + 25000) : LWS_QUIC_DEFAULT_PTO_US; lws_usec_t pto_delay = pto_base << qn->pto_count; if (pto_delay > 10000000) @@ -120,6 +158,50 @@ lws_quic_decode_packet_number(uint64_t largest_pn, uint64_t truncated_pn, int pn return candidate_pn; } +void +lws_quic_detect_loss(struct lws *nwsi, int level, uint64_t largest_acked) +{ + struct lws_quic_netconn *qn = nwsi->quic.qn; + if (!qn) return; + + size_t total_bytes_lost = 0; + lws_usec_t now = lws_now_usecs(); + lws_usec_t loss_time = qn->smoothed_rtt ? (qn->smoothed_rtt * 9 / 8) : 50000; + + int check_levels[] = { level, level == LWS_QUIC_LEVEL_APP ? LWS_QUIC_LEVEL_EARLY : -1 }; + for (int i = 0; i < 2; i++) { + int curlvl = check_levels[i]; + if (curlvl == -1) continue; + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->in_flight[curlvl].head) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + + if (f->sent_in_pn <= largest_acked && + (f->sent_in_pn + 3 <= largest_acked || (f->sent_in_pn < largest_acked && now > f->sent_time_us + loss_time))) { + + /* Mark as lost */ + total_bytes_lost += f->wire_len; + lwsl_wsi_info(nwsi, "QUIC LOSS: Packet %llu lost, frame %d", (unsigned long long)f->sent_in_pn, f->type); + + lws_dll2_remove(&f->list); + f->wire_len = 0; + + int pending_lvl = curlvl; + if (curlvl == LWS_QUIC_LEVEL_EARLY) + pending_lvl = LWS_QUIC_LEVEL_APP; + + lws_dll2_add_tail(&f->list, &qn->pending_tx[pending_lvl]); + } + } lws_end_foreach_dll_safe(d, d1); + } + + if (total_bytes_lost && qn->cc_ops && qn->cc_ops->on_loss) + qn->cc_ops->on_loss(nwsi, total_bytes_lost); + + if (total_bytes_lost) + lws_callback_on_writable(nwsi); +} + void lws_quic_handle_ack(struct lws *nwsi, int level, uint64_t acked_pn) { @@ -142,27 +224,33 @@ lws_quic_handle_ack(struct lws *nwsi, int level, uint64_t acked_pn) lws_usec_t rtt = 0; lws_usec_t now = lws_now_usecs(); - lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->in_flight[level].head) { - struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + int check_levels[] = { level, level == LWS_QUIC_LEVEL_APP ? LWS_QUIC_LEVEL_EARLY : -1 }; + for (int i = 0; i < 2; i++) { + int curlvl = check_levels[i]; + if (curlvl == -1) continue; - if (f->sent_in_pn == acked_pn) { - uint64_t sid = f->stream_id; - bytes_acked += f->wire_len; - rtt = now > f->sent_time_us ? now - f->sent_time_us : 0; - /* Packet was received successfully, free the frame! */ - lws_dll2_remove(&f->list); - lws_free(f); + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->in_flight[curlvl].head) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); - struct lws *child = lws_quic_stream_find(nwsi, sid); - if (child && (lwsi_state(child) == LRS_FLUSHING_BEFORE_CLOSE + if (f->sent_in_pn == acked_pn) { + uint64_t sid = f->stream_id; + bytes_acked += f->wire_len; + rtt = now > f->sent_time_us ? now - f->sent_time_us : 0; + /* Packet was received successfully, free the frame! */ + lws_dll2_remove(&f->list); + lws_free(f); + + struct lws *child = lws_quic_stream_find(nwsi, sid); + if (child && (lwsi_state(child) == LRS_FLUSHING_BEFORE_CLOSE #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) || defined(LWS_ROLE_H3) - || child->http.deferred_transaction_completed + || child->http.deferred_transaction_completed #endif - )) { - lws_callback_on_writable(child); + )) { + lws_callback_on_writable(child); + } } - } - } lws_end_foreach_dll_safe(d, d1); + } lws_end_foreach_dll_safe(d, d1); + } if (bytes_acked) { lws_callback_on_writable(nwsi); @@ -194,21 +282,66 @@ lws_quic_handle_ack(struct lws *nwsi, int level, uint64_t acked_pn) } if (pending) lws_callback_on_writable(nwsi); + + { + struct lws_quic_cc_newreno *_cc = qn->cc_ops ? (struct lws_quic_cc_newreno *)qn->cc_state : NULL; + (void)_cc; + lwsl_debug("QUIC TX: ACK processing: bytes_acked=%zu, cc_bif=%zu cc_cwnd=%zu cc_pacing=%zu, in_flight_app=%d, pending_tx=%d, blocked=%d\n", + bytes_acked, + _cc ? _cc->bytes_in_flight : 0, + _cc ? _cc->cwnd : 0, + _cc ? _cc->pacing_credit : 0, + qn->in_flight[LWS_QUIC_LEVEL_APP].count, + qn->pending_tx[LWS_QUIC_LEVEL_APP].count, + (int)(qn->cc_ops && qn->cc_ops->can_send ? !qn->cc_ops->can_send(nwsi, 1000) : 0)); + } + + + /* + * ACKs opened the congestion window. Child streams may have + * been stalled by the application-layer pacing throttle in + * rops_tx_credit_quic() (64KB pending_tx cap). Wake them up + * so they can generate more application data. + */ + { + struct lws *w = nwsi->mux.child_list; + + while (w) { + if (w->mux.requested_POLLOUT) + lws_callback_on_writable(w); + w = w->mux.sibling_list; + } + } } /* If there are no more in-flight packets across all levels, we can cancel the PTO timer */ - int any_in_flight = 0; - for (int i = 0; i < LWS_QUIC_LEVEL_COUNT; i++) { - if (qn->in_flight[i].count) { - any_in_flight = 1; - break; + { + int any_in_flight2 = 0; + for (int i2 = 0; i2 < LWS_QUIC_LEVEL_COUNT; i2++) { + if (qn->in_flight[i2].count) { + any_in_flight2 = 1; + break; + } } - } - if (!any_in_flight) { - if (!qn->is_server && !qn->handshake_done) { - /* Keep PTO timer running to probe if server gets blocked (RFC 9002 6.2.2.1) */ - } else { - lws_sul_cancel(&qn->pto_sul); + if (!any_in_flight2) { + if (!qn->handshake_done) { /* was !qn->is_server && ... */ + /* Keep PTO timer running to probe if peer gets blocked (RFC 9002 6.2.2.1) */ + lwsl_notice("lws_quic_handle_ack: Keeping PTO timer active (no in-flight, handshake incomplete)\n"); + } else { + lws_sul_cancel(&qn->pto_sul); + } + } else if (bytes_acked) { + /* + * RFC 9002 Section 6.2.1: When a packet is acknowledged, the + * PTO timer must be updated. Reset it using pto_count=0 (since + * we just reset pto_count above) so the doubled timer from the + * PTO callback is replaced by a fresh base-rate timer. + */ + lws_usec_t pto_base2 = qn->smoothed_rtt ? + (qn->smoothed_rtt + (4 * qn->rttvar) + 25000) : + LWS_QUIC_DEFAULT_PTO_US; + lws_sul_schedule(nwsi->a.context, 0, &qn->pto_sul, + lws_quic_pto_cb, pto_base2); } } } @@ -397,6 +530,13 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, if (nwsi->quic.qn->rem_cid.len != scid.len || memcmp(nwsi->quic.qn->rem_cid.id, scid.id, scid.len)) { nwsi->quic.qn->rem_cid = scid; } + + /* Check if the server upgraded the version (Compatible Version Negotiation) */ + uint32_t pkt_version = ((uint32_t)p[1] << 24) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 8) | p[4]; + if (pkt_version != nwsi->quic.qn->version && pkt_version == LWS_QUIC_VERSION_2) { + nwsi->quic.qn->version = pkt_version; + lwsl_wsi_notice(wsi, "QUIC RX: Upgraded to QUIC v2 via Compatible Version Negotiation"); + } } } } else { @@ -536,9 +676,12 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, nwsi->quic.qn->nwsi = nwsi; nwsi->quic.qn->is_server = 1; + nwsi->quic.qn->next_stream_id_bidi_local = 1; + nwsi->quic.qn->next_stream_id_unidi_local = 3; nwsi->quic.qn->version = pkt_version; - nwsi->quic.qn->max_streams_bidi_local = 100; - nwsi->quic.qn->max_streams_unidi_local = 100; + nwsi->quic.qn->original_version = pkt_version; + nwsi->quic.qn->max_streams_bidi_local = 400; + nwsi->quic.qn->max_streams_unidi_local = 400; nwsi->quic.qn->current_mtu = 1280; nwsi->quic.qn->probed_mtu = 1380; /* first probe size */ @@ -597,8 +740,13 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, lws_get_random(wsi->a.context, nwsi->quic.qn->loc_cid.id, 8); { - uint8_t *tp = nwsi->quic.qn->local_tp_buf; - uint8_t *tp_end = tp + sizeof(nwsi->quic.qn->local_tp_buf); + uint8_t *local_tp_buf = lws_malloc(4096, "quic tp scratch"); + if (!local_tp_buf) { + lwsl_wsi_err(wsi, "OOM allocating tp scratch buffer"); + return LWS_HPI_RET_HANDLED; + } + uint8_t *tp = local_tp_buf; + uint8_t *tp_end = tp + 4096; #define LWS_QUIC_WRITE_TP_VARINT(_id, _val) \ do { \ @@ -620,7 +768,7 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, tp += (_len); \ } while (0) - lwsl_notice("QUIC CLIENT: Writing TP buffer! buf size %d", (int)(tp - wsi->quic.qn->local_tp_buf)); LWS_QUIC_WRITE_TP_VARINT(0x04, LWS_QUIC_DEFAULT_WINDOW); + lwsl_notice("QUIC CLIENT: Writing TP buffer! buf size %d", (int)(tp - local_tp_buf)); LWS_QUIC_WRITE_TP_VARINT(0x04, LWS_QUIC_DEFAULT_WINDOW); LWS_QUIC_WRITE_TP_VARINT(0x05, LWS_QUIC_DEFAULT_WINDOW); LWS_QUIC_WRITE_TP_VARINT(0x06, LWS_QUIC_DEFAULT_WINDOW); LWS_QUIC_WRITE_TP_VARINT(0x07, LWS_QUIC_DEFAULT_WINDOW); @@ -634,6 +782,22 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, if (nwsi->quic.qn->retry_scid.len) { LWS_QUIC_WRITE_TP_BUF(0x10, nwsi->quic.qn->retry_scid.id, nwsi->quic.qn->retry_scid.len); } + if (wsi->a.context->options & LWS_SERVER_OPTION_QUIC_LATEST_VERSION) { + uint8_t vi_buf[12]; + vi_buf[0] = (uint8_t)(nwsi->quic.qn->version >> 24); + vi_buf[1] = (uint8_t)(nwsi->quic.qn->version >> 16); + vi_buf[2] = (uint8_t)(nwsi->quic.qn->version >> 8); + vi_buf[3] = (uint8_t)(nwsi->quic.qn->version); + vi_buf[4] = (uint8_t)(LWS_QUIC_VERSION_1 >> 24); + vi_buf[5] = (uint8_t)(LWS_QUIC_VERSION_1 >> 16); + vi_buf[6] = (uint8_t)(LWS_QUIC_VERSION_1 >> 8); + vi_buf[7] = (uint8_t)(LWS_QUIC_VERSION_1); + vi_buf[8] = (uint8_t)(LWS_QUIC_VERSION_2 >> 24); + vi_buf[9] = (uint8_t)(LWS_QUIC_VERSION_2 >> 16); + vi_buf[10] = (uint8_t)(LWS_QUIC_VERSION_2 >> 8); + vi_buf[11] = (uint8_t)(LWS_QUIC_VERSION_2); + LWS_QUIC_WRITE_TP_BUF(0x11, vi_buf, 12); + } if (wsi->a.vhost->options & LWS_SERVER_OPTION_QUIC_PAD_CRYPTO) { lwsl_notice("QUIC TX: Padding handshake crypto data to artificially hit anti-amplification limits\n"); @@ -646,10 +810,100 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, tp += 3000; } - lws_tls_quic_set_transport_parameters(nwsi, nwsi->quic.qn->local_tp_buf, (size_t)(tp - nwsi->quic.qn->local_tp_buf)); - + if (wsi->a.vhost->quic_preferred_addresses) { + lwsl_notice("QUIC TX: Processing preferred_address config: '%s'\n", wsi->a.vhost->quic_preferred_addresses); + const char *p = wsi->a.vhost->quic_preferred_addresses; + lws_sockaddr46 sa4, sa6; + memset(&sa4, 0, sizeof(sa4)); + memset(&sa6, 0, sizeof(sa6)); + + while (p && *p) { + char chunk[128]; + const char *comma = strchr(p, ','); + size_t len = comma ? (size_t)(comma - p) : strlen(p); + if (len >= sizeof(chunk)) len = sizeof(chunk) - 1; + memcpy(chunk, p, len); + chunk[len] = '\0'; + + char *colon = strrchr(chunk, ':'); + int port = 443; + if (colon && colon > strchr(chunk, ']')) { + port = atoi(colon + 1); + *colon = '\0'; + } else if (colon && !strchr(chunk, ']')) { + port = atoi(colon + 1); + *colon = '\0'; + } + + char *addr = chunk; + if (addr[0] == '[') { + addr++; + char *rb = strchr(addr, ']'); + if (rb) *rb = '\0'; + } + + lws_sockaddr46 sa; + memset(&sa, 0, sizeof(sa)); + if (lws_sa46_parse_numeric_address(addr, &sa) == 0) { + if (sa.sa4.sin_family == AF_INET) { + sa4 = sa; + sa4.sa4.sin_port = htons((uint16_t)port); +#if defined(LWS_WITH_IPV6) + } else { + sa6 = sa; + sa6.sa6.sin6_port = htons((uint16_t)port); +#endif + } + } + p = comma ? comma + 1 : NULL; + } + + /* We only send preferred_address if we have a valid address */ +#if defined(LWS_WITH_IPV6) + if (sa4.sa4.sin_family == AF_INET || sa6.sa6.sin6_family == AF_INET6) { +#else + if (sa4.sa4.sin_family == AF_INET) { +#endif + uint8_t pref_buf[128]; + uint8_t *pb = pref_buf; + + /* IPv4 (4 bytes addr, 2 bytes port) */ + if (sa4.sa4.sin_family == AF_INET) { + memcpy(pb, &sa4.sa4.sin_addr.s_addr, 4); pb += 4; + memcpy(pb, &sa4.sa4.sin_port, 2); pb += 2; + } else { + memset(pb, 0, 6); pb += 6; + } + + /* IPv6 (16 bytes addr, 2 bytes port) */ +#if defined(LWS_WITH_IPV6) + if (sa6.sa6.sin6_family == AF_INET6) { + memcpy(pb, sa6.sa6.sin6_addr.s6_addr, 16); pb += 16; + memcpy(pb, &sa6.sa6.sin6_port, 2); pb += 2; + } else { + memset(pb, 0, 18); pb += 18; + } +#else + memset(pb, 0, 18); pb += 18; +#endif + + /* CID Length (1 byte) */ + *pb++ = 8; /* Generating an 8-byte CID */ + /* CID Bytes */ + lws_get_random(wsi->a.context, pb, 8); pb += 8; + /* Stateless Reset Token */ + lws_get_random(wsi->a.context, pb, 16); pb += 16; + + LWS_QUIC_WRITE_TP_BUF(0x0D, pref_buf, (size_t)(pb - pref_buf)); + lwsl_notice("QUIC TX: Sent preferred_address TP (0x0D)\n"); + } + } + + lws_tls_quic_set_transport_parameters(nwsi, local_tp_buf, (size_t)(tp - local_tp_buf)); + lws_free(local_tp_buf); goto tp_ok; tp_overflow: + lws_free(local_tp_buf); lwsl_wsi_err(wsi, "QUIC TX: tp buffer overflow"); lws_close_free_wsi(nwsi, LWS_CLOSE_STATUS_NOSTATUS, "tp overflow"); return LWS_HPI_RET_HANDLED; @@ -739,10 +993,25 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, continue; } if (p[0] & 0x80) { - uint8_t type = (uint8_t)((p[0] & 0x30) >> 4); - if (type == LWS_QUIC_PT_INITIAL) level = LWS_QUIC_LEVEL_INITIAL; - else if (type == LWS_QUIC_PT_HANDSHAKE) level = LWS_QUIC_LEVEL_HANDSHAKE; - else if (type == LWS_QUIC_PT_RETRY) { + uint8_t type = (uint8_t)((p[0] & 0x30) >> 4); + uint32_t parsed_pkt_version = (uint32_t)((p[1] << 24) | (p[2] << 16) | (p[3] << 8) | p[4]); + int is_v2 = (parsed_pkt_version == LWS_QUIC_VERSION_2) || + (nwsi && nwsi->quic.qn && nwsi->quic.qn->version == LWS_QUIC_VERSION_2); + + uint8_t v1_type = type; + if (is_v2) { + switch (type) { + case 0: v1_type = LWS_QUIC_PT_RETRY; break; + case 1: v1_type = LWS_QUIC_PT_INITIAL; break; + case 2: v1_type = LWS_QUIC_PT_0RTT; break; + case 3: v1_type = LWS_QUIC_PT_HANDSHAKE; break; + } + } + + if (v1_type == LWS_QUIC_PT_INITIAL) level = LWS_QUIC_LEVEL_INITIAL; + else if (v1_type == LWS_QUIC_PT_HANDSHAKE) level = LWS_QUIC_LEVEL_HANDSHAKE; + else if (v1_type == LWS_QUIC_PT_0RTT) level = LWS_QUIC_LEVEL_EARLY; + else if (v1_type == LWS_QUIC_PT_RETRY) { if (nwsi && nwsi->quic.qn && !nwsi->quic.qn->is_server) { size_t tag_pos = (size_t)n - 16; int client_scid_pos = 6 + dcid_len; @@ -788,15 +1057,20 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, nwsi->quic.qn->highest_rx_level = (uint8_t)level; } - /* Enforce 1200-byte padding for subsequent client-to-server Initial packets (RFC 9000 Section 14.1) */ - if (level == LWS_QUIC_LEVEL_INITIAL && nwsi && nwsi->quic.qn && nwsi->quic.qn->is_server && n < 1200) { - lwsl_wsi_notice(wsi, "QUIC RX: Dropping under-padded Initial packet (len %d)", n); - break; - } + int pn_space = (level == LWS_QUIC_LEVEL_EARLY || level == LWS_QUIC_LEVEL_APP) ? LWS_QUIC_LEVEL_APP : level; - /* 2. Parsing: Safely find the Packet Number offset */ - size_t payload_len_stated; - size_t pn_offset = lws_quic_get_pn_offset(p, (size_t)n, &payload_len_stated); + /* + * Enforce 1200-byte padding for subsequent client-to-server Initial packets (RFC 9000 Section 14.1) + * Note: RFC 9000 allows the client to stop padding Initial packets once it has received an + * acknowledgment from the server or sent/received a packet other than Initial/VN. Therefore, + * we MUST NOT discard subsequent unpadded Initial packets. The first Initial packet (which + * instantiates the connection) is already checked and validated for 1200-byte padding earlier. + */ + + size_t local_dcid_len = (nwsi && nwsi->quic.qn) ? nwsi->quic.qn->loc_cid.len : 8; + if (local_dcid_len == 0) local_dcid_len = 8; + size_t payload_len_stated = 0; + size_t pn_offset = lws_quic_get_pn_offset(p, (size_t)n, &payload_len_stated, local_dcid_len); if (!pn_offset) { lwsl_wsi_notice(wsi, "QUIC RX: Malformed or truncated packet"); break; @@ -810,7 +1084,7 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, struct lws_quic_keys *k = nwsi->quic.qn->keys[level]; - if (!k || !k->valid) { + if (!k || !k->el_hp_rx.len) { lwsl_wsi_notice(wsi, "QUIC RX: No valid keys for this packet level %d, skipping %zu bytes", level, packet_size); p += packet_size; n -= (int)packet_size; @@ -856,7 +1130,7 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, for (int i = 0; i < pn_len; i++) truncated_pn = (truncated_pn << 8) | p[pn_offset + (size_t)i]; - uint64_t largest_pn = nwsi->quic.qn ? nwsi->quic.qn->highest_rx_pn[level] : 0; + uint64_t largest_pn = nwsi->quic.qn ? nwsi->quic.qn->highest_rx_pn[pn_space] : 0; uint64_t full_pn = lws_quic_decode_packet_number(largest_pn, truncated_pn, pn_len * 8); lwsl_wsi_info(wsi, "QUIC RX: Packet level %d, decoded full_pn = %llu", level, (unsigned long long)full_pn); @@ -892,6 +1166,7 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, if (!nwsi->quic.qn->key_update_pending) { /* Peer initiated, so we echo by updating TX keys */ lws_quic_initiate_key_update(nwsi); + nwsi->quic.qn->key_update_pending = 0; /* Echoed */ } else { /* We initiated, this is the peer echoing back */ nwsi->quic.qn->key_update_pending = 0; @@ -927,24 +1202,24 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, /* Check for duplicate/replayed packet numbers (Security Fix) */ if (nwsi->quic.qn) { - uint64_t highest = nwsi->quic.qn->highest_rx_pn[level]; - if ((nwsi->quic.qn->rx_pn_bitmask[level] != 0 || highest != 0) && full_pn <= highest) { + uint64_t highest = nwsi->quic.qn->highest_rx_pn[pn_space]; + if ((nwsi->quic.qn->rx_pn_bitmask[pn_space] != 0 || highest != 0) && full_pn <= highest) { uint64_t diff = highest - full_pn; - if (diff >= 64 || (nwsi->quic.qn->rx_pn_bitmask[level] & (1ULL << diff))) { + if (diff >= 64 || (nwsi->quic.qn->rx_pn_bitmask[pn_space] & (1ULL << diff))) { lwsl_wsi_notice(wsi, "QUIC RX: Dropping duplicated or very old packet %llu", (unsigned long long)full_pn); goto next_packet; } - nwsi->quic.qn->rx_pn_bitmask[level] |= (1ULL << diff); + nwsi->quic.qn->rx_pn_bitmask[pn_space] |= (1ULL << diff); } else { - if (nwsi->quic.qn->rx_pn_bitmask[level] != 0 || highest != 0) { + if (nwsi->quic.qn->rx_pn_bitmask[pn_space] != 0 || highest != 0) { uint64_t diff = full_pn - highest; if (diff >= 64) - nwsi->quic.qn->rx_pn_bitmask[level] = 0; + nwsi->quic.qn->rx_pn_bitmask[pn_space] = 0; else - nwsi->quic.qn->rx_pn_bitmask[level] <<= diff; + nwsi->quic.qn->rx_pn_bitmask[pn_space] <<= diff; } - nwsi->quic.qn->rx_pn_bitmask[level] |= 1ULL; - nwsi->quic.qn->highest_rx_pn[level] = full_pn; + nwsi->quic.qn->rx_pn_bitmask[pn_space] |= 1ULL; + nwsi->quic.qn->highest_rx_pn[pn_space] = full_pn; } /* Connection Migration: Execute pending migration now that the packet is cryptographically verified */ @@ -1008,22 +1283,24 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, nwsi->quic.qn->bytes_received = (uint64_t)orig_n; nwsi->quic.qn->bytes_sent = 0; - /* Initiate Path Validation (Generate PATH_CHALLENGE) */ - struct lws_quic_tx_frame *f_pc = lws_zalloc(sizeof(*f_pc) + 8, "quic path_chall"); - if (f_pc) { - f_pc->type = LWS_QUIC_FT_PATH_CHALLENGE; - f_pc->len = 8; - f_pc->data = (uint8_t *)&f_pc[1]; - lws_get_random(wsi->a.context, f_pc->data, 8); - memcpy(nwsi->quic.qn->path_challenge, f_pc->data, 8); - nwsi->quic.qn->path_challenge_pending = 1; - - lws_dll2_add_head(&f_pc->list, &nwsi->quic.qn->pending_tx[LWS_QUIC_LEVEL_APP]); - lws_callback_on_writable(nwsi); + /* Initiate Path Validation (Generate PATH_CHALLENGE) if none pending */ + if (!nwsi->quic.qn->path_challenge_pending) { + struct lws_quic_tx_frame *f_pc = lws_zalloc(sizeof(*f_pc) + 8, "quic path_chall"); + if (f_pc) { + f_pc->type = LWS_QUIC_FT_PATH_CHALLENGE; + f_pc->len = 8; + f_pc->data = (uint8_t *)&f_pc[1]; + lws_get_random(wsi->a.context, f_pc->data, 8); + memcpy(nwsi->quic.qn->path_challenge, f_pc->data, 8); + nwsi->quic.qn->path_challenge_pending = 1; + + lws_dll2_add_head(&f_pc->list, &nwsi->quic.qn->pending_tx[LWS_QUIC_LEVEL_APP]); + lws_callback_on_writable(nwsi); + } } } - /* 5. Parse the plaintext frames */ + /* 5. Parse and handle all frames in the payload */ if (dec_len == 0) { lwsl_wsi_notice(wsi, "QUIC RX: Packet payload is empty (no frames)"); if (nwsi && nwsi != wsi) { @@ -1033,16 +1310,28 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, return LWS_HPI_RET_PLEASE_CLOSE_ME; } - int ack_eliciting = lws_quic_parse_frames(nwsi, level, &p[pn_offset + (size_t)pn_len], (size_t)dec_len); + int parse_res = lws_quic_parse_frames(nwsi, pn_space, &p[pn_offset + (size_t)pn_len], (size_t)dec_len, &sa46); /* ALPN negotiation might have migrated the network WSI! */ if (nwsi && !nwsi->quic.qn) { nwsi = lws_get_quic_network_wsi(nwsi); } - if (ack_eliciting < 0) { + if (nwsi) { + struct lws *w = nwsi->mux.child_list; + while (w) { + struct lws *next = w->mux.sibling_list; + if (w->quic.qs && w->quic.qs->close_after_rx) { + lwsl_wsi_notice(w, "QUIC RX Post-Processing: Closing stream WSI"); + lws_close_free_wsi(w, LWS_CLOSE_STATUS_NOSTATUS, "quic post rx stream close"); + } + w = next; + } + } + + if (parse_res < 0) { lwsl_wsi_notice(wsi, "QUIC RX: Frame parsing aborted"); - if (ack_eliciting == -3) { + if (parse_res == -3) { /* Peer closed the connection via CONNECTION_CLOSE. Drop silently without replying. */ lwsl_wsi_notice(nwsi ? nwsi : wsi, "QUIC RX: Peer closed connection. Dropping silently."); if (nwsi && nwsi != wsi) { @@ -1053,13 +1342,13 @@ rops_handle_POLLIN_quic(struct lws_context_per_thread *pt, struct lws *wsi, } /* We found an error and queued a CONNECTION_CLOSE frame */ if (nwsi) { - lws_quic_enter_closing_state(nwsi, ack_eliciting == -2 ? LWS_QUIC_ERR_PROTOCOL_VIOLATION : LWS_QUIC_ERR_FRAME_ENCODING_ERROR, 0, 0); + lws_quic_enter_closing_state(nwsi, parse_res == -2 ? LWS_QUIC_ERR_PROTOCOL_VIOLATION : LWS_QUIC_ERR_FRAME_ENCODING_ERROR, 0, 0); lws_callback_on_writable(nwsi); } goto next_packet; - } else if (ack_eliciting > 0) { + } else if (parse_res > 0) { if (nwsi && nwsi->quic.qn) { - nwsi->quic.qn->needs_ack[level] = 1; + nwsi->quic.qn->needs_ack[pn_space] = 1; lws_callback_on_writable(nwsi); /* Ensure POLLOUT fires so we send the ACK! */ } } @@ -1161,7 +1450,7 @@ lws_quic_enter_closing_state(struct lws *wsi, uint64_t err_code, uint64_t frame_ /* Determine highest available encryption level to send CONNECTION_CLOSE */ int start_level = qn->highest_rx_level; for (level = start_level; level >= LWS_QUIC_LEVEL_INITIAL; level--) { - if (qn->keys[level] && qn->keys[level]->valid) { + if (qn->keys[level] && qn->keys[level]->el_hp_tx.len) { target_level = level; break; } @@ -1219,57 +1508,75 @@ rops_handle_POLLOUT_quic(struct lws *wsi) struct lws_quic_netconn *qn = wsi->quic.qn; int level, n; int blocked = 0; + int eagain_blocked = 0; uint8_t pkt[2048]; memset(pkt, 0, sizeof(pkt)); - wsi->mux.requested_POLLOUT = 0; - // lwsl_notice("QUIC TX: POLLOUT called for %s, qn=%p, is_server=%d\n", lws_wsi_tag(wsi), qn, qn ? qn->is_server : -1); if (!qn) { struct lws *w; + lws_handling_result_t hr_ret = LWS_HP_RET_DROP_POLLOUT; if (wsi->mux.child_list) { w = wsi->mux.child_list; while (w) { struct lws *next = w->mux.sibling_list; if (w->mux.requested_POLLOUT) { w->mux.requested_POLLOUT = 0; - rops_handle_POLLOUT_quic(w); + lws_handling_result_t hr_child = rops_handle_POLLOUT_quic(w); + if (hr_child == LWS_HP_RET_BAIL_DIE) + return LWS_HP_RET_BAIL_DIE; + if (hr_child == LWS_HP_RET_BAIL_OK) + hr_ret = LWS_HP_RET_BAIL_OK; } w = next; } } - return LWS_HP_RET_DROP_POLLOUT; + return hr_ret; } + wsi->mux.requested_POLLOUT = 0; + lws_usec_t pto_base = qn->smoothed_rtt ? (qn->smoothed_rtt + (4 * qn->rttvar) + 25000) : LWS_QUIC_DEFAULT_PTO_US; lws_usec_t pto_delay = pto_base << qn->pto_count; if (pto_delay > 10000000) pto_delay = 10000000; - if (!wsi->quic.initialized && !qn->is_server) { - wsi->quic.initialized = 1; + if (!wsi->quic.initialized && !qn->is_server) { + wsi->quic.initialized = 1; + if (qn->migration_probing_wsi == wsi) { + /* Active Migration: just queue path challenge! */ + lws_quic_queue_path_challenge(wsi); + } else { #if defined(LWS_WITH_TLS) && defined(LWS_WITH_CLIENT) - if (wsi->tls.use_ssl & LCCSCF_USE_SSL) { - if (!wsi->tls.ssl) { - const char *cce = NULL; - if (lws_client_create_tls(wsi, &cce, 0) == CCTLS_RETURN_ERROR) { - lwsl_wsi_err(wsi, "Failed to create TLS BIO: %s", cce ? cce : "unknown"); - return LWS_HP_RET_BAIL_DIE; - } - } - - /* The BIO was already created, just init QUIC TLS */ - if (lws_tls_quic_init(wsi, quic_secret_cb)) { - lwsl_wsi_err(wsi, "Failed to init QUIC TLS"); - return LWS_HP_RET_BAIL_DIE; - } - /* Kick off the handshake */ - // lwsl_wsi_notice(wsi, "Kicking off QUIC TLS handshake"); - lws_tls_quic_rx_crypto(wsi, LWS_QUIC_LEVEL_INITIAL, NULL, 0); - } + if (wsi->tls.use_ssl & LCCSCF_USE_SSL) { + if (!wsi->tls.ssl) { + const char *cce = NULL; + if (lws_client_create_tls(wsi, &cce, 0) == CCTLS_RETURN_ERROR) { + lwsl_wsi_err(wsi, "Failed to create TLS BIO: %s", cce ? cce : "unknown"); + return LWS_HP_RET_BAIL_DIE; + } + } + /* The BIO was already created, just init QUIC TLS */ + if (lws_tls_quic_init(wsi, quic_secret_cb)) { + lwsl_wsi_err(wsi, "Failed to init QUIC TLS"); + return LWS_HP_RET_BAIL_DIE; + } + /* Kick off the handshake */ + // lwsl_wsi_notice(wsi, "Kicking off QUIC TLS handshake"); + lws_tls_quic_rx_crypto(wsi, LWS_QUIC_LEVEL_INITIAL, NULL, 0); + + { + struct lws *nwsi = lws_get_quic_network_wsi(wsi); + if (nwsi) { + wsi = nwsi; + qn = wsi->quic.qn; + } + } + } #endif - } + } + } if (qn->is_closing) { /* We are in the Closing State. Only process the CONNECTION_CLOSE frame. */ @@ -1322,16 +1629,27 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } } - /* Packet lost! Move it back to pending_tx */ + /* Packet lost! */ lws_dll2_remove(&f->list); - lws_dll2_add_head(&f->list, &qn->pending_tx[level]); total_bytes_lost += f->wire_len; - f->wire_len = 0; + if ((f->type & 0xfe) == LWS_QUIC_FT_DATAGRAM) { + lws_free(f); + } else { + lws_dll2_add_head(&f->list, &qn->pending_tx[level]); + f->wire_len = 0; + } } } lws_end_foreach_dll_safe(d, d1); } - if (total_bytes_lost && qn->cc_ops && qn->cc_ops->on_loss) - qn->cc_ops->on_loss(wsi, total_bytes_lost); + /* + * RFC 9002 Section 6.2.4: A PTO timer expiry does not indicate packet + * loss and MUST NOT cause the congestion window to be reduced (no on_loss). + * However, bytes_in_flight must be decremented for frames moved back to + * pending_tx, otherwise the CC state diverges from reality. Use on_discard + * which adjusts bytes_in_flight without touching cwnd or ssthresh. + */ + if (total_bytes_lost && qn->cc_ops && qn->cc_ops->on_discard) + qn->cc_ops->on_discard(wsi, total_bytes_lost); send_frames: /* @@ -1343,16 +1661,27 @@ rops_handle_POLLOUT_quic(struct lws *wsi) continue; } - if (!qn->keys[level]->valid) { + if (!qn->keys[level]->el_hp_tx.len) { continue; } - if (!qn->pending_tx[level].count && !qn->needs_ack[level]) { + /* Server MUST NOT send 0-RTT (EARLY) packets */ + if (level == LWS_QUIC_LEVEL_EARLY && qn->is_server) { continue; } - // lwsl_wsi_notice(wsi, "QUIC TX: Processing level %d. pending=%d, needs_ack=%d, pto_needed=%d", level, qn->pending_tx[level].count, qn->needs_ack[level], qn->pto_probe_needed); + int pn_space = (level == LWS_QUIC_LEVEL_EARLY || level == LWS_QUIC_LEVEL_APP) ? LWS_QUIC_LEVEL_APP : level; + int is_ack_allowed = (level != LWS_QUIC_LEVEL_EARLY); + if (!qn->pending_tx[level].count && !(qn->needs_ack[pn_space] && is_ack_allowed)) { + continue; + } + + lwsl_debug("QUIC TX: Processing level %d. pending=%d, needs_ack=%d, pto_needed=%d", level, qn->pending_tx[level].count, qn->needs_ack[pn_space], qn->pto_probe_needed); + + /* Determine if we're allowed to send */ + int is_congestion_limited = 0; + (void)is_congestion_limited; uint32_t mtu = qn->current_mtu ? qn->current_mtu : 1280; /* Enforce RFC 9000 Anti-Amplification Limit (Section 8.1) for servers */ @@ -1375,20 +1704,19 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } } - /* Check congestion window */ - if (!qn->pto_probe_needed && !qn->needs_ack[level] && qn->cc_ops && qn->cc_ops->can_send && !qn->cc_ops->can_send(wsi, mtu)) { -#if (_LWS_ENABLED_LOGS & LLL_INFO) - LWS_RATELIMIT_DEFINE_STATIC(rl); - lwsl_ratelimit_info(&rl, 1000000, "QUIC TX: Congestion window full, blocking POLLOUT\n"); -#endif + /* Check congestion window - bypass for PTO probes and ACKs */ + if (!qn->pto_probe_needed && !(qn->needs_ack[pn_space] && is_ack_allowed) && qn->cc_ops && qn->cc_ops->can_send && !qn->cc_ops->can_send(wsi, mtu)) { + is_congestion_limited = 1; + lwsl_notice("AGY-DEBUG: Congestion window full at level %d, blocking POLLOUT\n", level); blocked = 1; break; /* Stop processing sending loops */ } - /* Check pacing */ - if (!qn->needs_ack[level] && qn->cc_ops && qn->cc_ops->get_pacing_delay) { + /* Check pacing - bypass for PTO probes, exactly as we bypass CC */ + if (!qn->pto_probe_needed && !(qn->needs_ack[pn_space] && is_ack_allowed) && qn->cc_ops && qn->cc_ops->get_pacing_delay) { lws_usec_t delay = qn->cc_ops->get_pacing_delay(wsi, mtu); if (delay > 0) { + lwsl_notice("AGY-DEBUG: Pacing delay %llu us at level %d, scheduling timer\n", (unsigned long long)delay, level); lws_sul_schedule(wsi->a.context, 0, &qn->pacer_sul, lws_quic_pacer_cb, delay); blocked = 1; break; /* Stop processing sending loops */ @@ -1396,23 +1724,41 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } /* AEAD Confidentiality Limits Check (RFC 9001 Section 6.6) */ - if (level == LWS_QUIC_LEVEL_APP && qn->tx_packets_since_update > (1ULL << 20)) { + uint64_t update_limit = (1ULL << 20); + if (wsi->a.vhost && (wsi->a.vhost->options & LWS_SERVER_OPTION_QUIC_EARLY_KEY_UPDATE)) { + update_limit = 10; /* Trigger early for interop testing */ + } + if (level == LWS_QUIC_LEVEL_APP && qn->tx_packets_since_update > update_limit) { lws_quic_initiate_key_update(wsi); } /* We have frames to send at this encryption level! */ uint8_t *p = pkt; uint64_t my_pn = qn->keys[level]->pn_tx++; + lws_sockaddr46 packet_dest_sa46; + int has_packet_dest = 0; /* 1. Serialize Header */ size_t pn_offset = 0; size_t header_len = 0; - if (level == LWS_QUIC_LEVEL_INITIAL || level == LWS_QUIC_LEVEL_HANDSHAKE) { - if (level == LWS_QUIC_LEVEL_INITIAL) - *p++ = 0xc0 | 0x00 | 0x01; /* Long Header, Initial, 2-byte PN */ - else - *p++ = 0xc0 | 0x20 | 0x01; /* Long Header, Handshake, 2-byte PN */ + if (level == LWS_QUIC_LEVEL_INITIAL || level == LWS_QUIC_LEVEL_HANDSHAKE || level == LWS_QUIC_LEVEL_EARLY) { + if (level == LWS_QUIC_LEVEL_INITIAL) { + if (qn->version == LWS_QUIC_VERSION_2) + *p++ = 0xc0 | 0x10 | 0x01; /* v2 Initial */ + else + *p++ = 0xc0 | 0x00 | 0x01; /* v1 Initial */ + } else if (level == LWS_QUIC_LEVEL_EARLY) { + if (qn->version == LWS_QUIC_VERSION_2) + *p++ = 0xc0 | 0x20 | 0x01; /* v2 0-RTT */ + else + *p++ = 0xc0 | 0x10 | 0x01; /* v1 0-RTT */ + } else { + if (qn->version == LWS_QUIC_VERSION_2) + *p++ = 0xc0 | 0x30 | 0x01; /* v2 Handshake */ + else + *p++ = 0xc0 | 0x20 | 0x01; /* v1 Handshake */ + } /* Version */ *p++ = (uint8_t)(qn->version >> 24); @@ -1456,17 +1802,26 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } /* 1.5 Generate ACK frame if needed */ - if (qn->needs_ack[level]) { + int has_ack = 0; + int skip_ack_for_dest = 0; + if (qn->pending_tx[level].head) { + struct lws_quic_tx_frame *first_f = lws_container_of(qn->pending_tx[level].head, struct lws_quic_tx_frame, list); + if (first_f->has_dest) { + /* Skip ACK for now, this packet will be dedicated to first_f's destination */ + skip_ack_for_dest = 1; + } + } + if (qn->needs_ack[pn_space] && is_ack_allowed && !skip_ack_for_dest) { if (qn->ecn_rx_ect0 || qn->ecn_rx_ect1 || qn->ecn_rx_ce) { *p++ = 0x03; /* ACK_ECN */ } else { *p++ = LWS_QUIC_FT_ACK; /* ACK (0x02) */ } - p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), qn->highest_rx_pn[level]); /* Largest Acknowledged */ + p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), qn->highest_rx_pn[pn_space]); /* Largest Acknowledged */ p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), 0); /* ACK Delay (0 for now) */ p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), 0); /* ACK Range Count */ uint64_t first_ack_range = 0; - uint64_t bm = qn->rx_pn_bitmask[level] >> 1; + uint64_t bm = qn->rx_pn_bitmask[pn_space] >> 1; while (bm & 1) { first_ack_range++; bm >>= 1; @@ -1479,13 +1834,44 @@ rops_handle_POLLOUT_quic(struct lws *wsi) p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), qn->ecn_rx_ce); } - qn->needs_ack[level] = 0; + qn->needs_ack[pn_space] = 0; + has_ack = 1; } /* 2. Bundle frames from pending_tx until MTU is reached */ lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->pending_tx[level].head) { struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + if (has_packet_dest) { + if (!f->has_dest) { + d = d1; + continue; + } + /* Compare f->dest_sa46 with packet_dest_sa46 */ + int match = 0; + if (packet_dest_sa46.sa4.sin_family == f->dest_sa46.sa4.sin_family) { + if (packet_dest_sa46.sa4.sin_family == AF_INET) { + if (packet_dest_sa46.sa4.sin_addr.s_addr == f->dest_sa46.sa4.sin_addr.s_addr && + packet_dest_sa46.sa4.sin_port == f->dest_sa46.sa4.sin_port) + match = 1; + } +#if defined(LWS_WITH_IPV6) + else if (packet_dest_sa46.sa6.sin6_family == AF_INET6) { + if (!memcmp(&packet_dest_sa46.sa6.sin6_addr, &f->dest_sa46.sa6.sin6_addr, sizeof(struct in6_addr)) && + packet_dest_sa46.sa6.sin6_port == f->dest_sa46.sa6.sin6_port) + match = 1; + } +#endif + } + if (!match) { + d = d1; + continue; + } + } else if (f->has_dest) { + packet_dest_sa46 = f->dest_sa46; + has_packet_dest = 1; + } + /* Check if frame fits in remaining MTU (leaving room for headers and 16-byte AEAD tag) */ size_t frame_header_max_len = 1 + 8 + 8; size_t max_udp_payload = mtu > 48 ? mtu - 48 : 1200; @@ -1496,10 +1882,27 @@ rops_handle_POLLOUT_quic(struct lws *wsi) break; size_t send_len = f->len; + + if (f->type == LWS_QUIC_FT_PATH_CHALLENGE || f->type == LWS_QUIC_FT_PATH_RESPONSE) { + if (qn->migration_probing_wsi && wsi != qn->migration_probing_wsi) { + /* Only send path validation frames on the new probing socket */ + d = d1; + continue; + } + } else if (f->type != LWS_QUIC_FT_PADDING && f->type != LWS_QUIC_FT_NEW_CONNECTION_ID) { + if (qn->migration_probing_wsi && wsi == qn->migration_probing_wsi) { + /* Do not send non-probing frames on the new socket until validated */ + d = d1; + continue; + } + } + if ((size_t)(p - pkt) + frame_header_max_len + send_len + 32 > max_udp_payload) { if ((f->type & 0xf8) == LWS_QUIC_FT_STREAM || f->type == LWS_QUIC_FT_CRYPTO) { send_len = max_udp_payload - (size_t)(p - pkt) - frame_header_max_len - 32; } else { + lwsl_notice("QUIC TX: Frame doesn't fit! type=0x%02x, len=%zu, p-pkt=%zu, max_udp_payload=%zu", + f->type, send_len, (size_t)(p - pkt), max_udp_payload); break; /* Non-fragmentable frame doesn't fit */ } } @@ -1529,6 +1932,7 @@ rops_handle_POLLOUT_quic(struct lws *wsi) if (type & 0x02) /* LEN */ p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), send_len); } else if ((type & 0xfe) == LWS_QUIC_FT_DATAGRAM) { + lwsl_debug("QUIC TX: Serialized DATAGRAM frame! len=%zu", send_len); if (type & 0x01) /* LEN */ p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), send_len); } else if (type == LWS_QUIC_FT_MAX_DATA || type == LWS_QUIC_FT_DATA_BLOCKED) { @@ -1548,7 +1952,7 @@ rops_handle_POLLOUT_quic(struct lws *wsi) p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), f->offset); /* app_err_code */ } else if (type == LWS_QUIC_FT_MAX_STREAMS_BIDI || type == LWS_QUIC_FT_MAX_STREAMS_UNIDI || type == LWS_QUIC_FT_STREAMS_BLOCKED_BIDI || type == LWS_QUIC_FT_STREAMS_BLOCKED_UNIDI) { - p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), f->limit); + p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), f->limit); lwsl_err("QUIC TX: Serialized MAX_STREAMS! limit %llu\n", (unsigned long long)f->limit); } else if (type == LWS_QUIC_FT_NEW_CONNECTION_ID) { //lwsl_notice( "QUIC TX: Formatting MAX_STREAM_DATA for stream %llu", (unsigned long long)f->stream_id); p += lws_quic_write_varint(p, sizeof(pkt) - (size_t)(p - pkt), f->stream_id); /* seq */ @@ -1584,6 +1988,7 @@ rops_handle_POLLOUT_quic(struct lws *wsi) lws_dll2_add_tail(&f_sent->list, &qn->in_flight[level]); + /* Update original f in pending_tx */ f->offset += send_len; f->len -= send_len; @@ -1608,7 +2013,7 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } lws_end_foreach_dll_safe(d, d1); size_t payload_len = (size_t)(p - (pkt + header_len)); - if (payload_len == 0) + if (payload_len == 0 && !has_ack) continue; /* Ensure payload is at least 4 bytes for header protection sampling (RFC 9000 Section 5.4.2) */ @@ -1685,19 +2090,44 @@ rops_handle_POLLOUT_quic(struct lws *wsi) lwsl_wsi_debug(wsi, "QUIC TX: Dropping packet via lws_fi fault injection!"); n = (int)send_len; /* Pretend it succeeded */ } else { + const lws_sockaddr46 *dest_sa46 = NULL; + struct lws *nwsi_quic = lws_get_quic_network_wsi(wsi); + int is_client = nwsi_quic ? lwsi_role_client(nwsi_quic) : lwsi_role_client(wsi); + + if (!is_client) { + if (has_packet_dest) + dest_sa46 = &packet_dest_sa46; + else if (wsi->udp) + dest_sa46 = &wsi->udp->sa46; + else if (wsi->mux_substream && wsi->mux.parent_wsi && wsi->mux.parent_wsi->udp) + dest_sa46 = &wsi->mux.parent_wsi->udp->sa46; + } + + { + char adrbuf[64] = "none"; + if (dest_sa46) + lws_sa46_write_numeric_address((lws_sockaddr46 *)dest_sa46, adrbuf, sizeof(adrbuf)); + lwsl_debug("QUIC TX: sendto fd=%d, len=%zu, dest=%s, substream=%d, udp=%p", + fd, send_len, adrbuf, wsi->mux_substream, wsi->udp); + } #if defined(WIN32) || defined(_WIN32) - if (wsi->mux_substream && wsi->udp) + if (dest_sa46) n = sendto(fd, (const char *)pkt, (int)send_len, 0, - sa46_sockaddr(&wsi->udp->sa46), sa46_socklen(&wsi->udp->sa46)); + sa46_sockaddr(dest_sa46), sa46_socklen(dest_sa46)); else n = send(fd, (const char *)pkt, (int)send_len, 0); #else - if (wsi->mux_substream && wsi->udp) + if (dest_sa46) n = (int)sendto(fd, (const void *)pkt, send_len, 0, - sa46_sockaddr(&wsi->udp->sa46), sa46_socklen(&wsi->udp->sa46)); + sa46_sockaddr(dest_sa46), sa46_socklen(dest_sa46)); else n = (int)send(fd, (const void *)pkt, send_len, 0); #endif + if (n < 0) { + lwsl_warn("QUIC TX: sendto/send failed: returned %d, errno=%d\n", n, LWS_ERRNO); + } else { + lwsl_debug("QUIC TX: sent %d bytes successfully\n", n); + } } if (n < 0) { int e = LWS_ERRNO; @@ -1724,8 +2154,39 @@ rops_handle_POLLOUT_quic(struct lws *wsi) || e == ENOBUFS #endif ) { - lwsl_wsi_info(wsi, "QUIC TX: dropping packet (transient tx error), errno=%d", e); - n = (int)send_len; + lwsl_wsi_info(wsi, "QUIC TX: UDP socket EAGAIN/transient error, errno=%d. Pausing send.", e); + + /* + * The OS UDP socket buffer is full. We cannot send this packet. + * We MUST NOT pretend it was sent, because it would move to in_flight + * and wait for a PTO timer to retransmit, causing massive stalls. + * Instead, we keep the frames in pending_tx, and stop sending. + * The OS will wake us up with POLLOUT when the socket drains. + */ + struct lws_dll2 *d = qn->in_flight[level].tail; + while (d) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + if (f->sent_in_pn == my_pn) { + struct lws_dll2 *prev = d->prev; + lws_dll2_remove(d); + f->sent_in_pn = 0; + f->sent_time_us = 0; + f->wire_len = 0; + /* Push to head so it is sent first next time */ + lws_dll2_add_head(&f->list, &qn->pending_tx[level]); + d = prev; + } else { + break; + } + } + + /* Revert PN */ + qn->keys[level]->pn_tx--; + + /* Since we are stopping the send loop, we should keep POLLOUT enabled! */ + blocked = 1; + eagain_blocked = 1; + break; } else { lwsl_wsi_err(wsi, "QUIC TX: Write failed, errno=%d", e); return LWS_HP_RET_BAIL_OK; @@ -1751,8 +2212,12 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } lws_end_foreach_dll(d); } - if (ack_eliciting && qn->cc_ops && qn->cc_ops->on_sent) - qn->cc_ops->on_sent(wsi, send_len); + if (ack_eliciting) { + if (qn->pto_probe_needed > 0) + qn->pto_probe_needed--; + if (qn->cc_ops && qn->cc_ops->on_sent) + qn->cc_ops->on_sent(wsi, send_len); + } // lwsl_wsi_info(wsi, "QUIC TX: Sent %d bytes, bundled frames into PN %llu", // n, (unsigned long long)my_pn); @@ -1770,23 +2235,23 @@ rops_handle_POLLOUT_quic(struct lws *wsi) * has shrunk. We MUST wake up the child streams because they may have * been stalled by the application-layer pacing throttle in tx_credit! */ - if (level == LWS_QUIC_LEVEL_APP && send_len > 0) { - struct lws *curr = wsi->mux.child_list; - while (curr) { - // lwsl_wsi_notice(wsi, "QUIC TX: requesting POLLOUT for child %s", lws_wsi_tag(curr)); - lws_callback_on_writable(curr); - curr = curr->mux.sibling_list; + if (level == LWS_QUIC_LEVEL_APP) { + struct lws *w = wsi->mux.child_list; + + while (w) { + if (w->mux.requested_POLLOUT) + lws_callback_on_writable(w); + w = w->mux.sibling_list; } } } - qn->pto_probe_needed = 0; - /* If we handled all pending crypto/internal frames, give the user a chance to write */ struct lws *nwsi = lws_get_quic_network_wsi(wsi); - lwsl_info("QUIC TX POLLOUT: handshake_done=%d, tx_cr=%d, nwsi_tx_cr=%d\n", + lwsl_debug("QUIC TX POLLOUT: handshake_done=%d, tx_cr=%d, nwsi_tx_cr=%d\n", qn->handshake_done, (int)wsi->txc.tx_cr, (nwsi ? (int)nwsi->txc.tx_cr : 0)); - if (qn && qn->handshake_done) { + /* Process stream queues for Application (1-RTT) and Early (0-RTT) data */ + if (qn && (qn->handshake_done || qn->early_data_status == LWS_0RTT_STATUS_ATTEMPTED || qn->early_data_status == LWS_0RTT_STATUS_ACCEPTED)) { if (lws_wsi_txc_check_skint(&wsi->txc, (int32_t)wsi->txc.tx_cr)) goto end_children; if (nwsi && lws_wsi_txc_check_skint(&nwsi->txc, (int32_t)nwsi->txc.tx_cr)) @@ -1797,9 +2262,9 @@ rops_handle_POLLOUT_quic(struct lws *wsi) { struct lws *curr = wsi->mux.child_list; int sanity = 1000000; - lwsl_info("QUIC TX POLLOUT: nwsi=%s, tx_cr=%d\n", lws_wsi_tag(wsi), (int)wsi->txc.tx_cr); + lwsl_debug("QUIC TX POLLOUT: nwsi=%s, tx_cr=%d\n", lws_wsi_tag(wsi), (int)wsi->txc.tx_cr); while (curr && sanity--) { - lwsl_info("QUIC TX POLLOUT: child: %s, requested_POLLOUT=%d, tx_cr=%d\n", + lwsl_debug("QUIC TX POLLOUT: child: %s, requested_POLLOUT=%d, tx_cr=%d\n", lws_wsi_tag(curr), curr->mux.requested_POLLOUT, (int)curr->txc.tx_cr); curr = curr->mux.sibling_list; } @@ -1816,7 +2281,7 @@ rops_handle_POLLOUT_quic(struct lws *wsi) wa = &(*wsi2)->mux.sibling_list; - lwsl_info("QUIC TX POLLOUT: visiting child %s, requested_POLLOUT=%d\n", lws_wsi_tag(*wsi2), (*wsi2)->mux.requested_POLLOUT); + lwsl_debug("QUIC TX POLLOUT: visiting child %s, requested_POLLOUT=%d\n", lws_wsi_tag(*wsi2), (*wsi2)->mux.requested_POLLOUT); if (!(*wsi2)->mux.requested_POLLOUT) goto next_child; @@ -1832,6 +2297,7 @@ rops_handle_POLLOUT_quic(struct lws *wsi) int is_peer_initiated = (w->mux.my_sid & 1) != (qn->is_server ? 1 : 0); int is_unidiri = (w->mux.my_sid & 2); if (is_peer_initiated && is_unidiri) { + w->mux.requested_POLLOUT = 0; goto next_child; } @@ -1840,27 +2306,42 @@ rops_handle_POLLOUT_quic(struct lws *wsi) usable_credit = lws_rops_func_fidx(w->role_ops, LWS_ROPS_tx_credit). tx_credit(w, LWSTXCR_US_TO_PEER, 0); } - if (lws_wsi_txc_check_skint(&w->txc, usable_credit)) { - if (!w->quic.tx_blocked_sent) { - struct lws_quic_tx_frame *f_sdb = lws_zalloc(sizeof(*f_sdb), "quic sdb"); - if (f_sdb) { - f_sdb->type = LWS_QUIC_FT_STREAM_DATA_BLOCKED; - f_sdb->stream_id = w->mux.my_sid; - f_sdb->limit = w->quic.qs ? w->quic.qs->tx_offset : 0; - lws_dll2_add_head(&f_sdb->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); - } - w->quic.tx_blocked_sent = 1; - lws_callback_on_writable(wsi); // request POLLOUT for nwsi to send the frame - } - goto next_child; - } + + /* Check for actual flow control exhaustion */ + if (lws_wsi_txc_check_skint(&w->txc, w->txc.tx_cr)) { + if (!w->quic.tx_blocked_sent) { + struct lws_quic_tx_frame *f_sdb = lws_zalloc(sizeof(*f_sdb), "quic sdb"); + if (f_sdb) { + f_sdb->type = LWS_QUIC_FT_STREAM_DATA_BLOCKED; + f_sdb->stream_id = w->mux.my_sid; + f_sdb->limit = w->quic.qs ? w->quic.qs->tx_offset : 0; + lws_dll2_add_head(&f_sdb->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); + } + w->quic.tx_blocked_sent = 1; + lws_callback_on_writable(wsi); /* request POLLOUT for nwsi to send the frame */ + } + if (lwsi_state(w) != LRS_FLUSHING_BEFORE_CLOSE) { + w->mux.requested_POLLOUT = 0; + goto next_child; + } + } + + /* Check for local throttling (e.g., pending_tx queue full) */ + if (usable_credit <= 0 && lwsi_state(w) != LRS_FLUSHING_BEFORE_CLOSE) { + w->mux.requested_POLLOUT = 1; + break; + } w->mux.requested_POLLOUT = 0; - lwsl_info("QUIC TX POLLOUT: calling perform_user_POLLOUT/lws_callback_as_writeable for child %s\n", lws_wsi_tag(w)); + lwsl_debug("QUIC TX POLLOUT: calling perform_user_POLLOUT for child %s, usable_credit=%d, pending_tx=%d\n", + lws_wsi_tag(w), (int)usable_credit, qn->pending_tx[LWS_QUIC_LEVEL_APP].count); + + lwsl_debug("QUIC TX POLLOUT: calling perform_user_POLLOUT/lws_callback_as_writeable for child %s\n", lws_wsi_tag(w)); if (lws_rops_fidx(w->role_ops, LWS_ROPS_perform_user_POLLOUT)) { if (lws_rops_func_fidx(w->role_ops, LWS_ROPS_perform_user_POLLOUT). perform_user_POLLOUT(w) == -1) { + lwsl_wsi_info(w, "QUIC TX: child perform_user_POLLOUT requested close"); int _found = 0; lws_start_foreach_ll(struct lws *, _w1, wsi->mux.child_list) { @@ -1888,28 +2369,107 @@ rops_handle_POLLOUT_quic(struct lws *wsi) } end_children: - lwsl_info("QUIC TX POLLOUT: calling lws_wsi_mux_action_pending_writeable_reqs\n"); - - int can_process_children = (qn->handshake_done && wsi->txc.tx_cr > 0 && (!nwsi || nwsi->txc.tx_cr > 0)); + lwsl_debug("QUIC TX POLLOUT: calling lws_wsi_mux_action_pending_writeable_reqs\n"); + /* Give children output credits based on fair sharing */ + int can_process_children = ((qn->handshake_done || qn->early_data_status == LWS_0RTT_STATUS_ATTEMPTED || qn->early_data_status == LWS_0RTT_STATUS_ACCEPTED) && wsi->txc.tx_cr > 0 && (!nwsi || nwsi->txc.tx_cr > 0)); int have_pending_tx = 0; for (level = 0; level < LWS_QUIC_LEVEL_COUNT; level++) { - if (qn->pending_tx[level].count) { - have_pending_tx = 1; - break; + if (qn->pending_tx[level].count && qn->keys[level] && qn->keys[level]->el_hp_tx.len) { + if (level == LWS_QUIC_LEVEL_APP && qn->migration_probing_wsi) { + lws_start_foreach_dll(struct lws_dll2 *, d, qn->pending_tx[level].head) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + int is_path_frame = (f->type == LWS_QUIC_FT_PATH_CHALLENGE || f->type == LWS_QUIC_FT_PATH_RESPONSE); + if (wsi == qn->nwsi && is_path_frame) continue; + if (wsi == qn->migration_probing_wsi && !is_path_frame && f->type != LWS_QUIC_FT_PADDING && f->type != LWS_QUIC_FT_NEW_CONNECTION_ID) continue; + have_pending_tx = 1; + break; + } lws_end_foreach_dll(d); + } else { + have_pending_tx = 1; + } + if (have_pending_tx) + break; + } + } + + if (!blocked && can_process_children) { + if (lws_wsi_mux_action_pending_writeable_reqs(wsi)) + return LWS_HP_RET_BAIL_DIE; + + for (level = 0; level < LWS_QUIC_LEVEL_COUNT; level++) { + if (qn->pending_tx[level].count && qn->keys[level] && qn->keys[level]->el_hp_tx.len) { + if (level == LWS_QUIC_LEVEL_APP && qn->migration_probing_wsi) { + lws_start_foreach_dll(struct lws_dll2 *, d, qn->pending_tx[level].head) { + struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + int is_path_frame = (f->type == LWS_QUIC_FT_PATH_CHALLENGE || f->type == LWS_QUIC_FT_PATH_RESPONSE); + if (wsi == qn->nwsi && is_path_frame) continue; + if (wsi == qn->migration_probing_wsi && !is_path_frame && f->type != LWS_QUIC_FT_PADDING && f->type != LWS_QUIC_FT_NEW_CONNECTION_ID) continue; + have_pending_tx = 1; + break; + } lws_end_foreach_dll(d); + } else { + have_pending_tx = 1; + } + if (have_pending_tx) + break; + } } } // lwsl_wsi_notice(wsi, "QUIC TX: blocked=%d, have_pending_tx=%d, can_process_children=%d", blocked, have_pending_tx, can_process_children); - if (blocked || (!have_pending_tx && !can_process_children)) { - // lwsl_wsi_notice(wsi, "QUIC TX: dropping POLLOUT manually (LWS_POLLOUT, 0)"); - /* We are blocked by QUIC limits, or have nothing to send and children can't write. - * Stop asking the OS for POLLOUT. We will re-enable it when POLLIN brings ACKs. */ + int children_need_POLLOUT = 0; + { + struct lws *w_child = wsi->mux.child_list; + while (w_child) { + if (w_child->mux.requested_POLLOUT) { + children_need_POLLOUT = 1; + break; + } + w_child = w_child->mux.sibling_list; + } + } + + if (((blocked && !eagain_blocked) || !have_pending_tx) && !children_need_POLLOUT) { + lwsl_debug("QUIC TX: dropping POLLOUT manually for %s (blocked=%d, eagain_blocked=%d, have_pending_tx=%d)\n", lws_wsi_tag(wsi), blocked, eagain_blocked, have_pending_tx); + /* We are blocked by QUIC limits, or have nothing to send right now. + * Stop asking the OS for POLLOUT. We will re-enable it if children + * need to write. */ if (lws_change_pollfd(wsi, LWS_POLLOUT, 0)) return LWS_HP_RET_BAIL_DIE; - } else { - // lwsl_wsi_notice(wsi, "QUIC TX: calling lws_wsi_mux_action_pending_writeable_reqs (wsi->mux.requested_POLLOUT=%d)", wsi->mux.requested_POLLOUT); - if (lws_wsi_mux_action_pending_writeable_reqs(wsi)) - return LWS_HP_RET_BAIL_DIE; + } + + { + struct lws *w = wsi->mux.child_list; + while (w) { + struct lws *next = w->mux.sibling_list; + if (w->quic.qs && w->quic.qs->close_after_rx) { + lwsl_wsi_notice(w, "QUIC TX Post-Processing: Closing stream WSI"); + lws_close_free_wsi(w, LWS_CLOSE_STATUS_NOSTATUS, "quic post tx stream close"); + } + w = next; + } + } + + if (have_pending_tx) + return LWS_HP_RET_BAIL_OK; + + /* + * Check if any child still needs writeable service. + * lws_wsi_mux_action_pending_writeable_reqs() may have + * re-enabled POLLOUT on the fd, but the caller in service.c + * will drop it again if we return LWS_HP_RET_DROP_POLLOUT. + * This happens when children transition to LRS_ISSUING_FILE + * and call lws_callback_on_writable() — they need another + * POLLOUT cycle to call lws_serve_http_file_fragment(). + */ + { + struct lws *w = wsi->mux.child_list; + + while (w) { + if (w->mux.requested_POLLOUT) + return LWS_HP_RET_BAIL_OK; + w = w->mux.sibling_list; + } } } @@ -1935,71 +2495,68 @@ rops_write_role_protocol_quic(struct lws *wsi, unsigned char *buf, size_t len, /* Enforce stream and connection flow control limits */ if (len > 0) { - lwsl_info("QUIC TX WRITE: wsi->txc.tx_cr=%d, nwsi->txc.tx_cr=%d\n", (int)wsi->txc.tx_cr, nwsi ? (int)nwsi->txc.tx_cr : -1); - if (wsi->txc.tx_cr < (int)len || wsi->txc.tx_cr <= 0 || - (nwsi && (nwsi->txc.tx_cr < (int)len || nwsi->txc.tx_cr <= 0))) { - int did_enqueue = 0; - if ((wsi->txc.tx_cr < (int)len || wsi->txc.tx_cr <= 0) && !wsi->quic.tx_blocked_sent) { - /* Generate STREAM_DATA_BLOCKED */ - struct lws_quic_tx_frame *f_sdb = lws_zalloc(sizeof(*f_sdb), "quic sdb"); - if (f_sdb) { - f_sdb->type = LWS_QUIC_FT_STREAM_DATA_BLOCKED; - f_sdb->stream_id = wsi->mux.my_sid; - f_sdb->limit = wsi->quic.qs ? wsi->quic.qs->tx_offset : 0; - lws_dll2_add_head(&f_sdb->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); - } - wsi->quic.tx_blocked_sent = 1; - did_enqueue = 1; + lwsl_debug("QUIC TX WRITE: wsi->txc.tx_cr=%d, nwsi->txc.tx_cr=%d\n", (int)wsi->txc.tx_cr, nwsi ? (int)nwsi->txc.tx_cr : -1); + + int did_enqueue = 0; + if (wsi->txc.tx_cr <= 0 && !wsi->quic.tx_blocked_sent) { + /* Generate STREAM_DATA_BLOCKED */ + struct lws_quic_tx_frame *f_sdb = lws_zalloc(sizeof(*f_sdb), "quic sdb"); + if (f_sdb) { + f_sdb->type = LWS_QUIC_FT_STREAM_DATA_BLOCKED; + f_sdb->stream_id = wsi->mux.my_sid; + f_sdb->limit = wsi->quic.qs ? wsi->quic.qs->tx_offset : 0; + lws_dll2_add_head(&f_sdb->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); } + wsi->quic.tx_blocked_sent = 1; + did_enqueue = 1; + } - if (nwsi && nwsi != wsi && (nwsi->txc.tx_cr < (int)len || nwsi->txc.tx_cr <= 0) && !nwsi->quic.tx_blocked_sent) { - /* Generate DATA_BLOCKED */ - struct lws_quic_tx_frame *f_db = lws_zalloc(sizeof(*f_db), "quic db"); - if (f_db) { - f_db->type = LWS_QUIC_FT_DATA_BLOCKED; - f_db->limit = qn->tx_conn_offset; - lws_dll2_add_head(&f_db->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); - } - nwsi->quic.tx_blocked_sent = 1; - did_enqueue = 1; + if (nwsi && nwsi != wsi && nwsi->txc.tx_cr <= 0 && !nwsi->quic.tx_blocked_sent) { + /* Generate DATA_BLOCKED */ + struct lws_quic_tx_frame *f_db = lws_zalloc(sizeof(*f_db), "quic db"); + if (f_db) { + f_db->type = LWS_QUIC_FT_DATA_BLOCKED; + f_db->limit = qn->tx_conn_offset; + lws_dll2_add_head(&f_db->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); } - - /* Kick output to send these control frames */ - if (did_enqueue) - lws_callback_on_writable(nwsi ? nwsi : wsi); - - return 0; /* Consumed 0 bytes, caller should yield and try again later */ + nwsi->quic.tx_blocked_sent = 1; + did_enqueue = 1; } + + /* Kick output to send these control frames */ + if (did_enqueue) + lws_callback_on_writable(nwsi ? nwsi : wsi); } - lwsl_info("QUIC TX WRITE: Stream %llu. Requested: %d, Stream tx_cr: %d, Conn tx_cr: %d\n", wsi->quic.qs ? (unsigned long long)wsi->quic.qs->stream_id : 0, (int)len, (int)wsi->txc.tx_cr, nwsi ? (int)nwsi->txc.tx_cr : -1); + lwsl_debug("QUIC TX WRITE: Stream %llu. Requested: %d, Stream tx_cr: %d, Conn tx_cr: %d\n", wsi->quic.qs ? (unsigned long long)wsi->quic.qs->stream_id : 0, (int)len, (int)wsi->txc.tx_cr, nwsi ? (int)nwsi->txc.tx_cr : -1); - // lwsl_notice("%s: allocating frame of size %d\n", __func__, (int)(sizeof(*f) + len)); - /* Allocate frame struct + payload buffer natively */ f = lws_zalloc(sizeof(*f) + len, "quic tx frame"); if (!f) return -1; - f->type = LWS_QUIC_FT_STREAM | 0x02 | 0x04; /* STREAM | OFF | LEN */ - if ((*wp) & LWS_WRITE_H2_STREAM_END) { - f->type |= 0x01; /* FIN */ - /* wsi->quic.qs->sent_fin = 1; could do if we had sent_fin flag */ - } f->data = (uint8_t *)&f[1]; f->len = len; - - // lwsl_notice("%s: copying data from buf=%p to f->data=%p\n", __func__, buf, f->data); - /* Copy the user payload */ memcpy(f->data, buf, len); - // lwsl_notice("%s: copied data\n", __func__); if (((*wp) & 0x1f) == LWS_WRITE_QUIC_DATAGRAM) { /* It's a DATAGRAM frame */ f->type = LWS_QUIC_FT_DATAGRAM + 1; /* with LEN */ f->stream_id = 0; /* Datagrams aren't attached to a stream ID */ f->offset = 0; - lwsl_info("QUIC TX WRITE: Datagram len=%u\n", (unsigned int)f->len); + lwsl_debug("QUIC TX WRITE: Datagram len=%u\n", (unsigned int)f->len); } else { + f->type = LWS_QUIC_FT_STREAM | 0x02 | 0x04; /* STREAM | OFF | LEN */ + if (((*wp) & LWS_WRITE_H2_STREAM_END) || + ((*wp) & 0x1f) == LWS_WRITE_HTTP_FINAL) { + f->type |= 0x01; /* FIN */ + if (wsi->quic.qs) { + wsi->quic.qs->sent_fin = 1; + if (wsi->quic.qs->is_unidirectional || wsi->quic.qs->fin_received) { + wsi->quic.qs->close_after_rx = 1; + } + } + } + if (!wsi->quic.qs) { lwsl_wsi_err(wsi, "QUIC: Cannot send stream data without a quic stream structure!"); lws_free(f); @@ -2009,15 +2566,13 @@ rops_write_role_protocol_quic(struct lws *wsi, unsigned char *buf, size_t len, f->offset = wsi->quic.qs->tx_offset; wsi->quic.qs->tx_offset += len; - lwsl_info("QUIC TX WRITE: stream_id=%llu, offset=%llu, len=%u, fin=%d\n", + lwsl_debug("QUIC TX WRITE: stream_id=%llu, offset=%llu, len=%u, fin=%d\n", (unsigned long long)f->stream_id, (unsigned long long)f->offset, (unsigned int)f->len, (f->type & 0x01)); - /* Deduct credit */ + /* Deduct credit here so current_max calculations work correctly */ wsi->txc.tx_cr -= (int)len; - if (nwsi && nwsi != wsi) { - nwsi->txc.tx_cr -= (int)len; - } if (nwsi) { + nwsi->txc.tx_cr -= (int)len; qn->tx_conn_offset += len; } } @@ -2034,8 +2589,8 @@ rops_write_role_protocol_quic(struct lws *wsi, unsigned char *buf, size_t len, tx_level = LWS_QUIC_LEVEL_EARLY; } - lwsl_info("QUIC TX: Enqueued STREAM frame for sid %llu, len %d, FIN=%d (level %d)\n", - (unsigned long long)f->stream_id, (int)f->len, (f->type & 0x01), tx_level); + lwsl_debug("QUIC TX: Enqueued frame type=0x%02x, len=%d, tx_level=%d, pending_count=%d", + f->type, (int)f->len, tx_level, qn->pending_tx[tx_level].count + 1); lws_dll2_add_tail(&f->list, &qn->pending_tx[tx_level]); /* Wake up the event loop to send the packet */ @@ -2067,17 +2622,32 @@ rops_client_bind_quic(struct lws *wsi, const struct lws_client_connect_info *i) memset(wsi->udp, 0, sizeof(*wsi->udp)); } - /* Allocate QUIC netconn for client! */ - if (!wsi->quic.qn) { - wsi->quic.qn = lws_zalloc(sizeof(*wsi->quic.qn), "quic_netconn"); + /* Handle Make-Before-Break Active Migration */ + if (wsi->quic.migrate_from_wsi && wsi->quic.migrate_from_wsi->quic.qn) { + wsi->quic.qn = wsi->quic.migrate_from_wsi->quic.qn; + wsi->quic.qn->migration_probing_wsi = wsi; + lwsl_notice("QUIC: Created migration probing wsi %p for qn %p\n", wsi, wsi->quic.qn); + + lws_quic_queue_path_challenge(wsi); + + /* Skip all initialization and key derivation, just return */ + return 1; + } + + /* Allocate QUIC netconn for client! */ + if (!wsi->quic.qn) { + wsi->quic.qn = lws_zalloc(sizeof(*wsi->quic.qn), "quic_netconn"); if (!wsi->quic.qn) return 1; } wsi->quic.qn->nwsi = wsi; wsi->quic.qn->is_server = 0; + wsi->quic.qn->next_stream_id_bidi_local = 0; + wsi->quic.qn->next_stream_id_unidi_local = 2; wsi->quic.qn->version = LWS_QUIC_VERSION_1; - wsi->quic.qn->max_streams_bidi_local = 1024; + wsi->quic.qn->original_version = wsi->quic.qn->version; + wsi->quic.qn->max_streams_bidi_local = 1000; wsi->quic.qn->max_streams_unidi_local = 1024; wsi->quic.qn->current_mtu = 1280; @@ -2119,8 +2689,13 @@ rops_client_bind_quic(struct lws *wsi, const struct lws_client_connect_info *i) } { - uint8_t *tp = wsi->quic.qn->local_tp_buf; - uint8_t *tp_end = tp + sizeof(wsi->quic.qn->local_tp_buf); + uint8_t *local_tp_buf = lws_malloc(4096, "quic tp scratch"); + if (!local_tp_buf) { + lwsl_wsi_err(wsi, "OOM allocating tp scratch buffer"); + return 1; + } + uint8_t *tp = local_tp_buf; + uint8_t *tp_end = tp + 4096; #define LWS_QUIC_WRITE_TP_VARINT(_id, _val) \ do { \ @@ -2142,7 +2717,7 @@ rops_client_bind_quic(struct lws *wsi, const struct lws_client_connect_info *i) tp += (_len); \ } while (0) - lwsl_notice("QUIC CLIENT: Writing TP buffer! buf size %d", (int)(tp - wsi->quic.qn->local_tp_buf)); LWS_QUIC_WRITE_TP_VARINT(0x04, 1048576); + lwsl_notice("QUIC CLIENT: Writing TP buffer! buf size %d", (int)(tp - local_tp_buf)); LWS_QUIC_WRITE_TP_VARINT(0x04, 1048576); LWS_QUIC_WRITE_TP_VARINT(0x05, 1048576); LWS_QUIC_WRITE_TP_VARINT(0x06, 1048576); LWS_QUIC_WRITE_TP_VARINT(0x07, 1048576); @@ -2152,11 +2727,28 @@ rops_client_bind_quic(struct lws *wsi, const struct lws_client_connect_info *i) LWS_QUIC_WRITE_TP_VARINT(0x01, 30000); LWS_QUIC_WRITE_TP_BUF(0x0F, wsi->quic.qn->loc_cid.id, wsi->quic.qn->loc_cid.len); + if (wsi->a.context->options & LWS_SERVER_OPTION_QUIC_LATEST_VERSION) { + uint8_t vi_buf[12]; + vi_buf[0] = (uint8_t)(LWS_QUIC_VERSION_1 >> 24); + vi_buf[1] = (uint8_t)(LWS_QUIC_VERSION_1 >> 16); + vi_buf[2] = (uint8_t)(LWS_QUIC_VERSION_1 >> 8); + vi_buf[3] = (uint8_t)(LWS_QUIC_VERSION_1); + vi_buf[4] = (uint8_t)(LWS_QUIC_VERSION_1 >> 24); + vi_buf[5] = (uint8_t)(LWS_QUIC_VERSION_1 >> 16); + vi_buf[6] = (uint8_t)(LWS_QUIC_VERSION_1 >> 8); + vi_buf[7] = (uint8_t)(LWS_QUIC_VERSION_1); + vi_buf[8] = (uint8_t)(LWS_QUIC_VERSION_2 >> 24); + vi_buf[9] = (uint8_t)(LWS_QUIC_VERSION_2 >> 16); + vi_buf[10] = (uint8_t)(LWS_QUIC_VERSION_2 >> 8); + vi_buf[11] = (uint8_t)(LWS_QUIC_VERSION_2); + LWS_QUIC_WRITE_TP_BUF(0x11, vi_buf, 12); + } - lws_tls_quic_set_transport_parameters(wsi, wsi->quic.qn->local_tp_buf, (size_t)(tp - wsi->quic.qn->local_tp_buf)); - + lws_tls_quic_set_transport_parameters(wsi, local_tp_buf, (size_t)(tp - local_tp_buf)); + lws_free(local_tp_buf); goto tp_ok2; tp_overflow2: + lws_free(local_tp_buf); lwsl_wsi_err(wsi, "QUIC TX: tp buffer overflow"); return 1; tp_ok2: @@ -2248,33 +2840,26 @@ static int rops_callback_on_writable_quic(struct lws *wsi) { struct lws *nwsi = lws_get_quic_network_wsi(wsi); - int already; if (!nwsi) return 0; - if (wsi->mux.requested_POLLOUT) { - // lwsl_info("rops_callback_on_writable_quic: %s already pending writable\n", lws_wsi_tag(wsi)); - } else { - lwsl_info("rops_callback_on_writable_quic: marking %s as pending writable (nwsi=%s)\n", lws_wsi_tag(wsi), lws_wsi_tag(nwsi)); - } - - already = lws_wsi_mux_mark_parents_needing_writeable(wsi); - - /* for network action, act only on the network wsi */ - if (already && nwsi && nwsi != wsi) - return lws_callback_on_writable(nwsi); + if (!wsi->mux.requested_POLLOUT) + lwsl_debug("rops_callback_on_writable_quic: marking %s as pending writable (nwsi=%s)\n", + lws_wsi_tag(wsi), lws_wsi_tag(nwsi)); - /* If we are the network wsi but we have a listener parent (shared UDP port), propagate to it */ - if (already && wsi->mux.parent_wsi) - return lws_callback_on_writable(wsi->mux.parent_wsi); + /* Mark the entire parent chain as needing POLLOUT */ + lws_wsi_mux_mark_parents_needing_writeable(wsi); - return 0; /* not handled, let core handle it */ + return 0; /* not handled, let core enable socket POLLOUT */ } void lws_quic_stream_cleanup(struct lws *wsi) { - struct lws *nwsi = lws_get_network_wsi(wsi); + struct lws *nwsi = wsi; + while (nwsi && !nwsi->quic.qn) { + nwsi = nwsi->mux.parent_wsi; + } struct lws_quic_netconn *qn = nwsi ? nwsi->quic.qn : NULL; int i; @@ -2294,7 +2879,7 @@ lws_quic_stream_cleanup(struct lws *wsi) if (qn) { uint64_t sid = wsi->quic.qs->stream_id; - int is_abort = (!wsi->quic.qs->fin_received || !wsi->quic.qs->fin_delivered); + int is_abort = (!wsi->quic.qs->fin_received || !wsi->quic.qs->sent_fin); /* If this is a unidirectional stream, adjust abort logic */ int is_unidirectional = (sid & 2) != 0; @@ -2302,7 +2887,7 @@ lws_quic_stream_cleanup(struct lws *wsi) int we_are_sender = (qn->is_server == is_remote_initiated) ? 1 : 0; if (is_unidirectional) { we_are_sender = is_remote_initiated ? 0 : 1; - is_abort = we_are_sender ? !wsi->quic.qs->fin_delivered : !wsi->quic.qs->fin_received; + is_abort = we_are_sender ? !wsi->quic.qs->sent_fin : !wsi->quic.qs->fin_received; } /* If we're closing the stream before FINs were exchanged, notify the peer */ @@ -2329,22 +2914,61 @@ lws_quic_stream_cleanup(struct lws *wsi) if (nwsi) lws_callback_on_writable(nwsi); } - /* If this was a remote-initiated stream, increase our limit and notify peer */ + /* If this was a remote-initiated stream, increase our limit and notify peer. + * + * Coalesce: scan the pending_tx queue for an existing MAX_STREAMS frame + * of the same type and update its limit in place, rather than allocating + * a new frame per closed stream. With many concurrent streams (e.g. the + * multiplexing QIR test with 1999 streams), the O(N) frame flood would + * fill the TX queue and trigger PTO/congestion-window collapse. + */ if (is_remote_initiated) { - struct lws_quic_tx_frame *f_max = lws_zalloc(sizeof(*f_max), "quic max strm"); - if (f_max) { - if (!is_unidirectional) { - qn->max_streams_bidi_local++; - f_max->type = LWS_QUIC_FT_MAX_STREAMS_BIDI; - f_max->limit = qn->max_streams_bidi_local; - } else { - qn->max_streams_unidi_local++; - f_max->type = LWS_QUIC_FT_MAX_STREAMS_UNIDI; - f_max->limit = qn->max_streams_unidi_local; + uint8_t want_type = is_unidirectional ? + LWS_QUIC_FT_MAX_STREAMS_UNIDI : LWS_QUIC_FT_MAX_STREAMS_BIDI; + struct lws_quic_tx_frame *f_exist = NULL; + + /* Increment our local limit first */ + if (!is_unidirectional) + qn->max_streams_bidi_local++; + else + qn->max_streams_unidi_local++; + + /* Look for an already-queued MAX_STREAMS frame to coalesce into */ + lws_start_foreach_dll(struct lws_dll2 *, d, + lws_dll2_get_head(&qn->pending_tx[LWS_QUIC_LEVEL_APP])) { + struct lws_quic_tx_frame *f = lws_container_of(d, + struct lws_quic_tx_frame, list); + if (f->type == want_type) { + f_exist = f; + break; + } + } lws_end_foreach_dll(d); + + if (f_exist) { + /* Update limit in the existing frame — no new allocation */ + f_exist->limit = is_unidirectional ? + qn->max_streams_unidi_local : + qn->max_streams_bidi_local; + lwsl_debug("QUIC TX: Coalesced MAX_STREAMS_%s limit → %llu\n", + is_unidirectional ? "UNIDI" : "BIDI", + (unsigned long long)f_exist->limit); + } else { + struct lws_quic_tx_frame *f_max = + lws_zalloc(sizeof(*f_max), "quic max strm"); + if (f_max) { + f_max->stream_id = ~0ULL; + f_max->type = want_type; + f_max->limit = is_unidirectional ? + qn->max_streams_unidi_local : + qn->max_streams_bidi_local; + lwsl_debug("QUIC TX: Queued MAX_STREAMS_%s with limit %llu\n", + is_unidirectional ? "UNIDI" : "BIDI", + (unsigned long long)f_max->limit); + lws_dll2_add_tail(&f_max->list, + &qn->pending_tx[LWS_QUIC_LEVEL_APP]); } - lws_dll2_add_tail(&f_max->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); - if (nwsi) lws_callback_on_writable(nwsi); } + if (nwsi) lws_callback_on_writable(nwsi); } if (is_abort) { @@ -2352,7 +2976,10 @@ lws_quic_stream_cleanup(struct lws *wsi) /* Purge pending_tx */ lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->pending_tx[i].head) { struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); - if (f->stream_id == sid && f->type != LWS_QUIC_FT_RESET_STREAM && f->type != LWS_QUIC_FT_STOP_SENDING) { + int is_stream_frame = ((f->type & 0xf8) == LWS_QUIC_FT_STREAM) || + (f->type == LWS_QUIC_FT_MAX_STREAM_DATA) || + (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED); + if (is_stream_frame && f->stream_id == sid) { lws_dll2_remove(&f->list); lws_free(f); } @@ -2361,7 +2988,12 @@ lws_quic_stream_cleanup(struct lws *wsi) /* Purge in_flight */ lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, qn->in_flight[i].head) { struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); - if (f->stream_id == sid) { + int is_stream_frame = ((f->type & 0xf8) == LWS_QUIC_FT_STREAM) || + (f->type == LWS_QUIC_FT_MAX_STREAM_DATA) || + (f->type == LWS_QUIC_FT_STREAM_DATA_BLOCKED); + if (is_stream_frame && f->stream_id == sid) { + if (qn->cc_ops && qn->cc_ops->on_discard) + qn->cc_ops->on_discard(nwsi, f->wire_len); lws_dll2_remove(&f->list); lws_free(f); } @@ -2387,7 +3019,7 @@ rops_close_kill_connection_quic(struct lws *wsi, enum lws_close_status reason) if (wsi->mux.parent_wsi) { struct lws *nwsi = wsi->mux.parent_wsi; lws_wsi_mux_sibling_disconnect(wsi); - if (nwsi->mux.child_count == 0) + if (nwsi->mux.child_count == 0 && nwsi->quic.qn) lws_set_timeout(nwsi, PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE, nwsi->a.vhost->keepalive_timeout ? nwsi->a.vhost->keepalive_timeout : 5); @@ -2454,6 +3086,13 @@ rops_close_kill_connection_quic(struct lws *wsi, enum lws_close_status reason) if (qn->cc_state) lws_free_set_NULL(qn->cc_state); + for (int _i = 0; _i < 4; _i++) { + if (qn->crypto_rx_buf[_i]) { + lws_free(qn->crypto_rx_buf[_i]); + qn->crypto_rx_buf[_i] = NULL; + } + } + lws_free_set_NULL(wsi->quic.qn); } @@ -2490,7 +3129,9 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) if (wsi->quic.qs) { wsi->quic.qs->rx_max_data += (uint64_t)(add > 0 ? add : 0); - uint64_t ungranted = wsi->quic.qs->advertised_rx_max_data - wsi->quic.qs->highest_rx_offset; + uint64_t ungranted = 0; + if (wsi->quic.qs->advertised_rx_max_data > wsi->quic.qs->highest_rx_offset) + ungranted = wsi->quic.qs->advertised_rx_max_data - wsi->quic.qs->highest_rx_offset; if (nwsi->a.context->quic_tx_credit_cb) { uint64_t new_win = nwsi->a.context->quic_tx_credit_cb( @@ -2499,7 +3140,9 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) if (new_win > wsi->quic.qs->rx_window_size && new_win <= LWS_QUIC_MAX_WINDOW) { wsi->quic.qs->rx_max_data += (new_win - wsi->quic.qs->rx_window_size); wsi->quic.qs->rx_window_size = new_win; - ungranted = wsi->quic.qs->advertised_rx_max_data - wsi->quic.qs->highest_rx_offset; + ungranted = 0; + if (wsi->quic.qs->advertised_rx_max_data > wsi->quic.qs->highest_rx_offset) + ungranted = wsi->quic.qs->advertised_rx_max_data - wsi->quic.qs->highest_rx_offset; } } @@ -2513,11 +3156,14 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) wsi->quic.qs->advertised_rx_max_data = wsi->quic.qs->rx_max_data; lws_dll2_add_tail(&f_msd->list, &qn->pending_tx[LWS_QUIC_LEVEL_APP]); } + lws_callback_on_writable(nwsi); } } qn->rx_max_data += (uint64_t)(add > 0 ? add : 0); - uint64_t ungranted = qn->advertised_rx_max_data - qn->highest_rx_offset; + uint64_t ungranted = 0; + if (qn->advertised_rx_max_data > qn->highest_rx_offset) + ungranted = qn->advertised_rx_max_data - qn->highest_rx_offset; if (nwsi->a.context->quic_tx_credit_cb) { uint64_t new_win = nwsi->a.context->quic_tx_credit_cb( @@ -2526,7 +3172,9 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) if (new_win > qn->rx_window_size && new_win <= LWS_QUIC_MAX_WINDOW) { qn->rx_max_data += (new_win - qn->rx_window_size); qn->rx_window_size = new_win; - ungranted = qn->advertised_rx_max_data - qn->highest_rx_offset; + ungranted = 0; + if (qn->advertised_rx_max_data > qn->highest_rx_offset) + ungranted = qn->advertised_rx_max_data - qn->highest_rx_offset; } } @@ -2534,6 +3182,7 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) qn->last_rx_update_us = now; struct lws_quic_tx_frame *f_md = lws_zalloc(sizeof(*f_md), "quic md"); if (f_md) { + lwsl_notice("QUIC: Generating MAX_DATA limit %llu", (unsigned long long)qn->rx_max_data); f_md->type = LWS_QUIC_FT_MAX_DATA; f_md->limit = qn->rx_max_data; qn->advertised_rx_max_data = qn->rx_max_data; @@ -2548,11 +3197,8 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) /* We're being told we can write an additional "add" bytes to the peer */ wsi->txc.tx_cr += add; - wsi->quic.tx_blocked_sent = 0; - if (nwsi != wsi) { - nwsi->txc.tx_cr += add; - nwsi->quic.tx_blocked_sent = 0; - } + if (add > 0) + wsi->quic.tx_blocked_sent = 0; /* Unblock if blocked */ if (wsi->txc.tx_cr > 0) { @@ -2577,23 +3223,57 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) if (nwsi->txc.tx_cr < cr) cr = nwsi->txc.tx_cr; - /* - * Throttle application-layer writes if the QUIC pending_tx list is growing too large. - * On WAN links, the CC limits the physical sending rate. If we don't throttle - * the application, it will buffer the entire file into RAM, consuming MBs of memory. + /* + * Throttle application-layer writes to prevent the pending_tx + * buffer from growing beyond what the congestion window can + * deliver promptly. The original flat 64KB cap caused a + * permanent stall: at startup cwnd=10×1280=12800 bytes, so + * 64KB is 5× cwnd, creating a bytes_in_flight debt that takes + * tens of seconds of ACKs to drain below the growing cwnd. + * + * Cap at max(128KB, 4×cwnd): + * - 128KB floor handles multiplexing (many small streams each + * needing HEADERS frames before the first ACK returns). + * - 4×cwnd keeps large-file transfers bounded to a manageable + * debt: at cwnd=12800 the cap is 51200 bytes (4 windows), + * which drains in <1 RTT once ACKs arrive. + * - As cwnd grows via slow-start the cap grows with it, + * maintaining full throughput. */ + size_t queued_bytes = 0; if (nwsi->quic.qn) { - size_t queued_bytes = 0; - lws_start_foreach_dll(struct lws_dll2 *, d, lws_dll2_get_head(&nwsi->quic.qn->pending_tx[LWS_QUIC_LEVEL_APP])) { - struct lws_quic_tx_frame *f = lws_container_of(d, struct lws_quic_tx_frame, list); + uint32_t mtu = nwsi->quic.qn->current_mtu ? + nwsi->quic.qn->current_mtu : 1280; + + lws_start_foreach_dll(struct lws_dll2 *, d, + lws_dll2_get_head(&nwsi->quic.qn->pending_tx[LWS_QUIC_LEVEL_APP])) { + struct lws_quic_tx_frame *f = lws_container_of(d, + struct lws_quic_tx_frame, list); queued_bytes += f->len; } lws_end_foreach_dll(d); - /* Cap at 64KB of buffered TX frames to pace application */ - if (queued_bytes >= 65536) - cr = 0; - else if (cr > (int)(65536 - queued_bytes)) - cr = (int)(65536 - queued_bytes); + /* + * Throttle based on available cwnd headroom: cap new writes + * to cwnd - bytes_in_flight - queued_bytes. bytes_in_flight + * only counts serialized frames; queued_bytes (pending_tx) is + * already committed but not yet on the wire. Together they + * represent our total debt against the cwnd. + * + * Floor: always allow at least 1 MTU so the stream + * makes progress even when the window is exactly full. + */ + if (nwsi->quic.qn->cc_state) { + struct lws_quic_cc_newreno *cc = + (struct lws_quic_cc_newreno *)nwsi->quic.qn->cc_state; + size_t debt = cc->bytes_in_flight + queued_bytes; + size_t headroom = (cc->cwnd > debt) ? cc->cwnd - debt : 0; + lwsl_notice("AGY-DEBUG: rops_tx_credit_quic: cwnd=%zu, bytes_in_flight=%zu, queued_bytes=%zu, headroom=%zu, cr_before=%d\n", + cc->cwnd, cc->bytes_in_flight, queued_bytes, headroom, cr); + if (headroom < mtu) + headroom = mtu; /* always allow 1 packet */ + if (cr > (int)headroom) + cr = (int)headroom; + } } /* @@ -2602,12 +3282,24 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) * H3 frames it (adding overhead), resulting in a write request of `cr + overhead` bytes, * which exceeds the flow control window and blocks, causing issues on non-seekable streams. */ - if (cr > 9) - cr -= 9; - else + if (cr >= 3) { + if (cr > 9) + cr -= 9; + else + cr -= 2; + } else { cr = 0; + } - lwsl_info("rops_tx_credit_quic: LWSTXCR_US_TO_PEER returning %d (wsi->txc.tx_cr=%d, nwsi->txc.tx_cr=%d)\n", + if (!cr && !add) + lwsl_debug("rops_tx_credit_quic: US_TO_PEER returning 0: " + "wsi_tx_cr=%d, nwsi_tx_cr=%d, queued=%zu, " + "wsi=%s, nwsi=%s\n", + (int)wsi->txc.tx_cr, (int)nwsi->txc.tx_cr, + queued_bytes, lws_wsi_tag(wsi), + lws_wsi_tag(nwsi)); + + lwsl_debug("rops_tx_credit_quic: LWSTXCR_US_TO_PEER returning %d (wsi->txc.tx_cr=%d, nwsi->txc.tx_cr=%d)\n", cr, (int)wsi->txc.tx_cr, (int)nwsi->txc.tx_cr); return cr; /* how much we can write to peer */ } @@ -2616,7 +3308,7 @@ rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add) if (n > nwsi->txc.peer_tx_cr_est) n = nwsi->txc.peer_tx_cr_est; - lwsl_info("rops_tx_credit_quic: returning %d\n", n); + lwsl_debug("rops_tx_credit_quic: returning %d\n", n); return n; } @@ -2670,6 +3362,19 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) return 1; } wsi->desc.sockfd = LWS_SOCK_INVALID; +#if defined(LWS_WITH_EVENT_LIBS) + if (wsi->a.context->event_loop_ops->evlib_size_wsi) { + memcpy(nwsi->evlib_wsi, wsi->evlib_wsi, wsi->a.context->event_loop_ops->evlib_size_wsi); + memset(wsi->evlib_wsi, 0, wsi->a.context->event_loop_ops->evlib_size_wsi); + + if (!strcmp(wsi->a.context->event_loop_ops->name, "libuv")) { + void **ppwatcher = (void **)nwsi->evlib_wsi; + if (ppwatcher && *ppwatcher) { + *(void **)(*ppwatcher) = nwsi; + } + } + } +#endif if (__insert_wsi_socket_into_fds(wsi->a.context, nwsi)) { lws_pt_unlock(pt); lws_close_free_wsi(nwsi, LWS_CLOSE_STATUS_NOSTATUS, "fd table fail"); @@ -2690,22 +3395,37 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) memset(&wsi->quic, 0, sizeof(wsi->quic)); memset(&wsi->tls, 0, sizeof(wsi->tls)); lws_tls_quic_migrate_wsi(wsi, nwsi); - wsi->quic.qs = lws_zalloc(sizeof(*wsi->quic.qs), "quic stream"); - if (wsi->quic.qs) { - wsi->quic.qs->rx_max_data = LWS_QUIC_DEFAULT_WINDOW; - wsi->quic.qs->advertised_rx_max_data = LWS_QUIC_DEFAULT_WINDOW; - wsi->quic.qs->rx_window_size = LWS_QUIC_DEFAULT_WINDOW; - wsi->quic.qs->last_rx_update_us = lws_now_usecs(); - } else { - lws_close_free_wsi(nwsi, LWS_CLOSE_STATUS_NOSTATUS, "quic stream oom"); - return 1; + if (!wsi->quic.qs) { + wsi->quic.qs = lws_zalloc(sizeof(*wsi->quic.qs), "quic stream"); + if (wsi->quic.qs) { + wsi->quic.qs->rx_max_data = LWS_QUIC_DEFAULT_WINDOW; + wsi->quic.qs->advertised_rx_max_data = LWS_QUIC_DEFAULT_WINDOW; + wsi->quic.qs->rx_window_size = LWS_QUIC_DEFAULT_WINDOW; + wsi->quic.qs->last_rx_update_us = lws_now_usecs(); + } else { + lws_close_free_wsi(nwsi, LWS_CLOSE_STATUS_NOSTATUS, "quic stream oom"); + return 1; + } } /* Initialize flow control credits for the new child stream */ int32_t init_cr = nwsi->txc.manual_initial_tx_credit; if (!init_cr) { - if (nwsi->quic.qn && nwsi->quic.qn->peer_initial_max_stream_data_bidi_remote) - init_cr = (int32_t)nwsi->quic.qn->peer_initial_max_stream_data_bidi_remote; + int is_bidi = (wsi->quic.qs && wsi->quic.qs->is_unidirectional == 0); + int is_remote_initiated = (wsi->quic.qs && wsi->quic.qs->is_server_initiated == nwsi->quic.qn->is_server ? 0 : 1); + uint64_t val = 0; + if (nwsi->quic.qn) { + if (is_bidi) { + if (is_remote_initiated) + val = nwsi->quic.qn->peer_initial_max_stream_data_bidi_local; + else + val = nwsi->quic.qn->peer_initial_max_stream_data_bidi_remote; + } else { + val = nwsi->quic.qn->peer_initial_max_stream_data_uni; + } + } + if (val) + init_cr = (val > (uint64_t)INT32_MAX) ? INT32_MAX : (int32_t)val; else init_cr = 65535; } @@ -2752,6 +3472,8 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) wsi->client_h2_alpn = 1; wsi->client_mux_migrated = 1; wsi->hdr_parsing_completed = 0; + nwsi->client_h2_alpn = 1; + nwsi->client_mux_migrated = 1; } #endif @@ -2780,7 +3502,7 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) if (wsi->a.context->h3_cap_cache && wsi->stash && wsi->stash->cis[CIS_HOST]) { lws_h3_cap_info_t cap; cap.state = LWS_H3_STATE_KNOWN_GOOD; - cap.latency_us = (uint32_t)(lws_now_usecs() - wsi->quic.quic_race_start_us); + cap.latency_us = (uint32_t)(lws_now_usecs() - nwsi->quic.quic_race_start_us); lws_cache_write_through(wsi->a.context->h3_cap_cache, wsi->stash->cis[CIS_HOST], (const uint8_t *)&cap, sizeof(cap), @@ -2803,7 +3525,24 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) if (wsi->mux.parent_wsi) lws_wsi_mux_sibling_disconnect(wsi); + struct lws *old_children = wsi->mux.child_list; + unsigned int old_child_count = wsi->mux.child_count; + + wsi->mux.child_list = NULL; + wsi->mux.child_count = 0; + + nwsi->mux.child_list = old_children; + nwsi->mux.child_count = old_child_count; + + struct lws *c = old_children; + while (c) { + c->mux.parent_wsi = nwsi; + c = c->mux.sibling_list; + } + lws_wsi_mux_insert(wsi, nwsi, 0); /* client first request is stream ID 0 */ + if (nwsi->quic.qn->next_stream_id_bidi_local == 0) + nwsi->quic.qn->next_stream_id_bidi_local = 4; lws_set_timeout(nwsi, NO_PENDING_TIMEOUT, 0); if (listener) { @@ -2815,7 +3554,11 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) } /* Inform the H3 role that it negotiated ALPN */ - lws_role_call_alpn_negotiated(wsi, alpn); + if (role && role != &role_ops_quic && + lws_rops_fidx(role, LWS_ROPS_alpn_negotiated)) { + (lws_rops_func_fidx(role, LWS_ROPS_alpn_negotiated)). + alpn_negotiated(wsi, alpn); + } /* We are ready to send headers! */ lws_callback_on_writable(wsi); diff --git a/lib/roles/quic/parse-quic.c b/lib/roles/quic/parse-quic.c index 9e7c9ff0f0..2e5c4d9570 100644 --- a/lib/roles/quic/parse-quic.c +++ b/lib/roles/quic/parse-quic.c @@ -72,26 +72,34 @@ lws_quic_parse_varint(const uint8_t *buf, size_t len, uint64_t *val) * Returns the offset in bytes, or 0 if the packet is malformed/truncated. */ size_t -lws_quic_get_pn_offset(const uint8_t *buf, size_t len, size_t *payload_len) +lws_quic_get_pn_offset(const uint8_t *buf, size_t len, size_t *payload_len, size_t dcid_len) { size_t pos = 1; uint64_t token_len, p_len; size_t consumed; - uint8_t type, dcid_len, scid_len; + uint8_t type, scid_len; if (len < 6) return 0; if (!(buf[0] & 0x80)) { /* Short Header: pn_offset immediately follows the 1-byte header + DCID */ - /* Assuming an 8-byte DCID for LWS endpoints */ - *payload_len = len - (1 + 8); - return 1 + 8; + if (len < 1 + dcid_len) return 0; + *payload_len = len - (1 + dcid_len); + return 1 + dcid_len; } /* Long Header (Form = 1) */ type = (buf[0] & 0x30) >> 4; + uint32_t version = (uint32_t)((buf[1] << 24) | (buf[2] << 16) | (buf[3] << 8) | buf[4]); + if (version == LWS_QUIC_VERSION_2) { + if (type == 1) type = 0; + else if (type == 0) type = 3; + else if (type == 2) type = 1; + else if (type == 3) type = 2; + } + /* Skip Version (4 bytes) */ pos += 4; @@ -177,8 +185,22 @@ void lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_stream *qs, uint64_t offset, uint8_t *buf, size_t len, int is_crypto, int level) { - uint64_t *expected_offset = is_crypto ? &nwsi->quic.qn->rx_crypto_offset[level] : &qs->rx_offset; - lws_dll2_owner_t *owner = is_crypto ? &nwsi->quic.qn->rx_crypto_chunks[level] : &qs->rx_chunks; + uint64_t *expected_offset; + lws_dll2_owner_t *owner; + + if (is_crypto) { + if (nwsi && !nwsi->quic.qn) + nwsi = lws_get_quic_network_wsi(nwsi); + if (!nwsi || !nwsi->quic.qn) + return; + expected_offset = &nwsi->quic.qn->rx_crypto_offset[level]; + owner = &nwsi->quic.qn->rx_crypto_chunks[level]; + } else { + if (!qs) + return; + expected_offset = &qs->rx_offset; + owner = &qs->rx_chunks; + } /* 1. If it's a past or overlapping frame, ignore it (simple version) */ if (offset + len <= *expected_offset && !(len == 0 && offset == *expected_offset)) @@ -200,12 +222,12 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_PROTOCOL_VIOLATION, 0, 0); return; } - if (nwsi && !nwsi->quic.qn) { + if (!nwsi->quic.qn) { nwsi = lws_get_quic_network_wsi(nwsi); - if (nwsi && nwsi->quic.qn) { - expected_offset = &nwsi->quic.qn->rx_crypto_offset[level]; - owner = &nwsi->quic.qn->rx_crypto_chunks[level]; - } + if (!nwsi || !nwsi->quic.qn) + return; + expected_offset = &nwsi->quic.qn->rx_crypto_offset[level]; + owner = &nwsi->quic.qn->rx_crypto_chunks[level]; } } else if (wsi_child) { int is_final = (wsi_child && qs && qs->fin_received && *expected_offset + len == qs->rx_final_size && !qs->fin_delivered); @@ -224,8 +246,11 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ #endif if (wsi_child && wsi_child->a.protocol && wsi_child->a.protocol->callback) { /* Application Stream Data */ - enum lws_callback_reasons reason = lwsi_role_client(wsi_child) ? - LWS_CALLBACK_QT_CLIENT_RECEIVE : LWS_CALLBACK_QT_SERVER_RECEIVE; + enum lws_callback_reasons reason = + wsi_child->role_ops->rx_cb[lwsi_role_server(wsi_child)] ? + (enum lws_callback_reasons)wsi_child->role_ops->rx_cb[lwsi_role_server(wsi_child)] : + (lwsi_role_client(wsi_child) ? + LWS_CALLBACK_QT_CLIENT_RECEIVE : LWS_CALLBACK_QT_SERVER_RECEIVE); int n = wsi_child->a.protocol->callback(wsi_child, reason, wsi_child->user_space, buf, len); if (n == 0) { /* Data consumed by application, replenish rx credit to generate MAX_DATA! */ @@ -262,6 +287,19 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ } } #endif + if (wsi_child && wsi_child->role_ops && (!strcmp(wsi_child->role_ops->name, "wt") || !strcmp(wsi_child->role_ops->name, "quic"))) { + if (qs->is_unidirectional) { + lwsl_wsi_notice(wsi_child, "QUIC/WT unidirectional stream received FIN. Flagging close_after_rx"); + qs->close_after_rx = 1; + } else { + if (qs->sent_fin) { + lwsl_wsi_notice(wsi_child, "QUIC/WT bidi stream received FIN (sent_fin=1). Flagging close_after_rx"); + qs->close_after_rx = 1; + } else { + lwsl_wsi_notice(wsi_child, "QUIC/WT bidi stream received FIN but sent_fin=0. Keeping open."); + } + } + } } } @@ -285,12 +323,12 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_PROTOCOL_VIOLATION, 0, 0); return; } - if (nwsi && !nwsi->quic.qn) { + if (!nwsi->quic.qn) { nwsi = lws_get_quic_network_wsi(nwsi); - if (nwsi && nwsi->quic.qn) { - expected_offset = &nwsi->quic.qn->rx_crypto_offset[level]; - owner = &nwsi->quic.qn->rx_crypto_chunks[level]; - } + if (!nwsi || !nwsi->quic.qn) + return; + expected_offset = &nwsi->quic.qn->rx_crypto_offset[level]; + owner = &nwsi->quic.qn->rx_crypto_chunks[level]; } } else if (wsi_child) { #if defined(LWS_ROLE_H3) @@ -302,8 +340,11 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ } else #endif if (wsi_child->a.protocol && wsi_child->a.protocol->callback) { - enum lws_callback_reasons reason = lwsi_role_client(wsi_child) ? - LWS_CALLBACK_QT_CLIENT_RECEIVE : LWS_CALLBACK_QT_SERVER_RECEIVE; + enum lws_callback_reasons reason = + wsi_child->role_ops->rx_cb[lwsi_role_server(wsi_child)] ? + (enum lws_callback_reasons)wsi_child->role_ops->rx_cb[lwsi_role_server(wsi_child)] : + (lwsi_role_client(wsi_child) ? + LWS_CALLBACK_QT_CLIENT_RECEIVE : LWS_CALLBACK_QT_SERVER_RECEIVE); int n = wsi_child->a.protocol->callback(wsi_child, reason, wsi_child->user_space, c->data, c->len); if (n == 0) { /* Data consumed by application, replenish rx credit to generate MAX_DATA! */ @@ -336,12 +377,28 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ } } #endif + if (wsi_child && wsi_child->role_ops && (!strcmp(wsi_child->role_ops->name, "wt") || !strcmp(wsi_child->role_ops->name, "quic"))) { + if (qs->is_unidirectional) { + lwsl_wsi_notice(wsi_child, "QUIC/WT unidirectional stream received FIN (flushed). Flagging close_after_rx"); + qs->close_after_rx = 1; + } else { + if (qs->sent_fin) { + lwsl_wsi_notice(wsi_child, "QUIC/WT bidi stream received FIN (flushed, sent_fin=1). Flagging close_after_rx"); + qs->close_after_rx = 1; + } else { + lwsl_wsi_notice(wsi_child, "QUIC/WT bidi stream received FIN (flushed) but sent_fin=0. Keeping open."); + } + } + } } } if (!is_crypto && !wsi_child) { - lws_dll2_remove(&c->list); - lws_free(c); + /* + * The WSI was closed and freed. Its cleanup routine + * already freed all buffered chunks, including c. + * We must not touch c, qs or the list anymore. + */ return; } @@ -412,14 +469,43 @@ lws_quic_rx_reassemble(struct lws *nwsi, struct lws *wsi_child, struct lws_quic_ struct lws * lws_quic_stream_find(struct lws *nwsi, uint64_t stream_id) { - struct lws *wsi_child = nwsi->mux.child_list; - while (wsi_child) { - if (wsi_child->quic.qs && wsi_child->quic.qs->stream_id == stream_id) - return wsi_child; - if (wsi_child->mux.my_sid == stream_id) /* Fallback */ - return wsi_child; - wsi_child = wsi_child->mux.sibling_list; + struct lws_quic_netconn *qn = nwsi ? nwsi->quic.qn : NULL; + struct lws *wsi_child; + + if (!qn) { + wsi_child = nwsi ? nwsi->mux.child_list : NULL; + while (wsi_child) { + if (wsi_child->quic.qs && wsi_child->quic.qs->stream_id == stream_id) + return wsi_child; + if ((uint64_t)wsi_child->mux.my_sid == stream_id) + return wsi_child; + wsi_child = wsi_child->mux.sibling_list; + } + return NULL; + } + + if (qn->nwsi) { + wsi_child = qn->nwsi->mux.child_list; + while (wsi_child) { + if (wsi_child->quic.qs && wsi_child->quic.qs->stream_id == stream_id) + return wsi_child; + if ((uint64_t)wsi_child->mux.my_sid == stream_id) + return wsi_child; + wsi_child = wsi_child->mux.sibling_list; + } } + + if (qn->migration_probing_wsi) { + wsi_child = qn->migration_probing_wsi->mux.child_list; + while (wsi_child) { + if (wsi_child->quic.qs && wsi_child->quic.qs->stream_id == stream_id) + return wsi_child; + if ((uint64_t)wsi_child->mux.my_sid == stream_id) + return wsi_child; + wsi_child = wsi_child->mux.sibling_list; + } + } + return NULL; } @@ -428,7 +514,7 @@ lws_quic_stream_find(struct lws *nwsi, uint64_t stream_id) * Parses QUIC frames from a decrypted payload and routes them. */ int -lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payload_len) +lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payload_len, const lws_sockaddr46 *sa46) { size_t pos = 0; uint64_t type, offset, len; @@ -528,6 +614,11 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl pn -= (first_ack_range + 1); /* 5. Additional ACK Ranges */ + if (ack_range_count > 1024 || ack_range_count > (payload_len - pos) / 2) { + lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_FRAME_ENCODING_ERROR, type, 0); + return -1; + } + for (uint64_t r = 0; r < ack_range_count; r++) { uint64_t gap, ack_range; @@ -565,6 +656,7 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl pos += consumed; } } + lws_quic_detect_loss(nwsi, level, largest_ack); break; } @@ -591,11 +683,21 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl } if (!is_peer_initiated) { - if (is_unidirectional || !wsi_child) { + if (is_unidirectional) { lwsl_wsi_notice(nwsi, "QUIC RX: Invalid RESET_STREAM on stream ID %llu", (unsigned long long)stream_id); lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_STREAM_STATE_ERROR, type, 0); return -1; } + if (!wsi_child) { + /* It could be a stream we already closed and freed, or one we never created. + * Check if we created it by comparing with next_stream_id. */ + uint64_t next_id = is_unidirectional ? qn->next_stream_id_unidi_local : qn->next_stream_id_bidi_local; + if (stream_id >= next_id) { + lwsl_wsi_notice(nwsi, "QUIC RX: RESET_STREAM on uncreated stream ID %llu", (unsigned long long)stream_id); + lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_STREAM_STATE_ERROR, type, 0); + return -1; + } + } } pos += consumed; consumed = lws_quic_parse_varint(&payload[pos], payload_len - pos, &app_err_code); @@ -632,7 +734,7 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl struct lws *child = nwsi->mux.child_list; while (child) { - if (child->mux.my_sid == stream_id) { + if ((uint64_t)child->mux.my_sid == stream_id) { lwsl_wsi_notice(child, "QUIC RX: Stream closed by peer via RESET_STREAM"); #if defined(LWS_ROLE_H3) if (child->role_ops == &role_ops_h3 && child->quic.qs && child->quic.qs->is_unidirectional) { @@ -707,9 +809,15 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl if (!is_peer_initiated) { if (!wsi_child) { - lwsl_wsi_notice(nwsi, "QUIC RX: STOP_SENDING on non-existing stream ID %llu", (unsigned long long)stream_id); - lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_STREAM_STATE_ERROR, type, 0); - return -1; + /* It could be a stream we already closed and freed, or one we never created. + * Check if we created it by comparing with next_stream_id. */ + uint64_t next_id = is_unidirectional ? qn->next_stream_id_unidi_local : qn->next_stream_id_bidi_local; + if (stream_id >= next_id) { + lwsl_wsi_notice(nwsi, "QUIC RX: STOP_SENDING on uncreated stream ID %llu", (unsigned long long)stream_id); + lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_STREAM_STATE_ERROR, type, 0); + return -1; + } + // Ignore STOP_SENDING for closed streams } } else { if (is_unidirectional) { @@ -725,7 +833,7 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl lwsl_wsi_info(nwsi, "QUIC RX: Parsed STOP_SENDING! stream_id %llu", (unsigned long long)stream_id); struct lws *child = nwsi->mux.child_list; while (child) { - if (child->mux.my_sid == stream_id) { + if ((uint64_t)child->mux.my_sid == stream_id) { lwsl_wsi_notice(child, "QUIC RX: Stream closed by peer via STOP_SENDING"); lws_close_free_wsi(child, LWS_CLOSE_STATUS_ABNORMAL_CLOSE, "quic stop sending"); break; @@ -747,18 +855,22 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_FRAME_ENCODING_ERROR, type, 0); return -1; } - lwsl_wsi_info(nwsi, "QUIC RX: Parsed MAX/BLOCKED STREAMS! max_streams %llu", (unsigned long long)max_streams); + lwsl_wsi_notice(nwsi, "QUIC RX: Parsed MAX/BLOCKED STREAMS! max_streams %llu", (unsigned long long)max_streams); if (qn) { if (type == LWS_QUIC_FT_MAX_STREAMS_BIDI) { - qn->max_streams_bidi_remote = max_streams; + if (max_streams > qn->max_streams_bidi_remote) { + qn->max_streams_bidi_remote = max_streams; #if defined(LWS_WITH_CLIENT) - lws_wsi_mux_apply_queue(nwsi); + lws_wsi_mux_apply_queue(nwsi); #endif + } } else if (type == LWS_QUIC_FT_MAX_STREAMS_UNIDI) { - qn->max_streams_unidi_remote = max_streams; + if (max_streams > qn->max_streams_unidi_remote) { + qn->max_streams_unidi_remote = max_streams; #if defined(LWS_WITH_CLIENT) - lws_wsi_mux_apply_queue(nwsi); + lws_wsi_mux_apply_queue(nwsi); #endif + } } } break; @@ -816,6 +928,10 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl f_pr->len = 8; f_pr->data = (uint8_t *)&f_pr[1]; memcpy(f_pr->data, path_data, 8); + if (sa46) { + f_pr->dest_sa46 = *sa46; + f_pr->has_dest = 1; + } lws_dll2_add_head(&f_pr->list, &nwsi->quic.qn->pending_tx[level]); lws_callback_on_writable(nwsi); } @@ -824,6 +940,49 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl lwsl_wsi_notice(nwsi, "QUIC RX: Path validated via PATH_RESPONSE!"); nwsi->quic.qn->address_validated = 1; nwsi->quic.qn->path_challenge_pending = 0; + + if (nwsi->quic.qn->migration_probing_wsi == nwsi) { + struct lws *old_nwsi = nwsi->quic.qn->nwsi; + /* Reparent the connection! */ + nwsi->quic.qn->nwsi = nwsi; + nwsi->quic.qn->migration_probing_wsi = NULL; + + if (old_nwsi && old_nwsi != nwsi) { + lwsl_wsi_notice(nwsi, "QUIC: Active Migration Make-Before-Break Complete! Closing old nwsi."); + old_nwsi->quic.qn = NULL; /* Detach so close doesn't free it */ + + /* Transition new nwsi to established state and cancel its timeout/grace timers */ + lwsi_set_state(nwsi, LRS_ESTABLISHED); + lws_set_timeout(nwsi, NO_PENDING_TIMEOUT, 0); +#if defined(LWS_WITH_CLIENT) + lws_sul_cancel(&nwsi->sul_h3_grace); + lws_sul_cancel(&nwsi->sul_happy_eyeballs); +#endif + lws_sul_cancel(&nwsi->sul_connect_timeout); + + /* Reparent all child streams to the new nwsi */ + struct lws *w = old_nwsi->mux.child_list; + while (w) { + w->mux.parent_wsi = nwsi; + w = w->mux.sibling_list; + } + nwsi->mux.child_list = old_nwsi->mux.child_list; + old_nwsi->mux.child_list = NULL; + nwsi->mux.child_count = old_nwsi->mux.child_count; + old_nwsi->mux.child_count = 0; + nwsi->mux.highest_sid = old_nwsi->mux.highest_sid; + +#if defined(LWS_WITH_TLS) + extern int lws_tls_quic_migrate_wsi(struct lws *old_wsi, struct lws *new_wsi); + /* Move TLS object so we can decrypt NEW_SESSION_TICKET and KeyUpdate */ + nwsi->tls = old_nwsi->tls; + memset(&old_nwsi->tls, 0, sizeof(old_nwsi->tls)); + lws_tls_quic_migrate_wsi(old_nwsi, nwsi); +#endif + + lws_close_free_wsi(old_nwsi, LWS_CLOSE_STATUS_NOSTATUS, "migrated"); + } + } } else { lwsl_wsi_notice(nwsi, "QUIC RX: Spurious or mismatched PATH_RESPONSE, ignoring"); } @@ -872,32 +1031,21 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl lwsl_wsi_notice(nwsi, "QUIC RX: Truncated DATAGRAM frame"); return -1; } - lwsl_wsi_info(nwsi, "QUIC RX: Parsed DATAGRAM! len %llu", (unsigned long long)datagram_len); - - /* Pass datagram payload to protocol callback via LWS_CALLBACK_RECEIVE on network wsi - Wait, actually we should use the h3 role's receive processing or just dispatch to user callback. - Since DATAGRAMs in H3 have Quarter Stream IDs, we will just queue them to the nwsi rx chunks, - or just invoke a direct callback. - For now, lws_quic_rx_reassemble to the network wsi's own rx_chunks if it was supported, - but network wsi doesn't have a stream. Let's just create an rx_chunk on nwsi if we can, - or directly call LWS_CALLBACK_RECEIVE if possible. We will call the user callback directly. */ - if (nwsi->role_ops && nwsi->a.protocol && nwsi->a.protocol->callback) { - /* Note: WebTransport datagrams will be dispatched inside H3 role */ - /* We can push this data to a special list or just call rxflow right away */ - if (lws_rops_fidx(nwsi->role_ops, LWS_ROPS_handle_POLLIN)) { - /* Create a dummy rx chunk on a special nwsi datagram queue? - Actually it's better to just call LWS_CALLBACK_RECEIVE on nwsi here. */ - /* But to keep rx flow control, let's just dispatch it. */ + lwsl_wsi_notice(nwsi, "QUIC RX: Parsed DATAGRAM! len %llu", (unsigned long long)datagram_len); + + /* Parse Quarter Session ID for WebTransport */ + uint64_t qsid = 0; + size_t qsid_len = lws_quic_parse_varint(&payload[pos], (size_t)datagram_len, &qsid); + lwsl_wsi_notice(nwsi, "QUIC RX: datagram qsid=%llu, qsid_len=%zu", (unsigned long long)qsid, qsid_len); + if (qsid_len > 0 && qsid_len <= (size_t)datagram_len) { + uint64_t sid = qsid * 4; + struct lws *wsi_session = lws_quic_stream_find(nwsi, sid); + lwsl_wsi_notice(nwsi, "QUIC RX: wsi_session found=%p (is_session=%d)", wsi_session, wsi_session ? wsi_session->wt.is_session : 0); + if (wsi_session && wsi_session->wt.is_session && wsi_session->a.protocol && wsi_session->a.protocol->callback) { + /* Route the datagram payload to the WebTransport session's callback */ + wsi_session->a.protocol->callback(wsi_session, LWS_CALLBACK_RECEIVE, + wsi_session->user_space, &payload[pos + qsid_len], (size_t)(datagram_len - qsid_len)); } - /* For now, direct callback. H3 will intercept this in its rxflow/rx handling */ - /* Let's set a flag or just call LWS_CALLBACK_RECEIVE with a new reason, - * but LWS_CALLBACK_RECEIVE_CLIENT_HTTP works. */ - nwsi->quic.qn->rx_packets_since_update++; /* Just a dummy increment */ - - /* A better way: store in nwsi->quic.qn->rx_crypto_chunks[LWS_QUIC_LEVEL_APP] for datagrams? No. */ - /* We will call lws_quic_rx_reassemble but with wsi_child = nwsi, and qs = NULL? - * Yes, let's add a datagram callback or just use lws_quic_rx_reassemble with is_crypto=2 */ - lws_quic_rx_reassemble(nwsi, nwsi, NULL, 0, &payload[pos], (size_t)datagram_len, 2, LWS_QUIC_LEVEL_APP); } pos += (size_t)datagram_len; @@ -1040,20 +1188,35 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl int is_remote_initiated = (wsi_child->quic.qs->is_server_initiated == nwsi->quic.qn->is_server ? 0 : 1); if (is_bidi) { if (is_remote_initiated) { - if (qn->peer_initial_max_stream_data_bidi_remote) { - wsi_child->txc.peer_tx_cr_est = (int32_t)qn->peer_initial_max_stream_data_bidi_remote; - wsi_child->txc.tx_cr = (int32_t)qn->peer_initial_max_stream_data_bidi_remote; + if (qn->peer_initial_max_stream_data_bidi_local) { + uint64_t val = qn->peer_initial_max_stream_data_bidi_local; + int32_t cval = (val > (uint64_t)INT32_MAX) ? INT32_MAX : (int32_t)val; + wsi_child->txc.peer_tx_cr_est = cval; + wsi_child->txc.tx_cr = cval; + } else { + wsi_child->txc.peer_tx_cr_est = 65535; + wsi_child->txc.tx_cr = 65535; } } else { - if (qn->peer_initial_max_stream_data_bidi_local) { - wsi_child->txc.peer_tx_cr_est = (int32_t)qn->peer_initial_max_stream_data_bidi_local; - wsi_child->txc.tx_cr = (int32_t)qn->peer_initial_max_stream_data_bidi_local; + if (qn->peer_initial_max_stream_data_bidi_remote) { + uint64_t val = qn->peer_initial_max_stream_data_bidi_remote; + int32_t cval = (val > (uint64_t)INT32_MAX) ? INT32_MAX : (int32_t)val; + wsi_child->txc.peer_tx_cr_est = cval; + wsi_child->txc.tx_cr = cval; + } else { + wsi_child->txc.peer_tx_cr_est = 65535; + wsi_child->txc.tx_cr = 65535; } } } else { if (qn->peer_initial_max_stream_data_uni) { - wsi_child->txc.peer_tx_cr_est = (int32_t)qn->peer_initial_max_stream_data_uni; - wsi_child->txc.tx_cr = (int32_t)qn->peer_initial_max_stream_data_uni; + uint64_t val = qn->peer_initial_max_stream_data_uni; + int32_t cval = (val > (uint64_t)INT32_MAX) ? INT32_MAX : (int32_t)val; + wsi_child->txc.peer_tx_cr_est = cval; + wsi_child->txc.tx_cr = cval; + } else { + wsi_child->txc.peer_tx_cr_est = 65535; + wsi_child->txc.tx_cr = 65535; } } } @@ -1118,10 +1281,12 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl type == LWS_QUIC_FT_MAX_DATA ? "MAX_DATA" : "DATA_BLOCKED", (unsigned long long)max_data); if (type == LWS_QUIC_FT_MAX_DATA) { - int32_t current_max = (int32_t)(nwsi->quic.qn->tx_conn_offset + (uint64_t)nwsi->txc.tx_cr); - int32_t delta = (int32_t)max_data - current_max; - if (delta > 0) + int64_t current_max = (int64_t)nwsi->quic.qn->tx_conn_offset + nwsi->txc.tx_cr; + if ((int64_t)max_data > current_max) { + int64_t delta64 = (int64_t)max_data - current_max; + int32_t delta = (delta64 > (int64_t)INT32_MAX) ? INT32_MAX : (int32_t)delta64; lws_wsi_tx_credit(nwsi, LWSTXCR_US_TO_PEER, delta); + } } break; } else if (type == LWS_QUIC_FT_MAX_STREAM_DATA || type == LWS_QUIC_FT_STREAM_DATA_BLOCKED) { @@ -1148,11 +1313,18 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl if (!is_peer_initiated) { if (!wsi_child) { - lwsl_wsi_notice(nwsi, "QUIC RX: %s on non-existing stream ID %llu", + uint64_t next_id = is_unidirectional ? qn->next_stream_id_unidi_local : qn->next_stream_id_bidi_local; + if (stream_id >= next_id) { + lwsl_wsi_notice(nwsi, "QUIC RX: %s on never-created stream ID %llu", + type == LWS_QUIC_FT_MAX_STREAM_DATA ? "MAX_STREAM_DATA" : "STREAM_DATA_BLOCKED", + (unsigned long long)stream_id); + lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_STREAM_STATE_ERROR, type, 0); + return -1; + } + lwsl_wsi_info(nwsi, "QUIC RX: Ignoring %s on already closed stream ID %llu", type == LWS_QUIC_FT_MAX_STREAM_DATA ? "MAX_STREAM_DATA" : "STREAM_DATA_BLOCKED", (unsigned long long)stream_id); - lws_quic_enter_closing_state(nwsi, LWS_QUIC_ERR_STREAM_STATE_ERROR, type, 0); - return -1; + break; } if (is_unidirectional && type == LWS_QUIC_FT_STREAM_DATA_BLOCKED) { lwsl_wsi_notice(nwsi, "QUIC RX: STREAM_DATA_BLOCKED on receive-only stream ID %llu", @@ -1176,10 +1348,12 @@ lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payl if (type == LWS_QUIC_FT_MAX_STREAM_DATA) { struct lws *child = lws_quic_stream_find(nwsi, stream_id); if (child) { - int32_t current_max = (int32_t)((child->quic.qs ? child->quic.qs->tx_offset : 0) + (uint64_t)child->txc.tx_cr); - int32_t delta = (int32_t)max_stream_data - current_max; - if (delta > 0) + int64_t current_max = (int64_t)(child->quic.qs ? child->quic.qs->tx_offset : 0) + child->txc.tx_cr; + if ((int64_t)max_stream_data > current_max) { + int64_t delta64 = (int64_t)max_stream_data - current_max; + int32_t delta = (delta64 > (int64_t)INT32_MAX) ? INT32_MAX : (int32_t)delta64; lws_wsi_tx_credit(child, LWSTXCR_US_TO_PEER, delta); + } } } break; @@ -1234,7 +1408,7 @@ lws_quic_parse_transport_parameters(struct lws *wsi, const uint8_t *buf, size_t /* Check for duplicates */ for (size_t i = 0; i < num_seen; i++) { if (seen_params[i] == param_id) { - lwsl_wsi_err(wsi, "QUIC TP error: Duplicate parameter ID %llu", (unsigned long long)param_id); + lwsl_wsi_err(wsi, "QUIC TP error: Duplicate parameter ID %llu", (unsigned long long)param_id); return -1; } } @@ -1242,6 +1416,28 @@ lws_quic_parse_transport_parameters(struct lws *wsi, const uint8_t *buf, size_t seen_params[num_seen++] = param_id; switch (param_id) { + case 0x11: { /* version_information */ + if (param_len < 4 || (param_len % 4) != 0) { + lwsl_wsi_err(wsi, "QUIC TP error: version_information bad length"); + return -1; + } + if (qn->is_server && (wsi->a.context->options & LWS_SERVER_OPTION_QUIC_LATEST_VERSION)) { + size_t offset = 4; + while (offset < param_len) { + uint32_t av = ((uint32_t)buf[pos + offset] << 24) | + ((uint32_t)buf[pos + offset + 1] << 16) | + ((uint32_t)buf[pos + offset + 2] << 8) | + ((uint32_t)buf[pos + offset + 3]); + if (av == LWS_QUIC_VERSION_2) { + qn->version = LWS_QUIC_VERSION_2; + lwsl_wsi_notice(wsi, "QUIC: Upgrading to QUIC v2 via Compatible Version Negotiation"); + break; + } + offset += 4; + } + } + break; + } case 0x0f: /* initial_source_connection_id */ seen_initial_source_cid = 1; break; @@ -1339,17 +1535,62 @@ lws_quic_parse_transport_parameters(struct lws *wsi, const uint8_t *buf, size_t } break; case 0x02: /* stateless_reset_token */ - case 0x0d: /* preferred_address */ if (qn->is_server) { /* Client cannot send these */ lwsl_wsi_err(wsi, "QUIC TP error: Client sent server-only parameter %llu", (unsigned long long)param_id); return -1; } - if (param_id == 0x02 && param_len != 16) { + if (param_len != 16) { lwsl_wsi_err(wsi, "QUIC TP error: stateless_reset_token length %llu != 16", (unsigned long long)param_len); return -1; } break; +#if defined(LWS_WITH_CLIENT) + case 0x0d: /* preferred_address */ + if (qn->is_server) { + /* Client cannot send these */ + lwsl_wsi_err(wsi, "QUIC TP error: Client sent server-only parameter %llu", (unsigned long long)param_id); + return -1; + } + if (param_len >= 4+2+16+2+1+16) { + /* Pick IPv4 if present, else IPv6 */ + char addr_str[64]; + int port = 0; + struct lws_client_connect_info i; + + /* Try IPv4 first (RFC says it's 4 bytes IP + 2 bytes port) */ + uint32_t ip4; + memcpy(&ip4, &buf[pos], 4); + if (ip4 != 0) { + lws_snprintf(addr_str, sizeof(addr_str), "%u.%u.%u.%u", + buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]); + port = (buf[pos+4] << 8) | buf[pos+5]; + } else { + /* Try IPv6 */ + const uint8_t *v6 = &buf[pos+6]; + lws_snprintf(addr_str, sizeof(addr_str), "[%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x]", + v6[0], v6[1], v6[2], v6[3], v6[4], v6[5], v6[6], v6[7], + v6[8], v6[9], v6[10], v6[11], v6[12], v6[13], v6[14], v6[15]); + port = (buf[pos+22] << 8) | buf[pos+23]; + } + + if (port > 0) { + lwsl_wsi_notice(wsi, "QUIC TP: Migrating to preferred_address %s:%d", addr_str, port); + memset(&i, 0, sizeof(i)); + i.context = wsi->a.context; + i.vhost = wsi->a.vhost; + i.address = addr_str; + i.host = addr_str; + i.origin = addr_str; + i.port = port; + i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_INSECURE; + i.quic_migrate_from_wsi = qn->nwsi; + + lws_client_connect_via_info(&i); + } + } + break; +#endif default: break; } diff --git a/lib/roles/quic/private-lib-roles-quic.h b/lib/roles/quic/private-lib-roles-quic.h index f52c847cc3..302ff40540 100644 --- a/lib/roles/quic/private-lib-roles-quic.h +++ b/lib/roles/quic/private-lib-roles-quic.h @@ -29,8 +29,8 @@ extern const struct lws_role_ops role_ops_quic; #define LWS_QUIC_MAX_CID_LEN 20 -#define LWS_QUIC_VERSION_1 0x00000001 -#define LWS_QUIC_VERSION_2 0x709a50c4 +#define LWS_QUIC_VERSION_1 0x1 +#define LWS_QUIC_VERSION_2 0x6b3343cf #define LWS_QUIC_DEFAULT_WINDOW (1024 * 1024) #define LWS_QUIC_MAX_WINDOW (16 * 1024 * 1024) @@ -169,6 +169,8 @@ struct lws_quic_tx_frame { lws_usec_t sent_time_us; size_t wire_len; uint16_t packet_size; + lws_sockaddr46 dest_sa46; + uint8_t has_dest; }; struct lws_quic_rx_chunk { @@ -209,6 +211,8 @@ struct lws_quic_stream { uint8_t is_unidirectional:1; uint8_t is_server_initiated:1; uint8_t opted_into_early_data:1; + uint8_t close_after_rx:1; + uint8_t sent_fin:1; }; @@ -220,8 +224,6 @@ struct lws_quic_netconn { struct lws_quic_cid rem_cid; /* Remote peer's Connection ID */ struct lws_quic_cid orig_dcid; /* Original Destination Connection ID from client */ - uint8_t local_tp_buf[4096]; /* buffer for transport parameters */ - /* Array of pointers to lazily allocated key material */ struct lws_quic_keys *keys[LWS_QUIC_LEVEL_COUNT]; @@ -247,6 +249,8 @@ struct lws_quic_netconn { uint64_t next_stream_id_bidi_local; uint64_t next_stream_id_unidi_local; + uint64_t next_stream_id_bidi_remote; + uint64_t next_stream_id_unidi_remote; /* Frames waiting to be bundled into outgoing packets */ lws_dll2_owner_t pending_tx[LWS_QUIC_LEVEL_COUNT]; @@ -283,10 +287,13 @@ struct lws_quic_netconn { uint64_t bytes_sent; uint32_t version; + uint32_t original_version; - uint64_t conn_close_err; - size_t crypto_rx_expected_msg_len[4]; - uint8_t highest_rx_level; + uint64_t conn_close_err; + size_t crypto_rx_expected_msg_len[4]; + uint8_t *crypto_rx_buf[4]; + size_t crypto_rx_buf_len[4]; + uint8_t highest_rx_level; uint8_t pto_count; /* Key Update Tracking */ @@ -303,6 +310,7 @@ struct lws_quic_netconn { /* Path Validation (RFC 9000 Section 8.2) */ uint8_t path_challenge[8]; uint8_t path_challenge_pending:1; + struct lws *migration_probing_wsi; /* ECN (Explicit Congestion Notification) */ uint64_t ecn_rx_ect0; @@ -313,7 +321,7 @@ struct lws_quic_netconn { uint8_t handshake_done:1; uint8_t tp_parsed:1; uint8_t alpn_migrated:1; - uint8_t pto_probe_needed:1; + uint8_t pto_probe_needed:2; uint8_t address_validated:1; uint8_t is_closing:1; @@ -329,6 +337,16 @@ struct lws_quic_netconn { struct lws_quic_cid retry_scid; }; +struct lws_quic_cc_newreno { + size_t cwnd; + size_t ssthresh; + size_t bytes_in_flight; + lws_usec_t congestion_recovery_start_time; + + lws_usec_t last_pacing_time; + size_t pacing_credit; +}; + extern const struct lws_cc_ops lws_cc_ops_newreno; int @@ -340,6 +358,9 @@ lws_quic_set_keys(struct lws *wsi, enum lws_tls_quic_secret_type type, const uin void lws_quic_keys_destroy(struct lws_quic_keys *keys); +void +lws_quic_queue_path_challenge(struct lws *nwsi); + int lws_quic_update_keys(struct lws_quic_keys *k, int is_rx); @@ -358,16 +379,20 @@ size_t lws_quic_parse_varint(const uint8_t *buf, size_t len, uint64_t *val); size_t -lws_quic_get_pn_offset(const uint8_t *buf, size_t len, size_t *payload_len); +lws_quic_get_pn_offset(const uint8_t *buf, size_t len, size_t *payload_len, size_t dcid_len); size_t lws_quic_write_varint(uint8_t *buf, size_t len, uint64_t val); int -lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payload_len); +lws_quic_parse_frames(struct lws *nwsi, int level, uint8_t *payload, size_t payload_len, const lws_sockaddr46 *sa46); void lws_quic_handle_ack(struct lws *nwsi, int level, uint64_t acked_pn); +void + +lws_quic_detect_loss(struct lws *nwsi, int level, uint64_t largest_acked); + void lws_quic_discard_keys(struct lws *nwsi, int level); @@ -397,6 +422,8 @@ struct _lws_quic_related { struct lws_quic_netconn *qn; /* malloc'd for root net conn */ struct lws_quic_stream *qs; /* malloc'd for stream child wsi */ + struct lws *migrate_from_wsi; /* if set, this nwsi is migrating from an existing nwsi */ + lws_usec_t quic_race_start_us; uint8_t initialized:1; diff --git a/lib/roles/ws/client-ws.c b/lib/roles/ws/client-ws.c index ea3e9c09ea..d9db397933 100644 --- a/lib/roles/ws/client-ws.c +++ b/lib/roles/ws/client-ws.c @@ -592,12 +592,16 @@ lws_client_ws_upgrade(struct lws *wsi, const char **cce) #endif /* - * Confirm his accept token is the one we precomputed + * Confirm his accept token is the one we precomputed. An extended + * CONNECT reply (ws-over-h2, RFC 8441) has no key exchange and so no + * accept token; p is NULL there and there is nothing to check. */ p = lws_hdr_simple_ptr(wsi, WSI_TOKEN_ACCEPT); - if (strcmp(p, wsi->http.ah->initial_handshake_hash_base64)) { - lwsl_wsi_warn(wsi, "lws_client_int_s_hs: accept '%s' wrong vs '%s'", p, + if (!wsi->client_mux_substream && + (!p || strcmp(p, wsi->http.ah->initial_handshake_hash_base64))) { + lwsl_wsi_warn(wsi, "lws_client_int_s_hs: accept '%s' wrong vs '%s'", + p ? p : "(null)", wsi->http.ah->initial_handshake_hash_base64); *cce = "HS: Accept hash wrong"; goto bail2; @@ -627,7 +631,15 @@ lws_client_ws_upgrade(struct lws *wsi, const char **cce) /* free up his parsing allocations */ lws_header_table_detach(wsi, 0); - lws_role_transition(wsi, LWSIFR_CLIENT, LRS_ESTABLISHED, &role_ops_ws); + /* + * A ws stream carried inside an h2 connection (RFC 8441) must keep + * the encapsulation visible in its role, like the server side does: + * writes, writable requests and tx credit all route via the h2 + * parent based on lwsi_role_h2_ENCAPSULATION(). + */ + lws_role_transition(wsi, LWSIFR_CLIENT | + (wsi->client_mux_substream ? LWSIFR_P_ENCAP_H2 : 0), + LRS_ESTABLISHED, &role_ops_ws); lws_validity_confirmed(wsi); wsi->rxflow_change_to = LWS_RXFLOW_ALLOW; diff --git a/lib/roles/ws/ops-ws.c b/lib/roles/ws/ops-ws.c index 2082767dd5..bb2ef32c37 100644 --- a/lib/roles/ws/ops-ws.c +++ b/lib/roles/ws/ops-ws.c @@ -1979,10 +1979,46 @@ rops_write_role_protocol_ws(struct lws *wsi, unsigned char *buf, size_t len, assert(encap != wsi); - return lws_rops_func_fidx(encap->role_ops, +#if !defined(LWS_WITHOUT_EXTENSIONS) + /* + * The h1 path fires LWS_EXT_CB_PACKET_TX_PRESEND from + * lws_issue_raw_ext_access(); this encapsulated path used to + * bypass it, so permessage-deflate never got to set RSV1 (or + * fix up the first-frame opcode) on the compressed frame it + * already produced above -- peers then treated the deflated + * payload as plain text. Give active extensions the same + * look at the assembled frame here. + */ + { + struct lws_tokens ebuf; + + ebuf.token = buf - pre; + ebuf.len = (int)(len + (unsigned int)pre); + + if (lws_ext_cb_active(wsi, LWS_EXT_CB_PACKET_TX_PRESEND, + &ebuf, 0) < 0) + return -1; + } +#endif + + n = lws_rops_func_fidx(encap->role_ops, LWS_ROPS_write_role_protocol). write_role_protocol(wsi, buf - pre, len + (unsigned int)pre, wp); + if (n < 0) + return n; + + /* + * The lws_write() contract is to report how much of the + * CALLER's payload was accepted. len here is the + * post-extension (eg, permessage-deflate compressed) frame + * size, which is routinely SMALLER than what the caller + * passed in -- returning it (as this path used to) makes + * well-behaved callers conclude the write failed short and + * kill the connection. The h1 path returns orig_len for + * exactly this reason; do the same. + */ + return (int)orig_len; } switch ((*wp) & 0x1f) { @@ -2071,6 +2107,15 @@ rops_callback_on_writable_ws(struct lws *wsi) encapsulation_parent(wsi); assert(enc); + if (!enc) + /* + * Mid-teardown: close_kill_connection already unlinked + * us from the h2 parent (the close callback fires + * after that). Nothing can be scheduled any more; on + * release builds the assert above is compiled out and + * this used to segfault. + */ + return 1; if (lws_rops_func_fidx(enc->role_ops, LWS_ROPS_callback_on_writable). callback_on_writable(wsi)) @@ -2080,6 +2125,26 @@ rops_callback_on_writable_ws(struct lws *wsi) return 0; } +static int +rops_tx_credit_ws(struct lws *wsi, char peer_to_us, int add) +{ +#if defined(LWS_WITH_HTTP2) + /* + * ws-over-h2: flow control belongs to the encapsulating h2 stream. + * Delegate so lws_get_peer_write_allowance() reports the stream's + * real tx window and so lws_wsi_tx_credit() can grant manual rx + * credit (LCCSCF_H2_MANUAL_RXFLOW) on a ws-upgraded stream. + */ + if (lwsi_role_h2_ENCAPSULATION(wsi)) + return lws_rops_func_fidx(&role_ops_h2, LWS_ROPS_tx_credit). + tx_credit(wsi, peer_to_us, add); +#endif + (void)peer_to_us; + (void)add; + + return -1; /* no guidance, like the rops being absent */ +} + static int rops_init_vhost_ws(struct lws_vhost *vh, const struct lws_context_creation_info *info) @@ -2196,6 +2261,7 @@ static const lws_rops_t rops_table_ws[] = { /* 10 */ { .close_kill_connection = rops_close_kill_connection_ws }, /* 11 */ { .destroy_role = rops_destroy_role_ws }, /* 12 */ { .issue_keepalive = rops_issue_keepalive_ws }, + /* 13 */ { .tx_credit = rops_tx_credit_ws }, }; const struct lws_role_ops role_ops_ws = { @@ -2213,7 +2279,7 @@ const struct lws_role_ops role_ops_ws = { /* LWS_ROPS_handle_POLLOUT */ /* LWS_ROPS_perform_user_POLLOUT */ 0x50, /* LWS_ROPS_callback_on_writable */ - /* LWS_ROPS_tx_credit */ 0x60, + /* LWS_ROPS_tx_credit */ 0x6d, /* LWS_ROPS_write_role_protocol */ /* LWS_ROPS_encapsulation_parent */ 0x70, /* LWS_ROPS_alpn_negotiated */ diff --git a/lib/roles/wt/ops-wt.c b/lib/roles/wt/ops-wt.c index 5c228b027c..af9989c25c 100644 --- a/lib/roles/wt/ops-wt.c +++ b/lib/roles/wt/ops-wt.c @@ -38,11 +38,13 @@ rops_handle_POLLIN_wt(struct lws_context_per_thread *pt, struct lws *wsi, return LWS_HPI_RET_HANDLED; } +struct lws *lws_get_quic_network_wsi(struct lws *wsi); + static int rops_write_role_protocol_wt(struct lws *wsi, unsigned char *buf, size_t len, enum lws_write_protocol *wp) { - struct lws *nwsi = lws_get_network_wsi(wsi); + struct lws *nwsi = lws_get_quic_network_wsi(wsi); if (!nwsi) return -1; @@ -88,13 +90,19 @@ rops_close_kill_connection_wt(struct lws *wsi, enum lws_close_status reason) if (wsi->wt.wtn) { lws_free_set_NULL(wsi->wt.wtn); } + if (wsi->mux.parent_wsi) { + lws_wsi_mux_sibling_disconnect(wsi); + } return 0; } +struct lws * +lws_get_quic_network_wsi(struct lws *wsi); + LWS_VISIBLE struct lws * lws_wt_create_stream(struct lws *wsi_session, int unidi) { - struct lws *nwsi = lws_get_network_wsi(wsi_session); + struct lws *nwsi = lws_get_quic_network_wsi(wsi_session); struct lws_quic_netconn *qn = nwsi ? nwsi->quic.qn : NULL; struct lws *cwsi; @@ -179,10 +187,76 @@ lws_wt_is_session(struct lws *wsi) return wsi->wt.is_session; } + +LWS_VISIBLE int +lws_wt_is_unidi(struct lws *wsi) +{ + return wsi->quic.qs && wsi->quic.qs->is_unidirectional; +} + +LWS_VISIBLE struct lws * +lws_wt_create_stream_from_child(struct lws *child_wsi, int unidi) +{ + struct lws *quic_nwsi = lws_get_quic_network_wsi(child_wsi); + if (quic_nwsi) { + struct lws *child = quic_nwsi->mux.child_list; + while (child) { + if (child->wt.is_session) { + return lws_wt_create_stream(child, unidi); + } + child = child->mux.sibling_list; + } + } + return NULL; +} + +LWS_VISIBLE struct lws * +lws_wt_get_session_wsi(struct lws *wsi) +{ + struct lws *quic_nwsi = lws_get_quic_network_wsi(wsi); + if (quic_nwsi) { + struct lws *child = quic_nwsi->mux.child_list; + while (child) { + if (child->wt.is_session) { + return child; + } + child = child->mux.sibling_list; + } + } + return NULL; +} + +static int +rops_perform_user_POLLOUT_wt(struct lws *wsi) +{ + return lws_callback_as_writeable(wsi); +} + +static int +rops_callback_on_writable_wt(struct lws *wsi) +{ + struct lws *nwsi = lws_get_network_wsi(wsi); + + lwsl_debug("rops_callback_on_writable_wt called for %s (nwsi=%s)\n", + lws_wsi_tag(wsi), nwsi ? lws_wsi_tag(nwsi) : "none"); + + if (!nwsi) + return 0; + + lws_wsi_mux_mark_parents_needing_writeable(wsi); + + return lws_callback_on_writable(nwsi); +} + +extern int rops_tx_credit_quic(struct lws *wsi, char peer_to_us, int add); + static const lws_rops_t rops_table_wt[] = { /* 1 */ { .handle_POLLIN = rops_handle_POLLIN_wt }, /* 2 */ { .write_role_protocol = rops_write_role_protocol_wt }, /* 3 */ { .close_kill_connection = rops_close_kill_connection_wt }, + /* 4 */ { .perform_user_POLLOUT = rops_perform_user_POLLOUT_wt }, + /* 5 */ { .callback_on_writable = rops_callback_on_writable_wt }, + /* 6 */ { .tx_credit = rops_tx_credit_quic }, }; const struct lws_role_ops role_ops_wt = { @@ -196,17 +270,17 @@ const struct lws_role_ops role_ops_wt = { /* LWS_ROPS_init_vhost */ /* LWS_ROPS_destroy_vhost */ 0x00, /* LWS_ROPS_service_flag_pending */ - /* LWS_ROPS_handle_POLLIN */ 0x10, + /* LWS_ROPS_handle_POLLIN */ 0x01, /* LWS_ROPS_handle_POLLOUT */ - /* LWS_ROPS_perform_user_POLLOUT */ 0x00, + /* LWS_ROPS_perform_user_POLLOUT */ 0x04, /* LWS_ROPS_callback_on_writable */ - /* LWS_ROPS_tx_credit */ 0x00, + /* LWS_ROPS_tx_credit */ 0x56, /* LWS_ROPS_write_role_protocol */ - /* LWS_ROPS_encapsulation_parent */ 0x02, + /* LWS_ROPS_encapsulation_parent */ 0x20, /* LWS_ROPS_alpn_negotiated */ /* LWS_ROPS_close_via_role_protocol */ 0x00, /* LWS_ROPS_close_role */ - /* LWS_ROPS_close_kill_connection */ 0x30, + /* LWS_ROPS_close_kill_connection */ 0x03, /* LWS_ROPS_destroy_role */ /* LWS_ROPS_adoption_bind */ 0x00, /* LWS_ROPS_client_bind */ diff --git a/lib/secure-streams/cpp/lss.cxx b/lib/secure-streams/cpp/lss.cxx index c1cb89ed34..c5bfba3057 100644 --- a/lib/secure-streams/cpp/lss.cxx +++ b/lib/secure-streams/cpp/lss.cxx @@ -31,9 +31,9 @@ static const char *pcols[] = { "https://", "h2://", /* LWSSSP_H2 */ "h2s://", - "ws://", /* LWSSSP_WS */ + "ws://", /* LWSSSP_WS */ // NOSONAR "wss://", - "mqtt://", /* LWSSSP_MQTT */ + "mqtt://", /* LWSSSP_MQTT */ // NOSONAR "mqtts://", "raw://", /* LWSSSP_RAW */ "raws://", diff --git a/lib/system/policy.c b/lib/system/policy.c index 9d0270e04e..a9c3441da0 100644 --- a/lib/system/policy.c +++ b/lib/system/policy.c @@ -26,6 +26,14 @@ #if defined(LWS_WITH_NETWORK) #if defined(LWS_WITH_FILE_OPS) +/* + * lws_system_parse_policy() parses a JSON policy file and so depends on the + * LEJP JSON parser. Only build the real implementation when LEJP is enabled; + * otherwise provide a stub (in the #else below) that reports "no policy", so + * callers that reference it without gating on LWS_WITH_LEJP (eg, the ACME / + * versioned-cert path in tls.c) still link without pulling in lejp. + */ +#if defined(LWS_WITH_LEJP) static const char * const policy_paths[] = { "dns_base_dir", @@ -157,7 +165,19 @@ lws_system_parse_policy(struct lws_context *cx, const char *filepath, lws_system lws_system_policy_free(p); return 1; } -#endif + +#else /* !LWS_WITH_LEJP */ + +int +lws_system_parse_policy(struct lws_context *cx, const char *filepath, + lws_system_policy_t **_policy) +{ + *_policy = NULL; + return 1; /* no JSON parser available without LEJP */ +} + +#endif /* LWS_WITH_LEJP */ +#endif /* LWS_WITH_FILE_OPS */ void lws_system_policy_free(lws_system_policy_t *policy) diff --git a/lib/tls/gnutls/gnutls-quic.c b/lib/tls/gnutls/gnutls-quic.c index eb5f3ceaf9..03f02873be 100644 --- a/lib/tls/gnutls/gnutls-quic.c +++ b/lib/tls/gnutls/gnutls-quic.c @@ -69,7 +69,7 @@ gnutls_quic_secret_func(gnutls_session_t session, struct lws *wsi = (struct lws *)gnutls_session_get_ptr(session); int is_client; - if (!wsi || !wsi->tls.quic_secret_cb) + if (!wsi || !wsi->tls.quic_secret_cb || secret_size > 48) return 0; if (wsi->a.vhost) @@ -161,8 +161,8 @@ gnutls_quic_read_func(gnutls_session_t session, return 0; } - if (qlevel == b->target_level) { - if (!b->out || b->out_len + data_size > b->out_max) + if (b->out && qlevel == b->target_level) { + if (b->out_len + data_size > b->out_max) return GNUTLS_E_MEMORY_ERROR; memcpy(b->out + b->out_len, data, data_size); b->out_len += data_size; @@ -276,16 +276,34 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) return -1; } - /* Enforce QUIC requirements: TLS 1.3 only, NO compatibility mode (empty legacy_session_id) */ - ret = gnutls_priority_set_direct(session, "NORMAL:-VERS-ALL:+VERS-TLS1.3:%DISABLE_TLS13_COMPAT_MODE", NULL); - if (ret < 0) { - lwsl_err("gnutls_priority_set_direct failed: %s\n", gnutls_strerror(ret)); + const char *user_ciphers = NULL; + char priority[256]; + +#if defined(LWS_WITH_CLIENT) + if (wsi->quic.qn && !wsi->quic.qn->is_server) { + user_ciphers = wsi->a.vhost ? wsi->a.vhost->tls.cfg_tls_client_cipher_list : NULL; + } else +#endif + { + user_ciphers = wsi->a.vhost ? wsi->a.vhost->tls.cfg_ssl_cipher_list : NULL; } + if (user_ciphers) { + lws_snprintf(priority, sizeof(priority), "%s:%%DISABLE_TLS13_COMPAT_MODE", user_ciphers); + } else { + lws_snprintf(priority, sizeof(priority), "NORMAL:-VERS-ALL:+VERS-TLS1.3:%%DISABLE_TLS13_COMPAT_MODE"); + } + + + /* Enforce QUIC requirements: TLS 1.3 only, NO compatibility mode (empty legacy_session_id) */ + ret = gnutls_priority_set_direct(session, priority, NULL); + if (ret < 0) + lwsl_notice("gnutls_priority_set_direct failed: %s\n", gnutls_strerror(ret)); + if (!wsi->tls.quic_tp_send) { /* Construct dynamically to include loc_cid */ struct lws_quic_netconn *qn = wsi->quic.qn; - static uint8_t dynamic_tp[128]; + uint8_t dynamic_tp[128]; uint8_t *p = dynamic_tp; /* initial_max_stream_data_bidi_local (0x05), len 4, val 65535 */ @@ -302,8 +320,14 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) *p++ = 0x09; *p++ = 0x02; *p++ = 0x40; *p++ = 0x64; /* max_idle_timeout (0x01), len 4, val 30000 */ *p++ = 0x01; *p++ = 0x04; *p++ = 0x80; *p++ = 0x00; *p++ = 0x75; *p++ = 0x30; + /* max_datagram_frame_size (0x20), len 4, val 65535 */ + *p++ = 0x20; *p++ = 0x04; *p++ = 0x80; *p++ = 0x00; *p++ = 0xff; *p++ = 0xff; /* initial_source_connection_id (0x0f) */ if (qn && qn->loc_cid.len > 0) { + if ((size_t)(p - dynamic_tp) + 2 + qn->loc_cid.len > sizeof(dynamic_tp)) { + lwsl_err("loc_cid too large for TP buffer\n"); + return -1; + } *p++ = 0x0f; *p++ = qn->loc_cid.len; memcpy(p, qn->loc_cid.id, qn->loc_cid.len); @@ -327,10 +351,10 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) char *comma = strchr(p, ','); alpn[i].data = (uint8_t *)p; if (comma) { - alpn[i].size = (unsigned int)(comma - p); + alpn[i].size = (unsigned int)lws_ptr_diff_size_t(comma, p); p = comma + 1; } else { - alpn[i].size = (unsigned int)(end - p); + alpn[i].size = (unsigned int)lws_ptr_diff_size_t(end, p); p = end; } /* Replace comma with NUL for cleaner logging, though gnutls only uses size */ @@ -441,14 +465,32 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, *out_len = 0; return -1; } - if (gnutls_handshake_write(session, glevel, in, in_len) < 0) - return -1; + int hw = gnutls_handshake_write(session, glevel, in, in_len); + if (hw < 0) { + lwsl_err("gnutls_handshake_write failed: %d\n", hw); + return hw; + } } +#if defined(LWS_WITH_CLIENT) && defined(LWS_WITH_TLS_SESSIONS) + /* + * Try to cache any newly available session data (like NewSessionTicket) + * BEFORE calling gnutls_handshake, because gnutls_handshake sets + * handshake_in_progress=1 and causes gnutls_session_get_data2 to fail with -408. + */ + if (lwsi_role_client(wsi) && wsi->quic.qn && wsi->quic.qn->handshake_done) + lws_tls_session_new_gnutls(wsi); +#endif + n = gnutls_handshake(session); + lwsl_notice("gnutls_handshake returned %d\n", n); + + struct lws *active_wsi = (struct lws *)gnutls_session_get_ptr(session); + if (!active_wsi) + active_wsi = wsi; #if defined(LWS_WITH_SERVER) && defined(LWS_WITH_TLS_SESSIONS) - if (n == GNUTLS_E_SUCCESS && !lwsi_role_client(wsi) && wsi->quic.qn && !wsi->quic.qn->handshake_done) { + if (n == GNUTLS_E_SUCCESS && !lwsi_role_client(active_wsi) && active_wsi->quic.qn && !active_wsi->quic.qn->handshake_done) { #if GNUTLS_VERSION_NUMBER >= 0x030603 int r = gnutls_session_ticket_send(session, 1, 0); (void)r; @@ -462,8 +504,8 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, for (l = 0; l < 4; l++) { if (b->other_out_len[l]) { - if (wsi->quic.qn) { - lws_tls_quic_tx_crypto_cb(wsi, l, b->other_out[l], b->other_out_len[l]); + if (active_wsi->quic.qn) { + lws_tls_quic_tx_crypto_cb(active_wsi, l, b->other_out[l], b->other_out_len[l]); } lws_free_set_NULL(b->other_out[l]); b->other_out_len[l] = 0; @@ -472,14 +514,15 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, if (n == GNUTLS_E_SUCCESS) { #if defined(LWS_WITH_CLIENT) && defined(LWS_WITH_TLS_SESSIONS) - if (lwsi_role_client(wsi)) - lws_tls_session_new_gnutls(wsi); + if (lwsi_role_client(active_wsi)) + lws_tls_session_new_gnutls(active_wsi); #endif return 0; } - if (n == GNUTLS_E_AGAIN || n == GNUTLS_E_INTERRUPTED) + if (n == GNUTLS_E_AGAIN || n == GNUTLS_E_INTERRUPTED) { return 1; + } lwsl_err("gnutls_handshake failed: %d\n", n); return n < 0 ? n : -1; diff --git a/lib/tls/gnutls/gnutls-session.c b/lib/tls/gnutls/gnutls-session.c index 981c54a4b5..17c9fed91c 100644 --- a/lib/tls/gnutls/gnutls-session.c +++ b/lib/tls/gnutls/gnutls-session.c @@ -97,12 +97,14 @@ lws_tls_reuse_session(struct lws *wsi) ts = __lws_tls_session_lookup_by_name(wsi->a.vhost, buf); if (!ts) { - lwsl_tlssess("%s: no existing session for %s\n", __func__, buf); + lwsl_notice("%s: no existing session for %s\n", __func__, buf); goto bail; } - if (!ts->ser_data || !ts->ser_data->data) /* cache entry is invalid */ + if (!ts->ser_data || !ts->ser_data->data) { /* cache entry is invalid */ + lwsl_notice("%s: cache entry invalid for %s\n", __func__, buf); goto bail; + } if (gnutls_session_set_data((gnutls_session_t)wsi->tls.ssl, ts->ser_data->data, @@ -111,7 +113,7 @@ lws_tls_reuse_session(struct lws *wsi) goto bail; } - lwsl_tlssess("%s: resumed session for %s\n", __func__, (const char *)&ts[1]); + lwsl_notice("%s: resumed session for %s\n", __func__, (const char *)&ts[1]); wsi->tls_session_reused = 1; /* keep our session list sorted in lru -> mru order */ @@ -192,12 +194,31 @@ lws_tls_session_new_gnutls(struct lws *wsi) nl = strlen(buf); +#if (_LWS_ENABLED_LOGS & LLL_NOTICE) + /* Check if a session ticket has actually been received (TLS 1.3 requirement) */ + unsigned sess_flags = gnutls_session_get_flags((gnutls_session_t)wsi->tls.ssl); + lwsl_notice("%s: QUIC session ticket check: flags=0x%x, has_ticket=%d\n", __func__, + sess_flags, !!(sess_flags & GNUTLS_SFLAGS_SESSION_TICKET)); +#endif + int ret = gnutls_session_get_data2((gnutls_session_t)wsi->tls.ssl, &gd); + lwsl_notice("%s: gnutls_session_get_data2 ret=%d, len=%u\n", __func__, ret, gd.size); if (ret != GNUTLS_E_SUCCESS) { if (ret == GNUTLS_E_INTERNAL_ERROR) - lwsl_debug("%s: gnutls_session_get_data2 failed: %d (%s)\n", __func__, ret, gnutls_strerror(ret)); + lwsl_notice("%s: gnutls_session_get_data2 INTERNAL_ERROR (no ticket yet?): %s\n", __func__, gnutls_strerror(ret)); else - lwsl_tlssess("%s: gnutls_session_get_data2 failed: %d (%s)\n", __func__, ret, gnutls_strerror(ret)); + lwsl_notice("%s: gnutls_session_get_data2 failed: %d (%s)\n", __func__, ret, gnutls_strerror(ret)); + return 0; + } + + /* A real TLS 1.3 session ticket is always well over 32 bytes. + * GnuTLS may return a 4-byte placeholder before NewSessionTicket + * arrives. Reject any suspiciously small blob to avoid persisting + * a useless stub that will fail to resume on next connection. */ + if (gd.size < 32) { + lwsl_notice("%s: session data too small (%u bytes), ignoring stub ticket\n", + __func__, gd.size); + gnutls_free(gd.data); return 0; } @@ -316,3 +337,153 @@ lws_tls_session_cache(struct lws_vhost *vh, uint32_t ttl) /* Default to 1hr max recommendation from RFC5246 F.1.4 */ vh->tls.tls_session_cache_ttl = !ttl ? 3600 : ttl; } + +static lws_tls_scm_t * +lws_tls_session_add_entry(struct lws_vhost *vh, const char *tag) +{ + lws_tls_scm_t *ts; + size_t nl = strlen(tag); + + if (vh->tls_sessions.count == (vh->tls_session_cache_max ? + vh->tls_session_cache_max : 10)) { + /* + * We have reached the vhost's session cache limit, + * prune the LRU / head + */ + ts = lws_container_of(vh->tls_sessions.head, + lws_tls_scm_t, list); + + if (ts) { + lwsl_tlssess("%s: pruning oldest session\n", __func__); + /* Note: we are already holding vh lock when calling this */ + __lws_tls_session_destroy(ts); + } + } + + ts = lws_malloc(sizeof(*ts) + nl + 1, __func__); + if (!ts) + return NULL; + + memset(ts, 0, sizeof(*ts)); + memcpy(&ts[1], tag, nl + 1); + + lws_dll2_add_tail(&ts->list, &vh->tls_sessions); + + return ts; +} + +int +lws_tls_session_dump_save(struct lws_vhost *vh, const char *host, uint16_t port, + lws_tls_sess_cb_t cb_save, void *opq) +{ + struct lws_tls_session_dump d; + lws_tls_scm_t *ts; + int ret = 1; + void *v; + + if (vh->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) + return 1; + + lws_tls_session_tag_discrete(vh->name, host, port, d.tag, sizeof(d.tag)); + + lws_context_lock(vh->context, __func__); /* -------------- cx { */ + lws_vhost_lock(vh); /* -------------- vh { */ + + ts = __lws_tls_session_lookup_by_name(vh, d.tag); + if (!ts || !ts->ser_data || !ts->ser_data->data) + goto bail; + + d.blob_len = ts->ser_data->len; + v = d.blob = lws_malloc(d.blob_len, __func__); + if (!d.blob) + goto bail; + + memcpy(d.blob, ts->ser_data->data, d.blob_len); + d.opaque = opq; + + if (cb_save(vh->context, &d)) + lwsl_notice("%s: save failed\n", __func__); + else + ret = 0; + + lws_free(v); + +bail: + lws_vhost_unlock(vh); /* } vh -------------- */ + lws_context_unlock(vh->context); /* } cx -------------- */ + + return ret; +} + +int +lws_tls_session_dump_load(struct lws_vhost *vh, const char *host, uint16_t port, + lws_tls_sess_cb_t cb_load, void *opq) +{ + struct lws_tls_session_dump d; + lws_tls_scm_t *ts; + void *v; + + if (vh->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) + return 1; + + d.opaque = opq; + lws_tls_session_tag_discrete(vh->name, host, port, d.tag, sizeof(d.tag)); + lwsl_notice("%s: looking for tag %s\n", __func__, d.tag); + + lws_context_lock(vh->context, __func__); /* -------------- cx { */ + lws_vhost_lock(vh); /* -------------- vh { */ + + ts = __lws_tls_session_lookup_by_name(vh, d.tag); + if (ts) { + /* session already in cache — no need to load from cold storage */ + lwsl_notice("%s: session already exists for %s\n", __func__, d.tag); + goto bail1; + } + + if (cb_load(vh->context, &d)) { + lwsl_warn("%s: load failed\n", __func__); + goto bail1; + } + + /* cb_load allocated the blob; insert it into the session cache */ + v = d.blob; + + ts = lws_tls_session_add_entry(vh, d.tag); + if (!ts) { + lwsl_warn("%s: unable to add cache entry\n", __func__); + free(v); + goto bail1; + } + + if (!ts->ser_data) { + ts->ser_data = lws_malloc(sizeof(*ts->ser_data), __func__); + if (!ts->ser_data) { + free(v); + goto bail1; + } + memset(ts->ser_data, 0, sizeof(*ts->ser_data)); + } + + ts->ser_data->data = lws_malloc(d.blob_len, __func__); + if (!ts->ser_data->data) { + free(v); + goto bail1; + } + + memcpy(ts->ser_data->data, v, d.blob_len); + ts->ser_data->len = d.blob_len; + free(v); + + lwsl_tlssess("%s: session loaded OK\n", __func__); + + lws_vhost_unlock(vh); /* } vh -------------- */ + lws_context_unlock(vh->context); /* } cx -------------- */ + + return 0; + +bail1: + lws_vhost_unlock(vh); /* } vh -------------- */ + lws_context_unlock(vh->context); /* } cx -------------- */ + + return 1; +} diff --git a/lib/tls/gnutls/gnutls-ssl.c b/lib/tls/gnutls/gnutls-ssl.c index 3be4c62ccd..93987e830d 100644 --- a/lib/tls/gnutls/gnutls-ssl.c +++ b/lib/tls/gnutls/gnutls-ssl.c @@ -201,6 +201,7 @@ lws_tls_client_connect(struct lws *wsi, char *errbuf, size_t len) #if defined(LWS_WITH_TLS_SESSIONS) lws_tls_session_new_gnutls(wsi); #endif + lws_tls_server_conn_alpn(wsi); return LWS_SSL_CAPABLE_DONE; } diff --git a/lib/tls/gnutls/gnutls-tls.c b/lib/tls/gnutls/gnutls-tls.c index 83558b7ff7..8f29e4e26a 100644 --- a/lib/tls/gnutls/gnutls-tls.c +++ b/lib/tls/gnutls/gnutls-tls.c @@ -128,6 +128,43 @@ lws_tls_vhost_backend_create_ctx(struct lws_vhost *vhost) } #if defined(LWS_WITH_SERVER) + +#if GNUTLS_VERSION_NUMBER >= 0x030605 +struct lws_gnutls_ar_entry { + lws_dll2_t list; + size_t size; + uint8_t key[128]; +}; + +static int +lws_gnutls_anti_replay_db_add(void *db_ptr, long int exp_time, + const gnutls_datum_t *key, + const gnutls_datum_t *data) +{ + lws_dll2_owner_t *owner = (lws_dll2_owner_t *)db_ptr; + struct lws_gnutls_ar_entry *e; + + lws_start_foreach_dll(struct lws_dll2 *, d, lws_dll2_get_head(owner)) { + e = lws_container_of(d, struct lws_gnutls_ar_entry, list); + if (e->size == key->size && !memcmp(e->key, key->data, key->size)) + return GNUTLS_E_DB_ENTRY_EXISTS; + } lws_end_foreach_dll(d); + + if (owner->count >= 256) { + e = lws_container_of(lws_dll2_get_head(owner), struct lws_gnutls_ar_entry, list); + lws_dll2_remove(&e->list); + lws_free(e); + } + + e = lws_zalloc(sizeof(*e), "anti_replay"); + if (!e) return GNUTLS_E_MEMORY_ERROR; + e->size = key->size < sizeof(e->key) ? key->size : sizeof(e->key); + memcpy(e->key, key->data, e->size); + lws_dll2_add_tail(&e->list, owner); + return 0; +} +#endif + int lws_tls_server_vhost_backend_init(const struct lws_context_creation_info *info, struct lws_vhost *vhost, struct lws *wsi) @@ -162,6 +199,21 @@ lws_tls_server_vhost_backend_init(const struct lws_context_creation_info *info, return 1; } +#if GNUTLS_VERSION_NUMBER >= 0x030605 + if (gnutls_anti_replay_init((gnutls_anti_replay_t *)&vhost->tls.anti_replay) < 0) { + lwsl_err("%s: failed to init anti-replay\n", __func__); + return 1; + } + + lws_dll2_owner_t *ar_owner = lws_zalloc(sizeof(*ar_owner), "anti_replay_owner"); + if (ar_owner) { + vhost->tls.anti_replay_owner = ar_owner; + gnutls_anti_replay_set_ptr((gnutls_anti_replay_t)vhost->tls.anti_replay, ar_owner); + gnutls_anti_replay_set_add_function((gnutls_anti_replay_t)vhost->tls.anti_replay, + (gnutls_db_add_func)lws_gnutls_anti_replay_db_add); + } +#endif + return 0; } #endif @@ -275,14 +327,53 @@ lws_gnutls_server_name_cb(gnutls_session_t session) return 0; } +// static void my_gnutls_log(int level, const char *msg) { +// fprintf(stderr, "GNUTLS_LOG[%d]: %s", level, msg); +// fflush(stderr); +// } + int lws_tls_server_new_nonblocking(struct lws *wsi, lws_sockfd_type accept_fd) { gnutls_session_t session; + unsigned int flags = GNUTLS_SERVER; + + if (!wsi->a.vhost) { + lwsl_err("%s: NULL vhost\n", __func__); + return 1; + } - if (gnutls_init(&session, GNUTLS_SERVER) < 0) +#if GNUTLS_VERSION_NUMBER >= 0x030605 + if (wsi->a.vhost->options & LWS_SERVER_OPTION_ALLOW_EARLY_DATA) { + flags |= GNUTLS_ENABLE_EARLY_DATA; +#if defined(LWS_ROLE_QUIC) && GNUTLS_VERSION_NUMBER >= 0x030702 + extern const struct lws_role_ops role_ops_quic; + if (wsi->role_ops == &role_ops_quic) { + flags |= GNUTLS_NO_END_OF_EARLY_DATA; + lwsl_notice("gnutls_init: enabling server 0-RTT with GNUTLS_NO_END_OF_EARLY_DATA\n"); + } else +#endif + lwsl_notice("gnutls_init: enabling server 0-RTT/early data\n"); + } +#endif + if (gnutls_init(&session, flags) < 0) + return 1; + // gnutls_global_set_log_level(99); + // gnutls_global_set_log_function(my_gnutls_log); + if (0) return 1; +#if GNUTLS_VERSION_NUMBER >= 0x030605 + if (flags & GNUTLS_ENABLE_EARLY_DATA) { + gnutls_record_set_max_early_data_size(session, wsi->a.context->quic_0rtt_max_size ? wsi->a.context->quic_0rtt_max_size : 0xFFFFFFFF); + if (wsi->a.vhost->tls.anti_replay) + gnutls_anti_replay_enable(session, (gnutls_anti_replay_t)wsi->a.vhost->tls.anti_replay); + } +#endif + + // gnutls_global_set_log_level(99); + // gnutls_global_set_log_function(my_gnutls_log); + wsi->tls.ssl = (lws_tls_conn *)session; wsi->tls.ctx_ref = lws_tls_ctx_ref_get(wsi->a.vhost); @@ -349,7 +440,25 @@ lws_ssl_client_bio_create(struct lws *wsi) p++; } - if (gnutls_init(&session, GNUTLS_CLIENT) < 0) + unsigned int flags = GNUTLS_CLIENT; +#if GNUTLS_VERSION_NUMBER >= 0x030605 + if (wsi->flags & LCCSCF_ALLOW_EARLY_DATA) { + flags |= GNUTLS_ENABLE_EARLY_DATA; +#if defined(LWS_ROLE_QUIC) && GNUTLS_VERSION_NUMBER >= 0x030702 + extern const struct lws_role_ops role_ops_quic; + if (wsi->role_ops == &role_ops_quic) { + flags |= GNUTLS_NO_END_OF_EARLY_DATA; + lwsl_notice("gnutls_init: enabling client 0-RTT with GNUTLS_NO_END_OF_EARLY_DATA\n"); + } else +#endif + lwsl_notice("gnutls_init: enabling client 0-RTT/early data\n"); + } +#endif + if (gnutls_init(&session, flags) < 0) + return 1; + // gnutls_global_set_log_level(99); + // gnutls_global_set_log_function(my_gnutls_log); + if (0) return 1; wsi->tls.ssl = (lws_tls_conn *)session; @@ -370,7 +479,26 @@ lws_ssl_client_bio_create(struct lws *wsi) lws_tls_reuse_session(wsi); #endif - if (wsi->a.vhost->tls.alpn_ctx.len) { + if (wsi->alpn[0]) { + gnutls_datum_t alpn[4]; + unsigned int i = 0; + char *p = wsi->alpn; + char *end = p + strlen(p); + + while (p < end && i < 4) { + char *comma = strchr(p, ','); + alpn[i].data = (uint8_t *)p; + if (comma) { + alpn[i].size = (unsigned int)lws_ptr_diff_size_t(comma, p); + p = comma + 1; + } else { + alpn[i].size = (unsigned int)lws_ptr_diff_size_t(end, p); + p = end; + } + i++; + } + gnutls_alpn_set_protocols(session, alpn, i, 0); + } else if (wsi->a.vhost->tls.alpn_ctx.len) { gnutls_datum_t alpn[4]; unsigned int i = 0, p = 0; while (p < wsi->a.vhost->tls.alpn_ctx.len && i < 4) { @@ -389,6 +517,21 @@ lws_ssl_client_bio_create(struct lws *wsi) void lws_ssl_SSL_CTX_destroy(struct lws_vhost *vhost) { +#if defined(LWS_WITH_SERVER) && GNUTLS_VERSION_NUMBER >= 0x030605 + if (vhost->tls.anti_replay) { + lws_dll2_owner_t *owner = (lws_dll2_owner_t *)vhost->tls.anti_replay_owner; + if (owner) { + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, lws_dll2_get_head(owner)) { + lws_dll2_remove(d); + lws_free(lws_container_of(d, struct lws_gnutls_ar_entry, list)); + } lws_end_foreach_dll_safe(d, d1); + lws_free(owner); + } + gnutls_anti_replay_deinit((gnutls_anti_replay_t)vhost->tls.anti_replay); + vhost->tls.anti_replay = NULL; + vhost->tls.anti_replay_owner = NULL; + } +#endif if (vhost->tls.ssl_ctx) { lws_tls_vhost_backend_free_ctx(vhost->tls.ssl_ctx); vhost->tls.ssl_ctx = NULL; @@ -623,18 +766,23 @@ lws_tls_session_is_reused(struct lws *wsi) return (int)gnutls_session_is_resumed((gnutls_session_t)wsi->tls.ssl); } +#if !defined(LWS_WITH_TLS_SESSIONS) int lws_tls_session_dump_save(struct lws_vhost *vh, const char *host, uint16_t port, - int (*cb)(struct lws_context *, struct lws_tls_session_dump *), void *user) + lws_tls_sess_cb_t cb_save, void *opq) { return -1; } int lws_tls_session_dump_load(struct lws_vhost *vh, const char *host, uint16_t port, - int (*cb)(struct lws_context *, struct lws_tls_session_dump *), void *user) + lws_tls_sess_cb_t cb_load, void *opq) { return -1; } +#endif + + + #endif diff --git a/lib/tls/gnutls/lws-genaes.c b/lib/tls/gnutls/lws-genaes.c index 0b7c4b7890..88a7450bcc 100644 --- a/lib/tls/gnutls/lws-genaes.c +++ b/lib/tls/gnutls/lws-genaes.c @@ -55,6 +55,13 @@ lws_genaes_create(struct lws_genaes_ctx *ctx, enum enum_aes_operation op, case LWS_GAESM_CFB8: if (key.size == 16) alg = GNUTLS_CIPHER_AES_128_CFB8; break; + case LWS_GAESM_CTR: + switch (key.size) { + case 16: alg = GNUTLS_CIPHER_AES_128_CBC; break; + case 24: alg = GNUTLS_CIPHER_AES_192_CBC; break; + case 32: alg = GNUTLS_CIPHER_AES_256_CBC; break; + } + break; case LWS_GAESM_GCM: switch (key.size) { case 16: alg = GNUTLS_CIPHER_AES_128_GCM; break; @@ -182,6 +189,54 @@ lws_genaes_crypt(struct lws_genaes_ctx *ctx, const uint8_t *in, size_t len, uint8_t *stream_block_16, size_t *nc_or_iv_off, int taglen) { int n; + (void)n; + + if (ctx->mode == LWS_GAESM_CTR) { + uint8_t counter[16]; + uint8_t keystream[16]; + size_t offset = 0; + + if (!iv_or_nonce_ctr_or_data_unit_16) { + lwsl_err("%s: CTR mode requires counter IV\n", __func__); + return -1; + } + + memcpy(counter, iv_or_nonce_ctr_or_data_unit_16, 16); + + while (offset < len) { + size_t block_len = len - offset; + if (block_len > 16) + block_len = 16; + + /* Reset CBC IV to zero for single block encryption (ECB mode emulation) */ + uint8_t zero_iv[16] = {0}; + gnutls_cipher_set_iv(ctx->ctx, zero_iv, 16); + + /* Encrypt the counter block */ + if (gnutls_cipher_encrypt2(ctx->ctx, counter, 16, keystream, 16) < 0) { + lwsl_err("%s: CTR block encryption failed\n", __func__); + return -1; + } + + /* XOR with input */ + for (size_t i = 0; i < block_len; i++) { + out[offset + i] = in[offset + i] ^ keystream[i]; + } + + /* Increment counter (128-bit big-endian) */ + for (int i = 15; i >= 0; i--) { + counter[i]++; + if (counter[i] != 0) + break; + } + + offset += block_len; + } + + /* Save updated counter back to IV buffer */ + memcpy(iv_or_nonce_ctr_or_data_unit_16, counter, 16); + return 0; + } if (ctx->mode == LWS_GAESM_CBC || ctx->mode == LWS_GAESM_ECB) { if (!(ctx->op == LWS_GAESO_ENC && ctx->padding == LWS_GAESP_WITH_PADDING)) { diff --git a/lib/tls/gnutls/lws-gendtls.c b/lib/tls/gnutls/lws-gendtls.c index 882811b095..b621e519ba 100644 --- a/lib/tls/gnutls/lws-gendtls.c +++ b/lib/tls/gnutls/lws-gendtls.c @@ -134,10 +134,11 @@ lws_gendtls_create(struct lws_gendtls_ctx *ctx, goto bail; } -#if defined(GNUTLS_SRTP_AES128_CM_HMAC_SHA1_80) /* SRTP is supported in GnuTLS >= 3.1.4 */ +#if 1 /* SRTP is supported in GnuTLS >= 3.1.4 */ if (info->use_srtp) { gnutls_srtp_profile_t profiles[4]; int n = 0; + int i; if ((char *)strstr(info->use_srtp, "SRTP_AES128_CM_SHA1_80")) profiles[n++] = GNUTLS_SRTP_AES128_CM_HMAC_SHA1_80; @@ -148,10 +149,10 @@ lws_gendtls_create(struct lws_gendtls_ctx *ctx, if ((char *)strstr(info->use_srtp, "SRTP_NULL_HMAC_SHA1_32")) profiles[n++] = GNUTLS_SRTP_NULL_HMAC_SHA1_32; - if (n) { - ret = gnutls_srtp_set_profile_direct(ctx->session, profiles, n); + for (i = 0; i < n; i++) { + ret = gnutls_srtp_set_profile(ctx->session, profiles[i]); if (ret != GNUTLS_E_SUCCESS) { - lwsl_err("%s: gnutls_srtp_set_profile_direct failed: %s\n", + lwsl_err("%s: gnutls_srtp_set_profile failed: %s\n", __func__, gnutls_strerror(ret)); goto bail; } @@ -244,8 +245,9 @@ lws_gendtls_set_cert_mem(struct lws_gendtls_ctx *ctx, const uint8_t *cert, size_ if (ctx->key_mem) { gnutls_datum_t c = { ctx->cert_mem, (unsigned int)ctx->cert_len - 1 }; gnutls_datum_t k = { ctx->key_mem, (unsigned int)ctx->key_len - 1 }; + gnutls_x509_crt_fmt_t fmt = (ctx->cert_mem[0] == '-') ? GNUTLS_X509_FMT_PEM : GNUTLS_X509_FMT_DER; - if (gnutls_certificate_set_x509_key_mem(ctx->cred, &c, &k, GNUTLS_X509_FMT_PEM) < 0) { + if (gnutls_certificate_set_x509_key_mem(ctx->cred, &c, &k, fmt) < 0) { lwsl_err("%s: failed to set cert/key\n", __func__); return -1; } @@ -271,8 +273,9 @@ lws_gendtls_set_key_mem(struct lws_gendtls_ctx *ctx, const uint8_t *key, size_t if (ctx->cert_mem) { gnutls_datum_t c = { ctx->cert_mem, (unsigned int)ctx->cert_len - 1 }; gnutls_datum_t k = { ctx->key_mem, (unsigned int)ctx->key_len - 1 }; + gnutls_x509_crt_fmt_t fmt = (ctx->cert_mem[0] == '-') ? GNUTLS_X509_FMT_PEM : GNUTLS_X509_FMT_DER; - if (gnutls_certificate_set_x509_key_mem(ctx->cred, &c, &k, GNUTLS_X509_FMT_PEM) < 0) { + if (gnutls_certificate_set_x509_key_mem(ctx->cred, &c, &k, fmt) < 0) { lwsl_err("%s: failed to set cert/key\n", __func__); return -1; } @@ -431,10 +434,10 @@ lws_gendtls_is_clean(struct lws_gendtls_ctx *ctx) const char * lws_gendtls_get_srtp_profile(struct lws_gendtls_ctx *ctx) { -#if defined(GNUTLS_SRTP_AES128_CM_HMAC_SHA1_80) +#if 1 gnutls_srtp_profile_t profile = 0; - if (gnutls_srtp_get_profile(ctx->session, &profile) != GNUTLS_E_SUCCESS) + if (gnutls_srtp_get_selected_profile(ctx->session, &profile) != GNUTLS_E_SUCCESS) return NULL; switch (profile) { diff --git a/lib/tls/mbedtls/mbedtls-quic.c b/lib/tls/mbedtls/mbedtls-quic.c index cd02470abc..d8b0b5ba60 100644 --- a/lib/tls/mbedtls/mbedtls-quic.c +++ b/lib/tls/mbedtls/mbedtls-quic.c @@ -118,7 +118,7 @@ mbedtls_quic_set_traffic_secrets(mbedtls_ssl_context *ssl, struct lws *wsi = (struct lws *)mbedtls_ssl_get_user_data_p(ssl); enum lws_tls_quic_secret_type ct, st; - if (!wsi || !wsi->tls.quic_secret_cb) + if (!wsi || !wsi->tls.quic_secret_cb || secret_len > 48) return 0; if (wsi->tls.quic_secret_cb == (lws_tls_quic_secret_cb)1) diff --git a/lib/tls/mbedtls/mbedtls-server.c b/lib/tls/mbedtls/mbedtls-server.c index a24e0fe356..71edfdc0f2 100644 --- a/lib/tls/mbedtls/mbedtls-server.c +++ b/lib/tls/mbedtls/mbedtls-server.c @@ -131,9 +131,16 @@ lws_tls_server_certs_load(struct lws_vhost *vhost, struct lws *wsi, uint8_t *p = NULL; int n; - if ((!cert || !private_key) && (!mem_cert || !mem_privkey)) { - lwsl_notice("%s: no usable input\n", __func__); - return 0; + char resolved_cert[256]; + char resolved_key[256]; + + if (cert && private_key) { + if (lws_tls_resolve_grace_period_certs(vhost->context, cert, private_key, + resolved_cert, sizeof(resolved_cert), + resolved_key, sizeof(resolved_key)) == 0) { + cert = resolved_cert; + private_key = resolved_key; + } } n = (int)lws_tls_generic_cert_checks(vhost, cert, private_key); diff --git a/lib/tls/openssl/openssl-quic.c b/lib/tls/openssl/openssl-quic.c index df15384aa2..2d2ccf3fc0 100644 --- a/lib/tls/openssl/openssl-quic.c +++ b/lib/tls/openssl/openssl-quic.c @@ -44,8 +44,8 @@ set_encryption_secrets(WOLFSSL *ssl, enum wolfssl_encryption_level_t level, struct lws *wsi = (struct lws *)SSL_get_app_data((SSL *)ssl); enum lws_tls_quic_secret_type rt, wt; - if (!wsi) - return 1; + if (!wsi || secret_len > 48) + return 0; switch (level) { case wolfssl_encryption_early_data: @@ -147,8 +147,8 @@ set_read_secret(SSL *ssl, enum ssl_encryption_level_t level, struct lws *wsi = (struct lws *)SSL_get_app_data(ssl); enum lws_tls_quic_secret_type t; - if (!wsi) - return 1; + if (!wsi || secret_len > 48) + return 0; switch (level) { case ssl_encryption_early_data: @@ -178,8 +178,8 @@ set_write_secret(SSL *ssl, enum ssl_encryption_level_t level, struct lws *wsi = (struct lws *)SSL_get_app_data(ssl); enum lws_tls_quic_secret_type t; - if (!wsi) - return 1; + if (!wsi || secret_len > 48) + return 0; switch (level) { case ssl_encryption_early_data: @@ -340,7 +340,7 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, if (wsi->tls.quic_secret_cb == test_secret_cb) { wsi->tls.quic_tp_send = out; - wsi->tls.quic_tp_recv_len = *out_len; + wsi->tls.quic_tp_recv_len = out_len ? *out_len : 0; wsi->tls.quic_tp_recv = NULL; } else { if (out_len) @@ -427,26 +427,33 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, return 0; } - int lws_tls_quic_set_transport_parameters(struct lws *wsi, const uint8_t *tp, size_t tp_len) { - wsi->tls.quic_tp_send = tp; + if (wsi->tls.quic_tp_send) { + lws_free((void *)wsi->tls.quic_tp_send); + wsi->tls.quic_tp_send = NULL; + } + + uint8_t *p = lws_malloc(tp_len, "quic tp"); + if (!p) + return -1; + memcpy(p, tp, tp_len); + wsi->tls.quic_tp_send = p; wsi->tls.quic_tp_send_len = tp_len; if (!wsi->tls.ssl) return 0; #if defined(USE_WOLFSSL) - if (wolfSSL_set_quic_transport_params(wsi->tls.ssl, tp, tp_len) != 1) + if (wolfSSL_set_quic_transport_params(wsi->tls.ssl, p, tp_len) != 1) return -1; #else - if (SSL_set_quic_transport_params(wsi->tls.ssl, tp, tp_len) != 1) + if (SSL_set_quic_transport_params(wsi->tls.ssl, p, tp_len) != 1) return -1; #endif return 0; } - int lws_tls_quic_get_transport_parameters(struct lws *wsi, const uint8_t **tp, size_t *tp_len) { @@ -529,7 +536,7 @@ static void openssl_quic_keylog_cb(const SSL *ssl, const char *line) { struct lws *wsi = (struct lws *)SSL_get_app_data(ssl); - enum lws_tls_quic_secret_type type; + enum lws_tls_quic_secret_type type = (enum lws_tls_quic_secret_type)-1; const char *secret_hex = NULL; uint8_t secret[64]; size_t len = 0; @@ -554,7 +561,7 @@ openssl_quic_keylog_cb(const SSL *ssl, const char *line) secret_hex = (char *)strchr(line + 24, ' '); } - if (!secret_hex) + if (!secret_hex || (int)type == -1) return; secret_hex++; /* skip space */ @@ -565,6 +572,7 @@ openssl_quic_keylog_cb(const SSL *ssl, const char *line) } wsi->tls.quic_secret_cb(wsi, type, secret, len); + lws_explicit_bzero(secret, sizeof(secret)); } int @@ -608,17 +616,10 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) SSL_set_app_data(wsi->tls.ssl, wsi); if (lwsi_role_client(wsi)) { - if (wsi->flags & LCCSCF_ALLOW_EARLY_DATA) { -#if !defined(USE_WOLFSSL) && !defined(LWS_WITH_MBEDTLS) - SSL_set_early_data_enabled(wsi->tls.ssl, 1); -#endif - } SSL_set_connect_state(wsi->tls.ssl); } else { if (wsi->a.vhost && (wsi->a.vhost->options & LWS_SERVER_OPTION_ALLOW_EARLY_DATA)) { -#if !defined(USE_WOLFSSL) && !defined(LWS_WITH_MBEDTLS) - SSL_set_early_data_enabled(wsi->tls.ssl, 1); -#endif + SSL_set_max_early_data(wsi->tls.ssl, wsi->a.context->quic_0rtt_max_size ? wsi->a.context->quic_0rtt_max_size : 0xFFFFFFFF); } SSL_set_accept_state(wsi->tls.ssl); } @@ -653,6 +654,18 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, written = (size_t)read_n; *out_len = written; lwsl_info("QUIC TLS: BIO_read extracted %d bytes of TX data\n", (int)written); + } else { + size_t pending = (size_t)BIO_ctrl_pending(wbio); + if (pending > 0) { + uint8_t *exact_buf = lws_malloc(pending, "quic tx exact"); + if (exact_buf) { + int read_n = BIO_read(wbio, exact_buf, (int)pending); + if (read_n > 0) { + lws_tls_quic_tx_crypto_cb(wsi, level, exact_buf, (size_t)read_n); + } + lws_free(exact_buf); + } + } } if (hs_n <= 0) { @@ -675,7 +688,16 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, int lws_tls_quic_set_transport_parameters(struct lws *wsi, const uint8_t *tp, size_t tp_len) { - wsi->tls.quic_tp_send = tp; + if (wsi->tls.quic_tp_send) { + lws_free((void *)wsi->tls.quic_tp_send); + wsi->tls.quic_tp_send = NULL; + } + + uint8_t *p = lws_malloc(tp_len, "quic tp"); + if (!p) + return -1; + memcpy(p, tp, tp_len); + wsi->tls.quic_tp_send = p; wsi->tls.quic_tp_send_len = tp_len; return 0; } diff --git a/lib/tls/openssl/openssl-server.c b/lib/tls/openssl/openssl-server.c index ec4843655c..90a7f183e0 100644 --- a/lib/tls/openssl/openssl-server.c +++ b/lib/tls/openssl/openssl-server.c @@ -193,6 +193,18 @@ lws_tls_server_certs_load(struct lws_vhost *vhost, struct lws *wsi, #if OPENSSL_VERSION_NUMBER >= 0x10100000L int ret; #endif + char resolved_cert[256]; + char resolved_key[256]; + + if (cert && private_key) { + if (lws_tls_resolve_grace_period_certs(vhost->context, cert, private_key, + resolved_cert, sizeof(resolved_cert), + resolved_key, sizeof(resolved_key)) == 0) { + cert = resolved_cert; + private_key = resolved_key; + } + } + int n = (int)lws_tls_generic_cert_checks(vhost, cert, private_key), m; if (!cert && !private_key) diff --git a/lib/tls/openssl/openssl-ssl.c b/lib/tls/openssl/openssl-ssl.c index 140234ff01..ab771ad578 100644 --- a/lib/tls/openssl/openssl-ssl.c +++ b/lib/tls/openssl/openssl-ssl.c @@ -238,7 +238,7 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) { unsigned int ms = (unsigned int)((lws_now_usecs() - _lws_start) / 1000); if (ms > 2) { - lws_latency_note(&wsi->a.context->pt[(int)wsi->tsi], _lws_start, 2000, "SSL_read:%dms", ms); + lws_latency_note(pt, _lws_start, 2000, "SSL_read:%dms", ms); } } #endif @@ -381,6 +381,10 @@ lws_ssl_pending(struct lws *wsi) int lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) { +#if defined(LWS_WITH_LATENCY) + struct lws_context *context = wsi->a.context; + struct lws_context_per_thread *pt = &context->pt[(int)wsi->tsi]; +#endif int n, m; #if defined(LWS_TLS_LOG_PLAINTEXT_TX) @@ -404,7 +408,7 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) n = SSL_write(wsi->tls.ssl, buf, (int)(ssize_t)len); unsigned int ms = (unsigned int)((lws_now_usecs() - _lws_start) / 1000); if (ms > 2) { - lws_latency_note(&wsi->a.context->pt[(int)wsi->tsi], _lws_start, 2000, "SSL_write:%dms", ms); + lws_latency_note(pt, _lws_start, 2000, "SSL_write:%dms", ms); } } #else @@ -525,6 +529,15 @@ lws_ssl_close(struct lws *wsi) SSL_free(wsi->tls.ssl); wsi->tls.ssl = NULL; + if (wsi->tls.quic_tp_recv) { + lws_free((void *)wsi->tls.quic_tp_recv); + wsi->tls.quic_tp_recv = NULL; + } + if (wsi->tls.quic_tp_send) { + lws_free((void *)wsi->tls.quic_tp_send); + wsi->tls.quic_tp_send = NULL; + } + lws_tls_restrict_return(wsi); // lwsl_notice("%s: ssl restr %d, simul %d\n", __func__, diff --git a/lib/tls/private-lib-tls.h b/lib/tls/private-lib-tls.h index 6f2c0498ec..fc7fb18365 100644 --- a/lib/tls/private-lib-tls.h +++ b/lib/tls/private-lib-tls.h @@ -320,6 +320,16 @@ lws_tls_session_tag_discrete(const char *vhname, const char *host, int lws_tls_session_tag_from_wsi(struct lws *wsi, char *buf, size_t len); +int +lws_tls_resolve_grace_period_certs(struct lws_context *context, + const char *certpath, const char *keypath, + char *resolved_cert, size_t resolved_cert_len, + char *resolved_key, size_t resolved_key_len); + +int +lws_tls_cert_get_x509_validity(struct lws_context *context, const char *filepath, + time_t *not_before, time_t *not_after); + #else /* ! WITH_TLS */ #define lws_tls_restrict_borrow(xxx) (0) diff --git a/lib/tls/private-network.h b/lib/tls/private-network.h index b780ca1aa1..fd0afd0ae3 100644 --- a/lib/tls/private-network.h +++ b/lib/tls/private-network.h @@ -102,6 +102,11 @@ struct lws_vhost_tls { uint32_t tls_session_cache_ttl; #endif +#if defined(LWS_WITH_GNUTLS) + struct gnutls_anti_replay_st *anti_replay; + void *anti_replay_owner; +#endif + unsigned int user_supplied_ssl_ctx:1; unsigned int skipped_certs:1; }; diff --git a/lib/tls/schannel/schannel-quic.c b/lib/tls/schannel/schannel-quic.c index 2fd28e9788..626b49fb31 100644 --- a/lib/tls/schannel/schannel-quic.c +++ b/lib/tls/schannel/schannel-quic.c @@ -175,11 +175,16 @@ int lws_tls_quic_set_transport_parameters(struct lws *wsi, const uint8_t *tp, size_t tp_len) { #if defined(SECPKG_ATTR_APPLICATION_PROTOCOL) || defined(SECPKG_ATTR_APP_DATA) - /* - * Transport parameter exchange on Windows using Schannel for QUIC - * typically requires MsQuic or newer Windows 11 / Server 2022 APIs. - */ - wsi->tls.quic_tp_send = tp; + if (wsi->tls.quic_tp_send) { + lws_free((void *)wsi->tls.quic_tp_send); + wsi->tls.quic_tp_send = NULL; + } + + uint8_t *p = lws_malloc(tp_len, "quic tp"); + if (!p) + return -1; + memcpy(p, tp, tp_len); + wsi->tls.quic_tp_send = p; wsi->tls.quic_tp_send_len = tp_len; return 0; #else @@ -278,7 +283,7 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, while (p && *p) { const char *comma = strchr(p, ','); - size_t item_len = comma ? (size_t)(comma - p) : strlen(p); + size_t item_len = comma ? lws_ptr_diff_size_t(comma, p) : strlen(p); if (item_len > 255 || (pData + item_len + 1 - alpn_u.buf) > 256) break; *pData++ = (uint8_t)item_len; memcpy(pData, p, item_len); @@ -322,7 +327,7 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, while (p && *p) { const char *comma = strchr(p, ','); - size_t item_len = comma ? (size_t)(comma - p) : strlen(p); + size_t item_len = comma ? lws_ptr_diff_size_t(comma, p) : strlen(p); if (item_len > 255 || (pData + item_len + 1 - alpn_u.buf) > 256) break; *pData++ = (uint8_t)item_len; memcpy(pData, p, item_len); @@ -546,6 +551,11 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, split_offset = secrets->MsgSequenceStart; } + if (secrets->TrafficSecretSize > 48) { + lwsl_err("%s: TrafficSecretSize %zu exceeds 48\n", __func__, secrets->TrafficSecretSize); + return -1; + } + if (wsi->tls.quic_secret_cb(wsi, mapped_type, secrets->TrafficSecret, secrets->TrafficSecretSize) < 0) { lwsl_err("%s: quic_secret_cb failed for type %d\n", __func__, mapped_type); return -1; @@ -556,17 +566,27 @@ lws_tls_quic_advance_handshake(struct lws *wsi, int level, } } - if (split_offset > 0 && split_offset < *out_len) { - size_t remainder = *out_len - split_offset; + if (out && out_len) { + if (split_offset > 0 && split_offset < *out_len) { + size_t remainder = *out_len - split_offset; - /* - * We return the Initial bytes back to the caller in `out`, but we must - * instantly push the Handshake bytes into the QUIC layer at Level 2, - * because the caller does not loop to pull them! - */ - lws_tls_quic_tx_crypto_cb(wsi, 2 /* LWS_QUIC_LEVEL_HANDSHAKE */, out + split_offset, remainder); + /* + * We return the Initial bytes back to the caller in `out`, but we must + * instantly push the Handshake bytes into the QUIC layer at Level 2, + * because the caller does not loop to pull them! + */ + lws_tls_quic_tx_crypto_cb(wsi, 2 /* LWS_QUIC_LEVEL_HANDSHAKE */, out + split_offset, remainder); - *out_len = split_offset; + *out_len = split_offset; + } + } else if (out_bufs[0].cbBuffer && out_bufs[0].pvBuffer) { + size_t len = out_bufs[0].cbBuffer; + if (split_offset > 0 && split_offset < len) { + size_t remainder = len - split_offset; + lws_tls_quic_tx_crypto_cb(wsi, 2 /* LWS_QUIC_LEVEL_HANDSHAKE */, (uint8_t *)out_bufs[0].pvBuffer + split_offset, remainder); + len = split_offset; + } + lws_tls_quic_tx_crypto_cb(wsi, level, out_bufs[0].pvBuffer, len); } #else diff --git a/lib/tls/schannel/schannel-ssl.c b/lib/tls/schannel/schannel-ssl.c index 7ebe5776be..adf2567920 100644 --- a/lib/tls/schannel/schannel-ssl.c +++ b/lib/tls/schannel/schannel-ssl.c @@ -151,7 +151,7 @@ lws_tls_client_connect(struct lws *wsi, char *errbuf, size_t len) while (p < end) { const char *comma = strchr(p, ','); size_t item_len; - if (comma) item_len = comma - p; + if (comma) item_len = lws_ptr_diff_size_t(comma, p); else item_len = strlen(p); if (item_len > 0 && item_len < 256) { @@ -417,7 +417,7 @@ lws_tls_server_accept(struct lws *wsi) while (p && *p) { const char *comma = strchr(p, ','); - size_t item_len = comma ? (size_t)(comma - p) : strlen(p); + size_t item_len = comma ? lws_ptr_diff_size_t(comma, p) : strlen(p); if (item_len > 255 || (pData + item_len + 1 - alpn_buf) > 256) break; *pData++ = (uint8_t)item_len; memcpy(pData, p, item_len); @@ -897,6 +897,15 @@ lws_ssl_close(struct lws *wsi) wsi->tls.ctx_ref = NULL; } + if (wsi->tls.quic_tp_recv) { + lws_free((void *)wsi->tls.quic_tp_recv); + wsi->tls.quic_tp_recv = NULL; + } + if (wsi->tls.quic_tp_send) { + lws_free((void *)wsi->tls.quic_tp_send); + wsi->tls.quic_tp_send = NULL; + } + return 0; } diff --git a/lib/tls/tls-network.c b/lib/tls/tls-network.c index b047c983fc..a17982e598 100644 --- a/lib/tls/tls-network.c +++ b/lib/tls/tls-network.c @@ -163,6 +163,43 @@ lws_tls_check_cert_lifetime(struct lws_vhost *v) union lws_tls_cert_info_results ir; int n; + /* Check if the active cert context needs rotation under grace period policy */ + if (v->tls.ssl_ctx && v->tls.cfg_alloc_cert_path && v->tls.cfg_key_path && + (strstr(v->tls.cfg_alloc_cert_path, "-latest.crt") || + strstr(v->tls.cfg_alloc_cert_path, "-latest-fullchain.crt"))) { + char resolved_cert[256]; + char resolved_key[256]; + time_t resolved_from = 0, resolved_to = 0; + union lws_tls_cert_info_results loaded_from, loaded_to; + + /* Resolve what certificate path should be active right now */ + if (lws_tls_resolve_grace_period_certs(v->context, + v->tls.cfg_alloc_cert_path, + v->tls.cfg_key_path, + resolved_cert, sizeof(resolved_cert), + resolved_key, sizeof(resolved_key)) == 0) { + /* Get validity of resolved cert file */ + if (lws_tls_cert_get_x509_validity(v->context, resolved_cert, + &resolved_from, &resolved_to) == 0) { + /* Get validity of currently loaded cert context */ + if (lws_tls_vhost_cert_info(v, LWS_TLS_CERT_INFO_VALIDITY_FROM, + &loaded_from, 0) == 0 && + lws_tls_vhost_cert_info(v, LWS_TLS_CERT_INFO_VALIDITY_TO, + &loaded_to, 0) == 0) { + if (resolved_from != loaded_from.time || + resolved_to != loaded_to.time) { + lwsl_notice("%s: Active certificate for vhost %s is out of date. Rotating dynamically.\n", + __func__, v->name); + lws_tls_cert_updated(v->context, + v->tls.cfg_alloc_cert_path, + v->tls.cfg_key_path, + NULL, 0, NULL, 0); + } + } + } + } + } + if (v->tls.ssl_ctx && !v->tls.skipped_certs) { if (now < 1542933698) /* Nov 23 2018 00:42 UTC */ diff --git a/lib/tls/tls-sessions.c b/lib/tls/tls-sessions.c index dc1eb8490c..574b8a06ad 100644 --- a/lib/tls/tls-sessions.c +++ b/lib/tls/tls-sessions.c @@ -62,6 +62,8 @@ lws_tls_session_tag_from_wsi(struct lws *wsi, char *buf, size_t len) lws_tls_session_tag_discrete(wsi->a.vhost->name, host, wsi->c_port, buf, len); + lwsl_notice("lws_tls_session_tag_from_wsi: generated tag '%s' for host '%s'\n", buf, host); + return 0; } diff --git a/lib/tls/tls.c b/lib/tls/tls.c index 2f388367bb..02f440257a 100644 --- a/lib/tls/tls.c +++ b/lib/tls/tls.c @@ -747,3 +747,284 @@ lws_tls_cert_get_x509_remaining(struct lws_context *context, const char *filepat return res; } + +int +lws_x509_cert_fingerprint(struct lws_x509_cert *x509, int type, + uint8_t *buf, size_t len) +{ + union lws_tls_cert_info_results *res; + struct lws_genhash_ctx hctx; + size_t hash_len; + uint8_t *der; + int ret = -1; + + hash_len = lws_genhash_size((enum lws_genhash_types)type); + if (!hash_len || len < hash_len) + return -1; + + der = lws_malloc(4096, "cert_fingerprint_der"); + if (!der) + return -1; + + res = (union lws_tls_cert_info_results *)der; + + if (lws_x509_info(x509, LWS_TLS_CERT_INFO_DER_RAW, res, 4096)) + goto bail; + + if (lws_genhash_init(&hctx, (enum lws_genhash_types)type)) + goto bail; + + if (lws_genhash_update(&hctx, res->ns.name, (size_t)res->ns.len)) { + lws_genhash_destroy(&hctx, NULL); + goto bail; + } + + if (lws_genhash_destroy(&hctx, buf)) + goto bail; + + ret = (int)hash_len; + +bail: + lws_free(der); + return ret; +} + +int +lws_tls_cert_get_x509_validity(struct lws_context *context, const char *filepath, + time_t *not_before, time_t *not_after) +{ + struct lws_x509_cert *x = NULL; + union lws_tls_cert_info_results cri, cri1; + uint8_t *p; + lws_filepos_t amount; + int res = -1; + + if (alloc_file(context, filepath, &p, &amount)) + return 1; + + p[amount] = '\0'; + + if (lws_x509_create(&x)) + goto bail; + + if (lws_x509_parse_from_pem(x, p, (size_t)amount)) + goto bail_destroy; + + if (!lws_x509_info(x, LWS_TLS_CERT_INFO_VALIDITY_FROM, &cri, 0) && + !lws_x509_info(x, LWS_TLS_CERT_INFO_VALIDITY_TO, &cri1, 0)) { + if (not_before) + *not_before = cri.time; + if (not_after) + *not_after = cri1.time; + res = 0; + } + +bail_destroy: + lws_x509_destroy(&x); +bail: + lws_free(p); + + return res; +} + +#if defined(LWS_WITH_DIR) +struct versioned_certs_scan { + const char *prefix; + const char *suffix; + char newest[256]; + char previous[256]; + int count; +}; + +static int +lws_tls_versioned_certs_cb(const char *dirpath, void *user, struct lws_dir_entry *lde) +{ + struct versioned_certs_scan *scan = (struct versioned_certs_scan *)user; + size_t len_prefix = strlen(scan->prefix); + size_t len_name = strlen(lde->name); + size_t len_suffix = strlen(scan->suffix); + + (void)dirpath; + + /* Filter files that start with prefix, end with suffix, and have YYYYMMDD-HHMMSS in between */ + if (lde->type == LDOT_FILE && + len_name == len_prefix + 16 + len_suffix && + !strncmp(lde->name, scan->prefix, len_prefix) && + !strcmp(lde->name + len_name - len_suffix, scan->suffix) && + lde->name[len_prefix] == '-') { + /* Check timestamp format: -YYYYMMDD-HHMMSS */ + const char *ts = lde->name + len_prefix + 1; + int i; + int ok = 1; + for (i = 0; i < 15; i++) { + if (i == 8) { + if (ts[i] != '-') { ok = 0; break; } + } else { + if (ts[i] < '0' || ts[i] > '9') { ok = 0; break; } + } + } + if (ok) { + /* Since lws_dir sorts alphabetically, each match is newer than the previous one */ + lws_strncpy(scan->previous, scan->newest, sizeof(scan->previous)); + lws_strncpy(scan->newest, lde->name, sizeof(scan->newest)); + scan->count++; + } + } + return 0; +} + +static void +lws_tls_find_versioned_certs(const char *filepath, char *dirpath, size_t dirpath_len, + char *newest, size_t newest_len, + char *previous, size_t previous_len) +{ + struct versioned_certs_scan scan; + char file_prefix[128]; + const char *suffix = NULL; + const char *p; + + newest[0] = '\0'; + previous[0] = '\0'; + dirpath[0] = '\0'; + + /* Find last separator to split path into directory and file */ + p = strrchr(filepath, '/'); +#if defined(WIN32) + if (!p) + p = strrchr(filepath, '\\'); +#endif + if (!p) + return; + + lws_strncpy(dirpath, filepath, lws_ptr_diff_size_t(p, filepath) + 2); + + /* Determine suffix */ + if (strstr(p, "-latest-fullchain.crt")) { + suffix = "-fullchain.crt"; + } else if (strstr(p, "-latest.crt")) { + suffix = ".crt"; + } else if (strstr(p, "-latest.key")) { + suffix = ".key"; + } else { + /* Not a versioned path suffix we support */ + return; + } + + /* Extract prefix */ + p++; /* skip separator */ + const char *latest_ptr = strstr(p, "-latest"); + if (!latest_ptr || lws_ptr_diff_size_t(latest_ptr, p) >= sizeof(file_prefix)) + return; + + lws_strncpy(file_prefix, p, lws_ptr_diff_size_t(latest_ptr, p) + 1); + + memset(&scan, 0, sizeof(scan)); + scan.prefix = file_prefix; + scan.suffix = suffix; + + lws_dir(dirpath, &scan, lws_tls_versioned_certs_cb); + + if (scan.newest[0]) + lws_snprintf(newest, newest_len, "%s%s", dirpath, scan.newest); + if (scan.previous[0]) + lws_snprintf(previous, previous_len, "%s%s", dirpath, scan.previous); +} +#endif + +int +lws_tls_resolve_grace_period_certs(struct lws_context *context, + const char *certpath, const char *keypath, + char *resolved_cert, size_t resolved_cert_len, + char *resolved_key, size_t resolved_key_len) +{ +#if defined(LWS_WITH_DIR) + char dirpath_cert[256], newest_cert[256], previous_cert[256]; + char dirpath_key[256], newest_key[256], previous_key[256]; + time_t now = time(NULL); + time_t newest_not_before = 0; + time_t previous_not_after = 0; +#if defined(LWS_WITH_NETWORK) && defined(LWS_WITH_FILE_OPS) + lws_system_policy_t *policy = NULL; +#endif + char d_path[1024]; + int grace_period = 900; /* 15 mins default */ + int fd_cfg; +#endif + + lws_strncpy(resolved_cert, certpath, resolved_cert_len); + lws_strncpy(resolved_key, keypath, resolved_key_len); + +#if defined(LWS_WITH_DIR) + /* Check if it is a versioned path */ + if (!strstr(certpath, "-latest.crt") && !strstr(certpath, "-latest-fullchain.crt")) + return 0; + + lws_tls_find_versioned_certs(certpath, dirpath_cert, sizeof(dirpath_cert), + newest_cert, sizeof(newest_cert), + previous_cert, sizeof(previous_cert)); + + lws_tls_find_versioned_certs(keypath, dirpath_key, sizeof(dirpath_key), + newest_key, sizeof(newest_key), + previous_key, sizeof(previous_key)); + + if (!newest_cert[0] || !newest_key[0]) + return 0; + + if (lws_tls_cert_get_x509_validity(context, newest_cert, &newest_not_before, NULL)) { + /* Failed to read newest cert validity, fallback to newest on disk */ + lws_strncpy(resolved_cert, newest_cert, resolved_cert_len); + lws_strncpy(resolved_key, newest_key, resolved_key_len); + return 0; + } + + lws_snprintf(d_path, sizeof(d_path), "/etc/lwsws/acme/acme_config.json"); +#if defined(LWS_WITH_NETWORK) && defined(LWS_WITH_FILE_OPS) + if (lws_system_parse_policy(context, "/etc/lwsws/policy", &policy) == 0 && policy) { + lws_snprintf(d_path, sizeof(d_path), "%s/acme_config.json", policy->dns_base_dir); + lws_system_policy_free(policy); + } +#endif + + fd_cfg = open(d_path, O_RDONLY); + if (fd_cfg >= 0) { + char buf[1024]; + ssize_t nr = read(fd_cfg, buf, sizeof(buf) - 1); + if (nr > 0) { + buf[nr] = '\0'; + const char *grace_ptr = strstr(buf, "\"rotation-grace-period\""); + if (!grace_ptr) + grace_ptr = strstr(buf, "\"rotation_grace_period\""); + if (grace_ptr) { + const char *num_ptr = strchr(grace_ptr, ':'); + if (num_ptr) { + num_ptr++; + while (*num_ptr == ' ' || *num_ptr == '\t') + num_ptr++; + if (*num_ptr >= '0' && *num_ptr <= '9') + grace_period = atoi(num_ptr); + } + } + } + close(fd_cfg); + } + + /* If previous cert exists and is still valid, and we are within the grace period of the new cert */ + if (previous_cert[0] && previous_key[0] && + !lws_tls_cert_get_x509_validity(context, previous_cert, NULL, &previous_not_after) && + now < previous_not_after && + now < newest_not_before + grace_period) { + lwsl_notice("%s: Deferring to previous cert %s (grace period: %llds left)\n", + __func__, previous_cert, (long long)(newest_not_before + grace_period - now)); + lws_strncpy(resolved_cert, previous_cert, resolved_cert_len); + lws_strncpy(resolved_key, previous_key, resolved_key_len); + } else { + lwsl_notice("%s: Using newest cert %s\n", __func__, newest_cert); + lws_strncpy(resolved_cert, newest_cert, resolved_cert_len); + lws_strncpy(resolved_key, newest_key, resolved_key_len); + } +#endif + + return 0; +} + + diff --git a/lwsws/main.c b/lwsws/main.c index a23dcf6afa..f929975000 100644 --- a/lwsws/main.c +++ b/lwsws/main.c @@ -187,6 +187,8 @@ context_creation(int argc, const char **argv) info.argc = argc; info.argv = argv; + lws_cmdline_option_handle_builtin(argc, argv, &info); + context = lws_create_context(&info); if (context == NULL) { lwsl_err("libwebsocket init failed\n"); diff --git a/minimal-examples-lowlevel/api-tests/api-test-qpack/CMakeLists.txt b/minimal-examples-lowlevel/api-tests/api-test-qpack/CMakeLists.txt index ccdb7ec809..f0f010789b 100644 --- a/minimal-examples-lowlevel/api-tests/api-test-qpack/CMakeLists.txt +++ b/minimal-examples-lowlevel/api-tests/api-test-qpack/CMakeLists.txt @@ -62,6 +62,7 @@ set(requirements 1) require_lws_config(LWS_WITH_HTTP3 1 requirements) require_lws_config(LWS_WITH_NETWORK 1 requirements) +require_lws_config(LWS_WITH_DIR 1 requirements) if (requirements) if (LWS_WITH_LS_QPACK) diff --git a/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd/CMakeLists.txt b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd/CMakeLists.txt new file mode 100644 index 0000000000..10102f2cf9 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd/CMakeLists.txt @@ -0,0 +1,37 @@ +project(lws-api-test-ws-h2-pmd C) +cmake_minimum_required(VERSION 3.10) +find_package(libwebsockets CONFIG REQUIRED) +list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR}) +include(CheckCSourceCompiles) +include(LwsCheckRequirements) + +set(SAMP lws-api-test-ws-h2-pmd) +set(SRCS main.c) + +set(requirements 1) +require_lws_config(LWS_ROLE_WS 1 requirements) +require_lws_config(LWS_ROLE_H2 1 requirements) +require_lws_config(LWS_WITH_CLIENT 1 requirements) +require_lws_config(LWS_WITH_SERVER 1 requirements) +require_lws_config(LWS_WITH_TLS 1 requirements) +require_lws_config(LWS_WITHOUT_EXTENSIONS 0 requirements) + +if (requirements) + add_executable(${SAMP} ${SRCS}) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared ${LIBWEBSOCKETS_DEP_LIBS}) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets ${LIBWEBSOCKETS_DEP_LIBS}) + endif() + + lws_get_free_port(PORT_WS_H2_PMD) + + add_test(NAME api-test-ws-h2-pmd COMMAND lws-api-test-ws-h2-pmd -p ${PORT_WS_H2_PMD}) + set_tests_properties(api-test-ws-h2-pmd + PROPERTIES + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd + TIMEOUT 60) + +endif() diff --git a/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd/main.c b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd/main.c new file mode 100644 index 0000000000..922693c586 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-pmd/main.c @@ -0,0 +1,423 @@ +/* + * lws-api-test-ws-h2-pmd + * + * Written in 2010-2026 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * Tests permessage-deflate over ws-over-h2 (RFC 8441) tx. + * + * A ws server vhost and an h2 ws client run in one process, both offering + * permessage-deflate. The server bulk-sends a 64KB deterministic pattern in + * 4KB messages, gated only on lws_send_pipe_choked(), like typical user code. + * Alternate messages are highly compressible (so the deflated frame is much + * smaller than the caller's payload) and incompressible (so the deflated + * output overflows pmd's default 1KB chunk buffer and the extension must + * drain across several POLLOUT services). + * + * That exercises the two encapsulated-tx bugs this test was written against: + * + * - lws_write() on a ws-over-h2 stream returned the POST-COMPRESSION frame + * size instead of the caller's payload length, so any well-compressed + * message made callers checking (wr < len) conclude a short write and + * kill the connection, and + * + * - ws->tx_draining_ext was never serviced for h2-encapsulated children + * (only rops_handle_POLLOUT_ws() does it, which mux children never + * reach), while lws_send_pipe_choked() reports choked during the drain, + * so the first message whose compressed output exceeded the pmd chunk + * buffer wedged the stream permanently. + * + * The test fails if + * - the negotiated connection is not actually ws-over-h2, + * - the client's upgrade did not offer permessage-deflate (the test would + * be vacuous), + * - any server lws_write() accepts fewer bytes than requested, + * - the received pattern is corrupted, or + * - the transfer doesn't complete in time (the drain wedge shows up here). + */ + +#include +#include +#include + +static int interrupted; +static int result = 1; +static struct lws_context *context; + +static const char * const test_cert = +"-----BEGIN CERTIFICATE-----\n" +"MIIF5jCCA86gAwIBAgIJANq50IuwPFKgMA0GCSqGSIb3DQEBCwUAMIGGMQswCQYD\n" +"VQQGEwJHQjEQMA4GA1UECAwHRXJld2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEb\n" +"MBkGA1UECgwSbGlid2Vic29ja2V0cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3Qx\n" +"HzAdBgkqhkiG9w0BCQEWEG5vbmVAaW52YWxpZC5vcmcwIBcNMTgwMzIwMDQxNjA3\n" +"WhgPMjExODAyMjQwNDE2MDdaMIGGMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRXJl\n" +"d2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEbMBkGA1UECgwSbGlid2Vic29ja2V0\n" +"cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEG5vbmVA\n" +"aW52YWxpZC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCjYtuW\n" +"aICCY0tJPubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8\n" +"Di3DAmHKnSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTek\n" +"LWcfI5ZZtoGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnH\n" +"KT/m6DSU0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6\n" +"jzhNyMBTJ1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQ\n" +"Ujy5N8pSNp7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAz\n" +"TK4l2pHNuC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBK\n" +"Izv9cgi9fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0\n" +"nPN1IMSnzXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzo\n" +"GMTvP/AuehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9p\n" +"sNcjTMaBQLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABo1MwUTAdBgNVHQ4EFgQU\n" +"9mYU23tW2zsomkKTAXarjr2vjuswHwYDVR0jBBgwFoAU9mYU23tW2zsomkKTAXar\n" +"jr2vjuswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEANjIBMrow\n" +"YNCbhAJdP7dhlhT2RUFRdeRUJD0IxrH/hkvb6myHHnK8nOYezFPjUlmRKUgNEDuA\n" +"xbnXZzPdCRNV9V2mShbXvCyiDY7WCQE2Bn44z26O0uWVk+7DNNLH9BnkwUtOnM9P\n" +"wtmD9phWexm4q2GnTsiL6Ul6cy0QlTJWKVLEUQQ6yda582e23J1AXqtqFcpfoE34\n" +"H3afEiGy882b+ZBiwkeV+oq6XVF8sFyr9zYrv9CvWTYlkpTQfLTZSsgPdEHYVcjv\n" +"xQ2D+XyDR0aRLRlvxUa9dHGFHLICG34Juq5Ai6lM1EsoD8HSsJpMcmrH7MWw2cKk\n" +"ujC3rMdFTtte83wF1uuF4FjUC72+SmcQN7A386BC/nk2TTsJawTDzqwOu/VdZv2g\n" +"1WpTHlumlClZeP+G/jkSyDwqNnTu1aodDmUa4xZodfhP1HWPwUKFcq8oQr148QYA\n" +"AOlbUOJQU7QwRWd1VbnwhDtQWXC92A2w1n/xkZSR1BM/NUSDhkBSUU1WjMbWg6Gg\n" +"mnIZLRerQCu1Oozr87rOQqQakPkyt8BUSNK3K42j2qcfhAONdRl8Hq8Qs5pupy+s\n" +"8sdCGDlwR3JNCMv6u48OK87F4mcIxhkSefFJUFII25pCGN5WtE4p5l+9cnO1GrIX\n" +"e2Hl/7M0c/lbZ4FvXgARlex2rkgS0Ka06HE=\n" +"-----END CERTIFICATE-----\n"; + +static const char * const test_key = +"-----BEGIN PRIVATE KEY-----\n" +"MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCjYtuWaICCY0tJ\n" +"PubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8Di3DAmHK\n" +"nSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTekLWcfI5ZZ\n" +"toGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnHKT/m6DSU\n" +"0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6jzhNyMBT\n" +"J1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQUjy5N8pS\n" +"Np7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAzTK4l2pHN\n" +"uC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBKIzv9cgi9\n" +"fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0nPN1IMSn\n" +"zXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzoGMTvP/Au\n" +"ehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9psNcjTMaB\n" +"QLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABAoICAFWe8MQZb37k2gdAV3Y6aq8f\n" +"qokKQqbCNLd3giGFwYkezHXoJfg6Di7oZxNcKyw35LFEghkgtQqErQqo35VPIoH+\n" +"vXUpWOjnCmM4muFA9/cX6mYMc8TmJsg0ewLdBCOZVw+wPABlaqz+0UOiSMMftpk9\n" +"fz9JwGd8ERyBsT+tk3Qi6D0vPZVsC1KqxxL/cwIFd3Hf2ZBtJXe0KBn1pktWht5A\n" +"Kqx9mld2Ovl7NjgiC1Fx9r+fZw/iOabFFwQA4dr+R8mEMK/7bd4VXfQ1o/QGGbMT\n" +"G+ulFrsiDyP+rBIAaGC0i7gDjLAIBQeDhP409ZhswIEc/GBtODU372a2CQK/u4Q/\n" +"HBQvuBtKFNkGUooLgCCbFxzgNUGc83GB/6IwbEM7R5uXqsFiE71LpmroDyjKTlQ8\n" +"YZkpIcLNVLw0usoGYHFm2rvCyEVlfsE3Ub8cFyTFk50SeOcF2QL2xzKmmbZEpXgl\n" +"xBHR0hjgon0IKJDGfor4bHO7Nt+1Ece8u2oTEKvpz5aIn44OeC5mApRGy83/0bvs\n" +"esnWjDE/bGpoT8qFuy+0urDEPNId44XcJm1IRIlG56ErxC3l0s11wrIpTmXXckqw\n" +"zFR9s2z7f0zjeyxqZg4NTPI7wkM3M8BXlvp2GTBIeoxrWB4V3YArwu8QF80QBgVz\n" +"mgHl24nTg00UH1OjZsABAoIBAQDOxftSDbSqGytcWqPYP3SZHAWDA0O4ACEM+eCw\n" +"au9ASutl0IDlNDMJ8nC2ph25BMe5hHDWp2cGQJog7pZ/3qQogQho2gUniKDifN77\n" +"40QdykllTzTVROqmP8+efreIvqlzHmuqaGfGs5oTkZaWj5su+B+bT+9rIwZcwfs5\n" +"YRINhQRx17qa++xh5mfE25c+M9fiIBTiNSo4lTxWMBShnK8xrGaMEmN7W0qTMbFH\n" +"PgQz5FcxRjCCqwHilwNBeLDTp/ZECEB7y34khVh531mBE2mNzSVIQcGZP1I/DvXj\n" +"W7UUNdgFwii/GW+6M0uUDy23UVQpbFzcV8o1C2nZc4Fb4zwBAoIBAQDKSJkFwwuR\n" +"naVJS6WxOKjX8MCu9/cKPnwBv2mmI2jgGxHTw5sr3ahmF5eTb8Zo19BowytN+tr6\n" +"2ZFoIBA9Ubc9esEAU8l3fggdfM82cuR9sGcfQVoCh8tMg6BP8IBLOmbSUhN3PG2m\n" +"39I802u0fFNVQCJKhx1m1MFFLOu7lVcDS9JN+oYVPb6MDfBLm5jOiPuYkFZ4gH79\n" +"J7gXI0/YKhaJ7yXthYVkdrSF6Eooer4RZgma62Dd1VNzSq3JBo6rYjF7Lvd+RwDC\n" +"R1thHrmf/IXplxpNVkoMVxtzbrrbgnC25QmvRYc0rlS/kvM4yQhMH3eA7IycDZMp\n" +"Y+0xm7I7jTT7AoIBAGKzKIMDXdCxBWKhNYJ8z7hiItNl1IZZMW2TPUiY0rl6yaCh\n" +"BVXjM9W0r07QPnHZsUiByqb743adkbTUjmxdJzjaVtxN7ZXwZvOVrY7I7fPWYnCE\n" +"fXCr4+IVpZI/ZHZWpGX6CGSgT6EOjCZ5IUufIvEpqVSmtF8MqfXO9o9uIYLokrWQ\n" +"x1dBl5UnuTLDqw8bChq7O5y6yfuWaOWvL7nxI8NvSsfj4y635gIa/0dFeBYZEfHI\n" +"UlGdNVomwXwYEzgE/c19ruIowX7HU/NgxMWTMZhpazlxgesXybel+YNcfDQ4e3RM\n" +"OMz3ZFiaMaJsGGNf4++d9TmMgk4Ns6oDs6Tb9AECggEBAJYzd+SOYo26iBu3nw3L\n" +"65uEeh6xou8pXH0Tu4gQrPQTRZZ/nT3iNgOwqu1gRuxcq7TOjt41UdqIKO8vN7/A\n" +"aJavCpaKoIMowy/aGCbvAvjNPpU3unU8jdl/t08EXs79S5IKPcgAx87sTTi7KDN5\n" +"SYt4tr2uPEe53NTXuSatilG5QCyExIELOuzWAMKzg7CAiIlNS9foWeLyVkBgCQ6S\n" +"me/L8ta+mUDy37K6vC34jh9vK9yrwF6X44ItRoOJafCaVfGI+175q/eWcqTX4q+I\n" +"G4tKls4sL4mgOJLq+ra50aYMxbcuommctPMXU6CrrYyQpPTHMNVDQy2ttFdsq9iK\n" +"TncCggEBAMmt/8yvPflS+xv3kg/ZBvR9JB1In2n3rUCYYD47ReKFqJ03Vmq5C9nY\n" +"56s9w7OUO8perBXlJYmKZQhO4293lvxZD2Iq4NcZbVSCMoHAUzhzY3brdgtSIxa2\n" +"gGveGAezZ38qKIU26dkz7deECY4vrsRkwhpTW0LGVCpjcQoaKvymAoCmAs8V2oMr\n" +"Ziw1YQ9uOUoWwOqm1wZqmVcOXvPIS2gWAs3fQlWjH9hkcQTMsUaXQDOD0aqkSY3E\n" +"NqOvbCV1/oUpRi3076khCoAXI1bKSn/AvR3KDP14B5toHI/F5OTSEiGhhHesgRrs\n" +"fBrpEY1IATtPq1taBZZogRqI3rOkkPk=\n" +"-----END PRIVATE KEY-----\n"; + +#define TEST_TOTAL 65536 +#define TEST_CHUNK 4096 + +static struct lws *client_wsi; +static size_t srv_sent; +static size_t cli_rx; +static int saw_pmd_offer; +static int port_tcp = 7681; +static lws_sorted_usec_list_t sul_timeout; + +static const struct lws_extension extensions[] = { + { + "permessage-deflate", + lws_extension_callback_pm_deflate, + "permessage-deflate" + "; client_no_context_takeover" + "; client_max_window_bits" + }, + { NULL, NULL, NULL /* terminator */ } +}; + +/* + * Deterministic pattern both sides can generate from the absolute byte + * offset: even 4KB blocks are a trivially-compressible letter cycle (the + * deflated frame is a fraction of the payload, catching a short lws_write() + * return), odd blocks are multiplicative-hash noise that deflate cannot + * shrink (the compressed output overflows pmd's 1KB chunk buffer, forcing + * the tx_draining_ext path). + */ +static uint8_t +pattern_byte(size_t o) +{ + uint32_t v; + + if (!((o / TEST_CHUNK) & 1)) + return (uint8_t)('A' + (o % 26)); + + v = (uint32_t)o * 2654435761u; + + return (uint8_t)(v ^ (v >> 11) ^ (v >> 22)); +} + +static void +sul_timeout_cb(lws_sorted_usec_list_t *sul) +{ + lwsl_err("--- timeout: rx %d / %d, sent %d ---\n", + (int)cli_rx, TEST_TOTAL, (int)srv_sent); + interrupted = 1; +} + +static int +callback_srv(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: { + char buf[256]; + + /* + * prove the client's upgrade actually offered pmd over the + * extended CONNECT, so a silently-dropped negotiation can't + * turn this into a test of nothing + */ + if (lws_hdr_copy(wsi, buf, sizeof(buf), + WSI_TOKEN_EXTENSIONS) > 0 && + strstr(buf, "permessage-deflate")) + saw_pmd_offer = 1; + break; + } + + case LWS_CALLBACK_ESTABLISHED: + lwsl_user("%s: server: established\n", __func__); + if (!saw_pmd_offer) { + lwsl_err("--- no permessage-deflate offer seen ---\n"); + interrupted = 1; + return -1; + } + srv_sent = 0; + lws_callback_on_writable(wsi); + break; + + case LWS_CALLBACK_SERVER_WRITEABLE: { + uint8_t buf[LWS_PRE + TEST_CHUNK]; + size_t n; + + /* + * bulk-send gated only on choked, like typical user code; + * while pmd is draining a big compressed message, choked + * reports 1 and the h2 child loop must advance the drain + * itself or we never come back here + */ + while (srv_sent < TEST_TOTAL && !lws_send_pipe_choked(wsi)) { + n = TEST_TOTAL - srv_sent; + if (n > TEST_CHUNK) + n = TEST_CHUNK; + for (size_t i = 0; i < n; i++) + buf[LWS_PRE + i] = pattern_byte(srv_sent + i); + int wr = lws_write(wsi, &buf[LWS_PRE], n, + LWS_WRITE_BINARY); + lwsl_info("%s: srv wrote %d of %d\n", __func__, wr, + (int)n); + if (wr < (int)n) { + lwsl_err("--- short write: accepted %d of %d " + "(post-compression size leaked?) ---\n", + wr, (int)n); + interrupted = 1; + return -1; + } + srv_sent += n; + } + if (srv_sent < TEST_TOTAL) + lws_callback_on_writable(wsi); + break; + } + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static int +callback_cli(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_CLIENT_ESTABLISHED: + lwsl_user("%s: client: established\n", __func__); + if (lws_get_network_wsi(wsi) == wsi) { + lwsl_err("--- not encapsulated in h2 ---\n"); + interrupted = 1; + return -1; + } + client_wsi = wsi; + break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + for (size_t i = 0; i < len; i++) + if (((uint8_t *)in)[i] != pattern_byte(cli_rx + i)) { + lwsl_err("--- pattern corrupt at ofs %d ---\n", + (int)(cli_rx + i)); + interrupted = 1; + return -1; + } + cli_rx += len; + + if (cli_rx == TEST_TOTAL) { + lwsl_user("--- transfer complete and intact. " + "Test passed. ---\n"); + result = 0; + interrupted = 1; + return -1; + } + break; + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + lwsl_err("--- client connection error: %s ---\n", + in ? (char *)in : "(null)"); + interrupted = 1; + break; + + case LWS_CALLBACK_CLIENT_CLOSED: + lwsl_user("%s: client: closed (rx %d sent %d)\n", __func__, + (int)cli_rx, (int)srv_sent); + client_wsi = NULL; + if (result) + interrupted = 1; + break; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static const struct lws_protocols protocols_srv[] = { + { "http", lws_callback_http_dummy, 0, 0, 0, NULL, 0 }, + { "pmdt", callback_srv, 0, TEST_CHUNK, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +static const struct lws_protocols protocols_cli[] = { + { "pmdt", callback_cli, 0, TEST_CHUNK, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + struct lws_context_creation_info info; + struct lws_client_connect_info i; + struct lws_vhost *vh; + const char *p; + int n = 0; + + lws_context_info_defaults(&info, NULL); + info.fd_limit_per_thread = 0; + lws_cmdline_option_handle_builtin(argc, argv, &info); + + if ((p = lws_cmdline_option(argc, argv, "-p"))) + port_tcp = atoi(p); + + signal(SIGINT, sigint_handler); + + lwsl_user("LWS API selftest: ws-over-h2 permessage-deflate\n"); + + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | + LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + /* ws server vhost, h2 alpn, pmd on offer */ + info.port = port_tcp; + info.vhost_name = "srv"; + info.alpn = "h2,http/1.1"; + info.protocols = protocols_srv; + info.extensions = extensions; + info.server_ssl_cert_mem = test_cert; + info.server_ssl_cert_mem_len = (unsigned int)strlen(test_cert); + info.server_ssl_private_key_mem = test_key; + info.server_ssl_private_key_mem_len = (unsigned int)strlen(test_key); + + vh = lws_create_vhost(context, &info); + if (!vh) { + lwsl_err("Failed to create server vhost\n"); + goto bail; + } + + /* client vhost, pmd on offer */ + info.port = CONTEXT_PORT_NO_LISTEN; + info.vhost_name = "cli"; + info.protocols = protocols_cli; + info.server_ssl_cert_mem = NULL; + info.server_ssl_cert_mem_len = 0; + info.server_ssl_private_key_mem = NULL; + info.server_ssl_private_key_mem_len = 0; + + vh = lws_create_vhost(context, &info); + if (!vh) { + lwsl_err("Failed to create client vhost\n"); + goto bail; + } + + memset(&i, 0, sizeof(i)); + i.context = context; + i.vhost = vh; + i.address = "127.0.0.1"; + i.port = port_tcp; + i.path = "/"; + i.host = "localhost"; + i.origin = "localhost"; + i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | + LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + i.alpn = "h2"; + i.protocol = "pmdt"; + i.local_protocol_name = "pmdt"; + + if (!lws_client_connect_via_info(&i)) { + lwsl_err("client connect failed\n"); + goto bail; + } + + lws_sul_schedule(context, 0, &sul_timeout, sul_timeout_cb, + 20 * LWS_US_PER_SEC); + + while (n >= 0 && !interrupted) + n = lws_service(context, 0); + +bail: + lws_context_destroy(context); + + lwsl_user("Completed: %s\n", result ? "FAIL" : "PASS"); + + return result; +} diff --git a/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit/CMakeLists.txt b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit/CMakeLists.txt new file mode 100644 index 0000000000..343beea838 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit/CMakeLists.txt @@ -0,0 +1,36 @@ +project(lws-api-test-ws-h2-txcredit C) +cmake_minimum_required(VERSION 3.10) +find_package(libwebsockets CONFIG REQUIRED) +list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR}) +include(CheckCSourceCompiles) +include(LwsCheckRequirements) + +set(SAMP lws-api-test-ws-h2-txcredit) +set(SRCS main.c) + +set(requirements 1) +require_lws_config(LWS_ROLE_WS 1 requirements) +require_lws_config(LWS_ROLE_H2 1 requirements) +require_lws_config(LWS_WITH_CLIENT 1 requirements) +require_lws_config(LWS_WITH_SERVER 1 requirements) +require_lws_config(LWS_WITH_TLS 1 requirements) + +if (requirements) + add_executable(${SAMP} ${SRCS}) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared ${LIBWEBSOCKETS_DEP_LIBS}) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets ${LIBWEBSOCKETS_DEP_LIBS}) + endif() + + lws_get_free_port(PORT_WS_H2_TXCR) + + add_test(NAME api-test-ws-h2-txcredit COMMAND lws-api-test-ws-h2-txcredit -p ${PORT_WS_H2_TXCR}) + set_tests_properties(api-test-ws-h2-txcredit + PROPERTIES + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit + TIMEOUT 60) + +endif() diff --git a/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit/main.c b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit/main.c new file mode 100644 index 0000000000..acaed35be1 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-ws-h2-txcredit/main.c @@ -0,0 +1,392 @@ +/* + * lws-api-test-ws-h2-txcredit + * + * Written in 2010-2026 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * Tests that a ws-over-h2 (RFC 8441) server respects the peer's h2 + * flow-control window on tx. + * + * A ws server vhost and an h2 ws client run in one process. The server + * bulk-sends a 64KB pattern in 1KB lws_write() chunks, gated only on + * lws_send_pipe_choked(), like typical user code. The client connects with + * LCCSCF_H2_MANUAL_RXFLOW and drip-feeds tx credit in 1KB WINDOW_UPDATEs, + * so the server is forced to hold DATA back (parking it inside the h2 role, + * splitting frames larger than the available credit) instead of overrunning + * the peer's window, which is a connection-fatal FLOW_CONTROL_ERROR with + * strict peers like nghttp2 / browsers. + * + * The test fails if + * - the negotiated connection is not actually ws-over-h2, + * - the client ever received more payload than it granted credit for + * (the ws frame headers ride inside DATA too, so payload > granted can + * only happen when the server ignored the window), or + * - the received pattern is corrupted / the transfer doesn't complete. + */ + +#include +#include +#include + +static int interrupted; +static int result = 1; +static struct lws_context *context; + +static const char * const test_cert = +"-----BEGIN CERTIFICATE-----\n" +"MIIF5jCCA86gAwIBAgIJANq50IuwPFKgMA0GCSqGSIb3DQEBCwUAMIGGMQswCQYD\n" +"VQQGEwJHQjEQMA4GA1UECAwHRXJld2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEb\n" +"MBkGA1UECgwSbGlid2Vic29ja2V0cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3Qx\n" +"HzAdBgkqhkiG9w0BCQEWEG5vbmVAaW52YWxpZC5vcmcwIBcNMTgwMzIwMDQxNjA3\n" +"WhgPMjExODAyMjQwNDE2MDdaMIGGMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRXJl\n" +"d2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEbMBkGA1UECgwSbGlid2Vic29ja2V0\n" +"cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEG5vbmVA\n" +"aW52YWxpZC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCjYtuW\n" +"aICCY0tJPubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8\n" +"Di3DAmHKnSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTek\n" +"LWcfI5ZZtoGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnH\n" +"KT/m6DSU0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6\n" +"jzhNyMBTJ1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQ\n" +"Ujy5N8pSNp7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAz\n" +"TK4l2pHNuC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBK\n" +"Izv9cgi9fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0\n" +"nPN1IMSnzXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzo\n" +"GMTvP/AuehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9p\n" +"sNcjTMaBQLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABo1MwUTAdBgNVHQ4EFgQU\n" +"9mYU23tW2zsomkKTAXarjr2vjuswHwYDVR0jBBgwFoAU9mYU23tW2zsomkKTAXar\n" +"jr2vjuswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEANjIBMrow\n" +"YNCbhAJdP7dhlhT2RUFRdeRUJD0IxrH/hkvb6myHHnK8nOYezFPjUlmRKUgNEDuA\n" +"xbnXZzPdCRNV9V2mShbXvCyiDY7WCQE2Bn44z26O0uWVk+7DNNLH9BnkwUtOnM9P\n" +"wtmD9phWexm4q2GnTsiL6Ul6cy0QlTJWKVLEUQQ6yda582e23J1AXqtqFcpfoE34\n" +"H3afEiGy882b+ZBiwkeV+oq6XVF8sFyr9zYrv9CvWTYlkpTQfLTZSsgPdEHYVcjv\n" +"xQ2D+XyDR0aRLRlvxUa9dHGFHLICG34Juq5Ai6lM1EsoD8HSsJpMcmrH7MWw2cKk\n" +"ujC3rMdFTtte83wF1uuF4FjUC72+SmcQN7A386BC/nk2TTsJawTDzqwOu/VdZv2g\n" +"1WpTHlumlClZeP+G/jkSyDwqNnTu1aodDmUa4xZodfhP1HWPwUKFcq8oQr148QYA\n" +"AOlbUOJQU7QwRWd1VbnwhDtQWXC92A2w1n/xkZSR1BM/NUSDhkBSUU1WjMbWg6Gg\n" +"mnIZLRerQCu1Oozr87rOQqQakPkyt8BUSNK3K42j2qcfhAONdRl8Hq8Qs5pupy+s\n" +"8sdCGDlwR3JNCMv6u48OK87F4mcIxhkSefFJUFII25pCGN5WtE4p5l+9cnO1GrIX\n" +"e2Hl/7M0c/lbZ4FvXgARlex2rkgS0Ka06HE=\n" +"-----END CERTIFICATE-----\n"; + +static const char * const test_key = +"-----BEGIN PRIVATE KEY-----\n" +"MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCjYtuWaICCY0tJ\n" +"PubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8Di3DAmHK\n" +"nSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTekLWcfI5ZZ\n" +"toGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnHKT/m6DSU\n" +"0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6jzhNyMBT\n" +"J1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQUjy5N8pS\n" +"Np7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAzTK4l2pHN\n" +"uC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBKIzv9cgi9\n" +"fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0nPN1IMSn\n" +"zXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzoGMTvP/Au\n" +"ehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9psNcjTMaB\n" +"QLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABAoICAFWe8MQZb37k2gdAV3Y6aq8f\n" +"qokKQqbCNLd3giGFwYkezHXoJfg6Di7oZxNcKyw35LFEghkgtQqErQqo35VPIoH+\n" +"vXUpWOjnCmM4muFA9/cX6mYMc8TmJsg0ewLdBCOZVw+wPABlaqz+0UOiSMMftpk9\n" +"fz9JwGd8ERyBsT+tk3Qi6D0vPZVsC1KqxxL/cwIFd3Hf2ZBtJXe0KBn1pktWht5A\n" +"Kqx9mld2Ovl7NjgiC1Fx9r+fZw/iOabFFwQA4dr+R8mEMK/7bd4VXfQ1o/QGGbMT\n" +"G+ulFrsiDyP+rBIAaGC0i7gDjLAIBQeDhP409ZhswIEc/GBtODU372a2CQK/u4Q/\n" +"HBQvuBtKFNkGUooLgCCbFxzgNUGc83GB/6IwbEM7R5uXqsFiE71LpmroDyjKTlQ8\n" +"YZkpIcLNVLw0usoGYHFm2rvCyEVlfsE3Ub8cFyTFk50SeOcF2QL2xzKmmbZEpXgl\n" +"xBHR0hjgon0IKJDGfor4bHO7Nt+1Ece8u2oTEKvpz5aIn44OeC5mApRGy83/0bvs\n" +"esnWjDE/bGpoT8qFuy+0urDEPNId44XcJm1IRIlG56ErxC3l0s11wrIpTmXXckqw\n" +"zFR9s2z7f0zjeyxqZg4NTPI7wkM3M8BXlvp2GTBIeoxrWB4V3YArwu8QF80QBgVz\n" +"mgHl24nTg00UH1OjZsABAoIBAQDOxftSDbSqGytcWqPYP3SZHAWDA0O4ACEM+eCw\n" +"au9ASutl0IDlNDMJ8nC2ph25BMe5hHDWp2cGQJog7pZ/3qQogQho2gUniKDifN77\n" +"40QdykllTzTVROqmP8+efreIvqlzHmuqaGfGs5oTkZaWj5su+B+bT+9rIwZcwfs5\n" +"YRINhQRx17qa++xh5mfE25c+M9fiIBTiNSo4lTxWMBShnK8xrGaMEmN7W0qTMbFH\n" +"PgQz5FcxRjCCqwHilwNBeLDTp/ZECEB7y34khVh531mBE2mNzSVIQcGZP1I/DvXj\n" +"W7UUNdgFwii/GW+6M0uUDy23UVQpbFzcV8o1C2nZc4Fb4zwBAoIBAQDKSJkFwwuR\n" +"naVJS6WxOKjX8MCu9/cKPnwBv2mmI2jgGxHTw5sr3ahmF5eTb8Zo19BowytN+tr6\n" +"2ZFoIBA9Ubc9esEAU8l3fggdfM82cuR9sGcfQVoCh8tMg6BP8IBLOmbSUhN3PG2m\n" +"39I802u0fFNVQCJKhx1m1MFFLOu7lVcDS9JN+oYVPb6MDfBLm5jOiPuYkFZ4gH79\n" +"J7gXI0/YKhaJ7yXthYVkdrSF6Eooer4RZgma62Dd1VNzSq3JBo6rYjF7Lvd+RwDC\n" +"R1thHrmf/IXplxpNVkoMVxtzbrrbgnC25QmvRYc0rlS/kvM4yQhMH3eA7IycDZMp\n" +"Y+0xm7I7jTT7AoIBAGKzKIMDXdCxBWKhNYJ8z7hiItNl1IZZMW2TPUiY0rl6yaCh\n" +"BVXjM9W0r07QPnHZsUiByqb743adkbTUjmxdJzjaVtxN7ZXwZvOVrY7I7fPWYnCE\n" +"fXCr4+IVpZI/ZHZWpGX6CGSgT6EOjCZ5IUufIvEpqVSmtF8MqfXO9o9uIYLokrWQ\n" +"x1dBl5UnuTLDqw8bChq7O5y6yfuWaOWvL7nxI8NvSsfj4y635gIa/0dFeBYZEfHI\n" +"UlGdNVomwXwYEzgE/c19ruIowX7HU/NgxMWTMZhpazlxgesXybel+YNcfDQ4e3RM\n" +"OMz3ZFiaMaJsGGNf4++d9TmMgk4Ns6oDs6Tb9AECggEBAJYzd+SOYo26iBu3nw3L\n" +"65uEeh6xou8pXH0Tu4gQrPQTRZZ/nT3iNgOwqu1gRuxcq7TOjt41UdqIKO8vN7/A\n" +"aJavCpaKoIMowy/aGCbvAvjNPpU3unU8jdl/t08EXs79S5IKPcgAx87sTTi7KDN5\n" +"SYt4tr2uPEe53NTXuSatilG5QCyExIELOuzWAMKzg7CAiIlNS9foWeLyVkBgCQ6S\n" +"me/L8ta+mUDy37K6vC34jh9vK9yrwF6X44ItRoOJafCaVfGI+175q/eWcqTX4q+I\n" +"G4tKls4sL4mgOJLq+ra50aYMxbcuommctPMXU6CrrYyQpPTHMNVDQy2ttFdsq9iK\n" +"TncCggEBAMmt/8yvPflS+xv3kg/ZBvR9JB1In2n3rUCYYD47ReKFqJ03Vmq5C9nY\n" +"56s9w7OUO8perBXlJYmKZQhO4293lvxZD2Iq4NcZbVSCMoHAUzhzY3brdgtSIxa2\n" +"gGveGAezZ38qKIU26dkz7deECY4vrsRkwhpTW0LGVCpjcQoaKvymAoCmAs8V2oMr\n" +"Ziw1YQ9uOUoWwOqm1wZqmVcOXvPIS2gWAs3fQlWjH9hkcQTMsUaXQDOD0aqkSY3E\n" +"NqOvbCV1/oUpRi3076khCoAXI1bKSn/AvR3KDP14B5toHI/F5OTSEiGhhHesgRrs\n" +"fBrpEY1IATtPq1taBZZogRqI3rOkkPk=\n" +"-----END PRIVATE KEY-----\n"; + +#define TEST_TOTAL 65536 +#define TEST_CHUNK 1024 +#define GRANT_QUANTUM 1024 +#define GRANT_US (10 * LWS_US_PER_MS) + +static struct lws *client_wsi; +static size_t srv_sent; +static size_t cli_rx; +static size_t cli_granted; +static int port_tcp = 7681; +static lws_sorted_usec_list_t sul_grant, sul_timeout; + +static void +sul_timeout_cb(lws_sorted_usec_list_t *sul) +{ + lwsl_err("--- timeout: rx %d / %d, granted %d ---\n", + (int)cli_rx, TEST_TOTAL, (int)cli_granted); + interrupted = 1; +} + +static void +sul_grant_cb(lws_sorted_usec_list_t *sul) +{ + if (!client_wsi || cli_rx >= TEST_TOTAL) + return; + + if (cli_granted < TEST_TOTAL + (2 * TEST_CHUNK)) { + cli_granted += GRANT_QUANTUM; + if (lws_wsi_tx_credit(client_wsi, LWSTXCR_PEER_TO_US, + GRANT_QUANTUM) < 0) + lwsl_warn("%s: tx_credit update failed\n", __func__); + } + + lws_sul_schedule(context, 0, &sul_grant, sul_grant_cb, GRANT_US); +} + +static int +callback_srv(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: + lwsl_user("%s: server: established\n", __func__); + srv_sent = 0; + lws_callback_on_writable(wsi); + break; + + case LWS_CALLBACK_SERVER_WRITEABLE: { + uint8_t buf[LWS_PRE + TEST_CHUNK]; + size_t n; + + lwsl_info("%s: srv writeable: sent %d, choked %d\n", __func__, + (int)srv_sent, lws_send_pipe_choked(wsi)); + + /* + * bulk-send gated only on choked, like typical user code: + * the h2 role must hold DATA back itself when the peer's + * window is exhausted + */ + while (srv_sent < TEST_TOTAL && !lws_send_pipe_choked(wsi)) { + n = TEST_TOTAL - srv_sent; + if (n > TEST_CHUNK) + n = TEST_CHUNK; + for (size_t i = 0; i < n; i++) + buf[LWS_PRE + i] = + (uint8_t)('A' + ((srv_sent + i) % 26)); + int wr = lws_write(wsi, &buf[LWS_PRE], n, + LWS_WRITE_BINARY); + lwsl_info("%s: srv wrote %d of %d\n", __func__, wr, (int)n); + if (wr < (int)n) { + lwsl_err("%s: server write failed\n", __func__); + return -1; + } + srv_sent += n; + } + if (srv_sent < TEST_TOTAL) + lws_callback_on_writable(wsi); + break; + } + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static int +callback_cli(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_CLIENT_ESTABLISHED: + lwsl_user("%s: client: established\n", __func__); + if (lws_get_network_wsi(wsi) == wsi) { + lwsl_err("--- not encapsulated in h2 ---\n"); + interrupted = 1; + return -1; + } + client_wsi = wsi; + lws_sul_schedule(context, 0, &sul_grant, sul_grant_cb, + GRANT_US); + break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + for (size_t i = 0; i < len; i++) + if (((uint8_t *)in)[i] != + (uint8_t)('A' + ((cli_rx + i) % 26))) { + lwsl_err("--- pattern corrupt at ofs %d ---\n", + (int)(cli_rx + i)); + interrupted = 1; + return -1; + } + cli_rx += len; + + /* + * ws frame headers consume window as well, so honest payload + * rx is always strictly below the granted DATA credit + */ + if (cli_rx > cli_granted) { + lwsl_err("--- flow control violated: rx %d > granted %d ---\n", + (int)cli_rx, (int)cli_granted); + interrupted = 1; + return -1; + } + + if (cli_rx == TEST_TOTAL) { + lwsl_user("--- transfer complete and in-window. " + "Test passed. ---\n"); + result = 0; + interrupted = 1; + return -1; + } + break; + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + lwsl_err("--- client connection error: %s ---\n", + in ? (char *)in : "(null)"); + interrupted = 1; + break; + + case LWS_CALLBACK_CLIENT_CLOSED: + lwsl_user("%s: client: closed (rx %d granted %d)\n", __func__, + (int)cli_rx, (int)cli_granted); + client_wsi = NULL; + if (result) + interrupted = 1; + break; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static const struct lws_protocols protocols_srv[] = { + { "http", lws_callback_http_dummy, 0, 0, 0, NULL, 0 }, + { "txcr", callback_srv, 0, TEST_CHUNK, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +static const struct lws_protocols protocols_cli[] = { + { "txcr", callback_cli, 0, TEST_CHUNK, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + struct lws_context_creation_info info; + struct lws_client_connect_info i; + struct lws_vhost *vh; + const char *p; + int n = 0; + + lws_context_info_defaults(&info, NULL); + info.fd_limit_per_thread = 0; + lws_cmdline_option_handle_builtin(argc, argv, &info); + + if ((p = lws_cmdline_option(argc, argv, "-p"))) + port_tcp = atoi(p); + + signal(SIGINT, sigint_handler); + + lwsl_user("LWS API selftest: ws-over-h2 tx credit\n"); + + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | + LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + /* ws server vhost, h2 alpn */ + info.port = port_tcp; + info.vhost_name = "srv"; + info.alpn = "h2,http/1.1"; + info.protocols = protocols_srv; + info.server_ssl_cert_mem = test_cert; + info.server_ssl_cert_mem_len = (unsigned int)strlen(test_cert); + info.server_ssl_private_key_mem = test_key; + info.server_ssl_private_key_mem_len = (unsigned int)strlen(test_key); + + vh = lws_create_vhost(context, &info); + if (!vh) { + lwsl_err("Failed to create server vhost\n"); + goto bail; + } + + /* client vhost */ + info.port = CONTEXT_PORT_NO_LISTEN; + info.vhost_name = "cli"; + info.protocols = protocols_cli; + info.server_ssl_cert_mem = NULL; + info.server_ssl_cert_mem_len = 0; + info.server_ssl_private_key_mem = NULL; + info.server_ssl_private_key_mem_len = 0; + + vh = lws_create_vhost(context, &info); + if (!vh) { + lwsl_err("Failed to create client vhost\n"); + goto bail; + } + + memset(&i, 0, sizeof(i)); + i.context = context; + i.vhost = vh; + i.address = "127.0.0.1"; + i.port = port_tcp; + i.path = "/"; + i.host = "localhost"; + i.origin = "localhost"; + i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | + LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK | + LCCSCF_H2_MANUAL_RXFLOW; + i.manual_initial_tx_credit = GRANT_QUANTUM; + i.alpn = "h2"; + i.protocol = "txcr"; + i.local_protocol_name = "txcr"; + cli_granted = GRANT_QUANTUM; + + if (!lws_client_connect_via_info(&i)) { + lwsl_err("client connect failed\n"); + goto bail; + } + + lws_sul_schedule(context, 0, &sul_timeout, sul_timeout_cb, + 20 * LWS_US_PER_SEC); + + while (n >= 0 && !interrupted) + n = lws_service(context, 0); + +bail: + lws_context_destroy(context); + + lwsl_user("Completed: %s\n", result ? "FAIL" : "PASS"); + + return result; +} diff --git a/minimal-examples-lowlevel/http-client/minimal-http-client-multi/minimal-http-client-multi.c b/minimal-examples-lowlevel/http-client/minimal-http-client-multi/minimal-http-client-multi.c index 35eab256ff..eacbaf1741 100644 --- a/minimal-examples-lowlevel/http-client/minimal-http-client-multi/minimal-http-client-multi.c +++ b/minimal-examples-lowlevel/http-client/minimal-http-client-multi/minimal-http-client-multi.c @@ -59,6 +59,7 @@ enum { LWS_SW_SAVE_TICKET, LWS_SW_LOAD_TICKET, LWS_SW_SEQ, + LWS_SW_QUIC_INITIAL_CWND, LWS_SW_HELP, }; @@ -85,6 +86,7 @@ static const struct lws_switches switches[] = { [LWS_SW_SAVE_TICKET] = { "--save-ticket", "Path to save TLS session ticket" }, [LWS_SW_LOAD_TICKET] = { "--load-ticket", "Path to load TLS session ticket" }, [LWS_SW_SEQ] = { "--seq", "Run sequentially" }, + [LWS_SW_QUIC_INITIAL_CWND] = { "--quic-initial-cwnd", "Initial congestion window in bytes" }, [LWS_SW_HELP] = { "--help", "Show this help information" }, }; @@ -98,7 +100,7 @@ static const struct lws_switches switches[] = { #include #endif -#define COUNT 1024 +#define COUNT 4096 struct cliuser { int index; @@ -126,11 +128,11 @@ static char save_ticket[256]; static char load_ticket[256]; struct req { - char address[128]; + char path[512]; + char address[256]; int port; - char path[128]; }; -static struct req reqs[1024]; +static struct req reqs[COUNT]; struct pss { char body_part; @@ -232,6 +234,10 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, switch (reason) { + case LWS_CALLBACK_CLIENT_ESTABLISHED_EARLY: + lwsl_user("LWS_CALLBACK_CLIENT_ESTABLISHED_EARLY: idx: %d, opting into 0-RTT\n", idx); + return 1; + case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: lwsl_user("LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: idx: %d, resp %u\n", idx, lws_http_client_http_response(wsi)); @@ -257,15 +263,12 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, #if defined(LWS_WITH_TLS_SESSIONS) && !defined(LWS_WITH_MBEDTLS) && !defined(WIN32) if (lws_tls_session_is_reused(wsi)) reuse++; - else - /* - * Attempt to store any new session into - * external storage - */ - if (lws_tls_session_dump_save(lws_get_vhost_by_name(context, "default"), - i.host, (uint16_t)i.port, - sess_save_cb, save_ticket)) - lwsl_warn("%s: session save failed\n", __func__); + /* + * Don't attempt to save the session here: the NewSessionTicket + * arrives asynchronously AFTER this callback fires, so the + * vhost session cache is empty or has only a stub at this + * point. The deferred save happens after the event loop exits. + */ #endif break; @@ -439,11 +442,10 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, lwsl_err("Done: failed: %d\n", failed); intr = 1; /* - * This is how we can exit the event loop even when it's an - * event library backing it... it will start and stage the - * destroy to happen after we exited this service for each pt + * Exit the loop by cancelling service so we can save TLS + * sessions in main() before actually destroying the context. */ - lws_context_destroy(lws_get_context(wsi)); + lws_cancel_service(lws_get_context(wsi)); } else if (sequential) { lws_try_client_connection(&i, completed); } @@ -502,7 +504,8 @@ lws_try_client_connection(struct lws_client_connect_info *ii, int m) lwsl_user("%s: failed: conn idx %d\n", __func__, m); if (++completed == count) { lwsl_user("Done: failed: %d\n", failed); - lws_context_destroy(context); + intr = 1; + lws_cancel_service(context); } } } else { @@ -556,7 +559,8 @@ signal_cb(void *handle, int signum) lwsl_err("%s: signal %d\n", __func__, signum); break; } - lws_context_destroy(context); + intr = 1; + lws_cancel_service(context); } static void @@ -612,7 +616,7 @@ stagger_cb(lws_sorted_usec_list_t *sul) if (stagger_idx == count) return; - next = 150 * LWS_US_PER_MS; + if (count > 100) next = 1 * LWS_US_PER_MS; else next = 150 * LWS_US_PER_MS; if (stagger_idx == count - 1) next += 400 * LWS_US_PER_MS; @@ -641,12 +645,14 @@ int main(int argc, const char **argv) int pl = 0; #endif - lws_context_info_defaults(&info, NULL);memset(&i, 0, sizeof i); /* otherwise uninitialized garbage */ + lws_context_info_defaults(&info, NULL); - lws_cmdline_option_handle_builtin(argc, argv, &info); + lws_cmdline_option_handle_builtin(argc, argv, &info); + + info.timeout_secs = 120; info.signal_cb = signal_cb; - info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; if (lws_cmdline_option(argc, argv, switches[LWS_SW_UV].sw)) info.options |= LWS_SERVER_OPTION_LIBUV; @@ -717,7 +723,64 @@ int main(int argc, const char **argv) */ info.simultaneous_ssl_handshake_restriction = atoi(p); - context = lws_create_context(&info); + if ((p = lws_cmdline_option(argc, argv, "--tls13-ciphers"))) { + info.client_tls_1_3_plus_cipher_list = p; + info.client_ssl_cipher_list = p; + } + + lwsl_notice("AGY-DEBUG: minimal: before TESTCASE check, client_ssl_cipher_list is '%s'\n", info.client_ssl_cipher_list ? info.client_ssl_cipher_list : "NULL"); + + const char *testcase = getenv("TESTCASE"); + if (testcase && !strcmp(testcase, "chacha20")) { +#if defined(LWS_WITH_GNUTLS) + info.client_tls_1_3_plus_cipher_list = "NORMAL:-CIPHER-ALL:-AES-256-GCM:-AES-128-GCM:-AES-128-CCM:-AES-128-CCM-8:+CHACHA20-POLY1305"; + info.client_ssl_cipher_list = "NORMAL:-CIPHER-ALL:-AES-256-GCM:-AES-128-GCM:-AES-128-CCM:-AES-128-CCM-8:+CHACHA20-POLY1305"; +#else + info.client_tls_1_3_plus_cipher_list = "TLS_CHACHA20_POLY1305_SHA256"; + info.client_ssl_cipher_list = "TLS_CHACHA20_POLY1305_SHA256"; +#endif + } + + if ((p = lws_cmdline_option(argc, argv, switches[LWS_SW_QUIC_INITIAL_CWND].sw))) { + int val = atoi(p); + if (val > 0) + info.quic_initial_cwnd = (uint32_t)val; + } + +#if defined(LWS_WITH_TLS_SESSIONS) && !defined(LWS_WITH_MBEDTLS) && !defined(WIN32) + /* + * When --0rtt or the QIR harness (TESTCASE=zerortt) is used, + * auto-configure the ticket persistence path so that phase 1 + * saves the ticket and phase 2 can load it and attempt 0-RTT, + * provided the caller hasn't already set an explicit path. + */ + if ((info.options & LWS_SERVER_OPTION_ALLOW_EARLY_DATA) || + (testcase && !strcmp(testcase, "zerortt"))) { + if (!save_ticket[0]) + lws_strncpy(save_ticket, "./lws_session_ticket", + sizeof(save_ticket)); + if (!load_ticket[0]) + lws_strncpy(load_ticket, "./lws_session_ticket", + sizeof(load_ticket)); + lwsl_notice("%s: early-data/zerortt: ticket path '%s'\n", + __func__, save_ticket); + if (!info.quic_initial_cwnd) + info.quic_initial_cwnd = 250000; + } +#endif + + if (lws_cmdline_option(argc, argv, "--quicv2")) + info.options |= LWS_SERVER_OPTION_QUIC_LATEST_VERSION; + + if (lws_cmdline_option(argc, argv, "--quic-early-key-update")) + info.options |= LWS_SERVER_OPTION_QUIC_EARLY_KEY_UPDATE; + + /* lws_context_info_defaults sets LWS_SERVER_OPTION_EXPLICIT_VHOSTS by default now, + * which skips creating the default vhost using our info struct. Clear it so + * our TLS cipher overrides are actually used to create the default vhost! */ + info.options &= ~(uint64_t)LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + context = lws_create_context(&info); if (!context) { lwsl_err("lws init failed\n"); return 1; @@ -768,6 +831,14 @@ int main(int argc, const char **argv) if (lws_cmdline_option(argc, argv, "--h3")) i.alpn = "h3"; + if (lws_cmdline_option(argc, argv, "--quicv2")) + info.options |= LWS_SERVER_OPTION_QUIC_LATEST_VERSION; + + if (lws_cmdline_option(argc, argv, "--quic-early-key-update")) + info.options |= LWS_SERVER_OPTION_QUIC_EARLY_KEY_UPDATE; + + + if (lws_cmdline_option(argc, argv, "--h1")) i.alpn = "http/1.1"; @@ -806,7 +877,7 @@ int main(int argc, const char **argv) lws_strncpy(reqs[count].address, url, sizeof(reqs[count].address)); } count++; - if (count == 1024) break; + if (count == COUNT) break; } } int default_urls = 0; @@ -846,6 +917,10 @@ int main(int argc, const char **argv) if (lws_cmdline_option(argc, argv, switches[LWS_SW_NO_TLS].sw)) i.ssl_connection &= ~(LCCSCF_USE_SSL); + if (lws_cmdline_option(argc, argv, "--0rtt") || + (testcase && !strcmp(testcase, "zerortt"))) + i.ssl_connection |= LCCSCF_ALLOW_EARLY_DATA; + int c_opt = count; if ((p = lws_cmdline_option(argc, argv, switches[LWS_SW_C].sw))) { c_opt = atoi(p); @@ -878,9 +953,11 @@ int main(int argc, const char **argv) /* * Attempt to preload a session from external storage */ - if (lws_tls_session_dump_load(lws_get_vhost_by_name(context, "default"), - i.host, (uint16_t)i.port, sess_load_cb, load_ticket)) - lwsl_warn("%s: session load failed\n", __func__); + if (load_ticket[0]) { + if (lws_tls_session_dump_load(lws_get_vhost_by_name(context, "default"), + i.host, (uint16_t)reqs[0].port, sess_load_cb, load_ticket)) + lwsl_warn("%s: session load failed\n", __func__); + } #endif start = us(); @@ -898,6 +975,32 @@ int main(int argc, const char **argv) #endif lwsl_user("Duration: %lldms\n", (us() - start) / 1000); + +#if defined(LWS_WITH_TLS_SESSIONS) && !defined(LWS_WITH_MBEDTLS) && !defined(WIN32) + /* + * Save TLS session AFTER the event loop, not at ESTABLISHED time. + * The TLS 1.3 NewSessionTicket arrives from the server asynchronously + * after the HTTP response begins. By the time we reach here the + * session is fully cached in-memory and ready for disk persistence. + * This is especially important for zerortt: phase 2 is a separate + * process and needs the ticket on disk to attempt early data. + */ + lwsl_user("save_ticket = %s\n", save_ticket); + if (save_ticket[0]) { + struct lws_vhost *save_vh = lws_get_vhost_by_name(context, "default"); + if (save_vh) { + if (lws_tls_session_dump_save(save_vh, i.host, + (uint16_t)reqs[0].port, + sess_save_cb, save_ticket)) + lwsl_warn("%s: deferred session save failed\n", + __func__); + else + lwsl_notice("%s: session saved to %s\n", + __func__, save_ticket); + } + } +#endif + lws_context_destroy(context); lwsl_user("Exiting with %d\n", failed || completed != count); diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/CMakeLists.txt b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/CMakeLists.txt new file mode 100644 index 0000000000..5ac36e35b1 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/CMakeLists.txt @@ -0,0 +1,50 @@ +project(lws-minimal-http-server-hls C) +cmake_minimum_required(VERSION 3.10) +find_package(libwebsockets CONFIG REQUIRED) +list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR}) +include(CheckCSourceCompiles) +include(LwsCheckRequirements) + +set(SAMP lws-minimal-http-server-hls) +set(SRCS minimal-http-server.c) + +set(requirements 1) +require_lws_config(LWS_ROLE_H1 1 requirements) +require_lws_config(LWS_WITH_SERVER 1 requirements) +require_lws_config(LWS_WITH_PLUGINS 1 requirements) + +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(PC_AVFORMAT libavformat) + pkg_check_modules(PC_AVCODEC libavcodec) + pkg_check_modules(PC_AVUTIL libavutil) + pkg_check_modules(PC_SWSCALE libswscale) + + if (NOT PC_AVFORMAT_FOUND OR NOT PC_AVCODEC_FOUND OR NOT PC_AVUTIL_FOUND OR NOT PC_SWSCALE_FOUND) + set(requirements 0) + endif() +else() + set(requirements 0) +endif() + +if (requirements) + add_executable(${SAMP} ${SRCS}) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared ${LIBWEBSOCKETS_DEP_LIBS}) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets ${LIBWEBSOCKETS_DEP_LIBS}) + endif() + + set_property(TARGET ${SAMP} APPEND PROPERTY COMPILE_DEFINITIONS + INSTALL_DATADIR="${CMAKE_INSTALL_PREFIX}/share" + ) + + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/media/" + DESTINATION share/libwebsockets-test-server/hls + COMPONENT examples) + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/mount-origin/" + DESTINATION share/libwebsockets-test-server/hls/mount-origin + COMPONENT examples) +endif() diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/README.md b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/README.md new file mode 100644 index 0000000000..966b539e51 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/README.md @@ -0,0 +1,58 @@ +# lws minimal http server HLS + +This example demonstrates how to use the `protocol_lws_hls` plugin to dynamically serve HTTP Live Streaming (HLS) content from static `.mp4` and `.mkv` files. It recursively scans the specified media directory, creates an HTML index, and chunks video files on the fly into HLS `.ts` segments. + +## build + +``` + $ cmake . && make +``` + +## usage + +``` + $ ./lws-minimal-http-server-hls +[2026/07/08 12:00:00:0000] USER: LWS minimal http server HLS | visit http://localhost:7681 +[2026/07/08 12:00:00:0000] USER: Media dir: /usr/local/share/libwebsockets-test-server/hls +``` + +Visit http://localhost:7681 to view the generated media library. + +## Commandline Options + +- `--media-dir `: Override the default directory containing media files (default: installed `media/` path). +- `--help`: Show built-in LWS options (e.g. `-d `). + +## lwsws configuration + +If you want to use the HLS plugin with `lwsws` (the LWS JSON-configured web server) instead of this minimal C example, you can enable and configure the plugin and mounts via your `lejp` vhost configuration file: + +```json +{ + "vhosts": [ + { + "name": "localhost", + "port": 7681, + "ws-protocols": [ + { + "lws-hls": { + "status": "ok", + "media-dir": "/path/to/your/media" + } + } + ], + "mounts": [ + { + "mountpoint": "/media", + "origin": "file:///usr/local/share/libwebsockets-test-server/hls/mount-origin", + "default": "index.html" + }, + { + "mountpoint": "/media/hls", + "origin": "callback://lws-hls" + } + ] + } + ] +} +``` diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/media/Big_Buck_Bunny_360_10s_1MB.mp4 b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/media/Big_Buck_Bunny_360_10s_1MB.mp4 new file mode 100644 index 0000000000..9b6d89da00 Binary files /dev/null and b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/media/Big_Buck_Bunny_360_10s_1MB.mp4 differ diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/minimal-http-server.c b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/minimal-http-server.c new file mode 100644 index 0000000000..e645b88678 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/minimal-http-server.c @@ -0,0 +1,172 @@ +/* + * lws-minimal-http-server-hls + */ + +#include +#include +#include + +static int interrupted; + +enum { + LWS_SW_H2_PRIOR_KNOWLEDGE, + LWS_SW_D, + LWS_SW_MEDIA_DIR, + LWS_SW_HELP, +}; + +static const struct lws_switches switches[] = { + [LWS_SW_H2_PRIOR_KNOWLEDGE] = { "--h2-prior-knowledge", "Enable --h2-prior-knowledge feature" }, + [LWS_SW_D] = { "-d", "Debug logs (e.g. -d 15)" }, + [LWS_SW_MEDIA_DIR] = { "--media-dir", "Directory containing media files (default: ./media)" }, + [LWS_SW_HELP] = { "--help", "Show this help information" }, +}; + +#ifndef INSTALL_DATADIR +#define INSTALL_DATADIR "/usr/local/share" +#endif + +static struct lws_protocol_vhost_options pvo_media = { + NULL, NULL, "media-dir", INSTALL_DATADIR "/libwebsockets-test-server/hls" +}; + +static const struct lws_protocol_vhost_options pvo_csp = { + NULL, NULL, "content-security-policy:", + "default-src 'self'; img-src 'self' data: ; " + "script-src 'self'; script-src-elem 'self'; font-src 'self'; " + "style-src 'self'; connect-src 'self' ws: wss:; " + "worker-src 'self' blob:; child-src 'self' blob:; media-src 'self' blob:; " + "frame-ancestors 'none'; base-uri 'none'; form-action 'self';" +}; + +static const struct lws_protocol_vhost_options pvo = { + NULL, &pvo_media, "lws-hls", "" +}; + +static const struct lws_http_mount mount_hls = { + .mount_next = NULL, + .mountpoint = "/hls", /* mountpoint URL */ + .origin = INSTALL_DATADIR "/libwebsockets-test-server/hls/mount-origin", + .def = "index.html", + .origin_protocol = LWSMPRO_FILE, /* serve from dir */ + .mountpoint_len = 4, /* char count */ +}; + +static const struct lws_http_mount mount_hls_live = { + .mount_next = &mount_hls, + .mountpoint = "/hls/hls", /* mountpoint URL */ + .protocol = "lws-hls", /* protocol name */ + .origin_protocol = LWSMPRO_CALLBACK, /* callback */ + .mountpoint_len = 8, /* char count */ +}; + +static const struct lws_protocol_vhost_options pvo_mime_mkv = { + NULL, NULL, ".mkv", "video/webm" +}; + +static const struct lws_protocol_vhost_options pvo_mime_mp4 = { + &pvo_mime_mkv, NULL, ".mp4", "video/mp4" +}; + +static struct lws_http_mount mount_raw_media = { + .mount_next = &mount_hls_live, + .mountpoint = "/media", /* mountpoint URL */ + .origin = NULL, /* set at runtime */ + .def = "index.html", + .origin_protocol = LWSMPRO_FILE, /* serve from dir */ + .mountpoint_len = 6, /* char count */ + .extra_mimetypes = &pvo_mime_mp4, +}; + +static const struct lws_http_mount mount = { + .mount_next = &mount_raw_media, + .mountpoint = "/", /* mountpoint URL */ + .origin = INSTALL_DATADIR "/libwebsockets-test-server/hls/mount-origin", + .def = "index.html", + .origin_protocol = LWSMPRO_FILE, /* serve from dir */ + .mountpoint_len = 1, /* char count */ +}; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + struct lws_context_creation_info info; + struct lws_context *context; + int n = 0; + const char *p; + +#if defined(LWS_WITH_PLUGINS) + /* LWS searches for plugins in this array of paths */ + static const char * const plugin_dirs[] = { + "../../lib", /* For running from build/minimal-examples-lowlevel/... */ + "./lib", + NULL + }; +#endif + + if (lws_cmdline_option(argc, argv, switches[LWS_SW_HELP].sw)) { + lws_switches_print_help(argv[0], switches, LWS_ARRAY_SIZE(switches)); + return 0; + } + + if ((p = lws_cmdline_option(argc, argv, switches[LWS_SW_MEDIA_DIR].sw))) + pvo_media.value = p; + + signal(SIGINT, sigint_handler); + + lws_context_info_defaults(&info, NULL); + lws_cmdline_option_handle_builtin(argc, argv, &info); + + if ((p = lws_cmdline_option(argc, argv, "-i"))) + info.iface = p; + else + info.iface = "lo"; + + if ((p = lws_cmdline_option(argc, argv, "-b"))) + pvo_media.value = p; + + lwsl_user("LWS minimal http server HLS | visit http://localhost:7681\n"); + lwsl_user("Media dir: %s\n", pvo_media.value); + if (info.iface) + lwsl_user("Binding to interface: %s\n", info.iface); + + info.port = 7681; + + info.headers = &pvo_csp; + info.pvo = &pvo; + mount_raw_media.origin = pvo_media.value; + info.mounts = &mount; +#if defined(LWS_WITH_PLUGINS) + info.plugin_dirs = plugin_dirs; +#endif + + info.options = LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE | + LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + if (lws_cmdline_option(argc, argv, switches[LWS_SW_H2_PRIOR_KNOWLEDGE].sw)) + info.options |= LWS_SERVER_OPTION_H2_PRIOR_KNOWLEDGE; + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + /* We create the vhost explicitly so plugins are loaded and attached */ + struct lws_vhost *vh = lws_create_vhost(context, &info); + if (!vh) { + lwsl_err("Failed to create vhost\n"); + return 1; + } + + while (n >= 0 && !interrupted) + n = lws_service(context, 0); + + lws_context_destroy(context); + + return 0; +} diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/404.html b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/404.html new file mode 100644 index 0000000000..bcaf47e4d6 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/404.html @@ -0,0 +1,13 @@ + + + + + 404 + + + LWS logo +
+

404

+ Sorry, that file doesn't exist. + + diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/dir.css b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/dir.css new file mode 100644 index 0000000000..d9f91ee200 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/dir.css @@ -0,0 +1,5 @@ +body { font-family: sans-serif; background: #1a1a1a; color: #eee; } +.item { display: inline-block; margin: 10px; text-align: center; background: #333; padding: 10px; border-radius: 8px; } +.thumb { width: 320px; height: 180px; object-fit: cover; background: #000; border-radius: 4px; } +a { color: #4db8ff; text-decoration: none; } +#auth-status { float: right; margin-top: 10px; margin-right: 20px; } diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/favicon.ico b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/favicon.ico new file mode 100644 index 0000000000..c0cc2e3dff Binary files /dev/null and b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/favicon.ico differ diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/hls.min.js b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/hls.min.js new file mode 100644 index 0000000000..77bb6b8a81 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/hls.min.js @@ -0,0 +1,2 @@ +!function t(e){var r,i;r=this,i=function(){"use strict";function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,i)}return r}function i(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,i=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function v(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var m={exports:{}};!function(t,e){var r,i,n,a,s;r=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,i=/^(?=([^\/?#]*))\1([^]*)$/,n=/(?:\/|^)\.(?=\/)/g,a=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,s={buildAbsoluteURL:function(t,e,r){if(r=r||{},t=t.trim(),!(e=e.trim())){if(!r.alwaysNormalize)return t;var n=s.parseURL(t);if(!n)throw new Error("Error trying to parse base URL.");return n.path=s.normalizePath(n.path),s.buildURLFromParts(n)}var a=s.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return r.alwaysNormalize?(a.path=s.normalizePath(a.path),s.buildURLFromParts(a)):e;var o=s.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=i.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var u={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(u.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var h=o.path,d=h.substring(0,h.lastIndexOf("/")+1)+a.path;u.path=s.normalizePath(d)}else u.path=o.path,a.params||(u.params=o.params,a.query||(u.query=o.query));return null===u.path&&(u.path=r.alwaysNormalize?s.normalizePath(a.path):a.path),s.buildURLFromParts(u)},parseURL:function(t){var e=r.exec(t);return e?{scheme:e[1]||"",netLoc:e[2]||"",path:e[3]||"",params:e[4]||"",query:e[5]||"",fragment:e[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(n,"");t.length!==(t=t.replace(a,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}},t.exports=s}(m);var p=m.exports,y=Number.isFinite||function(t){return"number"==typeof t&&isFinite(t)},E=Number.isSafeInteger||function(t){return"number"==typeof t&&Math.abs(t)<=T},T=Number.MAX_SAFE_INTEGER||9007199254740991,S=function(t){return t.MEDIA_ATTACHING="hlsMediaAttaching",t.MEDIA_ATTACHED="hlsMediaAttached",t.MEDIA_DETACHING="hlsMediaDetaching",t.MEDIA_DETACHED="hlsMediaDetached",t.BUFFER_RESET="hlsBufferReset",t.BUFFER_CODECS="hlsBufferCodecs",t.BUFFER_CREATED="hlsBufferCreated",t.BUFFER_APPENDING="hlsBufferAppending",t.BUFFER_APPENDED="hlsBufferAppended",t.BUFFER_EOS="hlsBufferEos",t.BUFFER_FLUSHING="hlsBufferFlushing",t.BUFFER_FLUSHED="hlsBufferFlushed",t.MANIFEST_LOADING="hlsManifestLoading",t.MANIFEST_LOADED="hlsManifestLoaded",t.MANIFEST_PARSED="hlsManifestParsed",t.LEVEL_SWITCHING="hlsLevelSwitching",t.LEVEL_SWITCHED="hlsLevelSwitched",t.LEVEL_LOADING="hlsLevelLoading",t.LEVEL_LOADED="hlsLevelLoaded",t.LEVEL_UPDATED="hlsLevelUpdated",t.LEVEL_PTS_UPDATED="hlsLevelPtsUpdated",t.LEVELS_UPDATED="hlsLevelsUpdated",t.AUDIO_TRACKS_UPDATED="hlsAudioTracksUpdated",t.AUDIO_TRACK_SWITCHING="hlsAudioTrackSwitching",t.AUDIO_TRACK_SWITCHED="hlsAudioTrackSwitched",t.AUDIO_TRACK_LOADING="hlsAudioTrackLoading",t.AUDIO_TRACK_LOADED="hlsAudioTrackLoaded",t.SUBTITLE_TRACKS_UPDATED="hlsSubtitleTracksUpdated",t.SUBTITLE_TRACKS_CLEARED="hlsSubtitleTracksCleared",t.SUBTITLE_TRACK_SWITCH="hlsSubtitleTrackSwitch",t.SUBTITLE_TRACK_LOADING="hlsSubtitleTrackLoading",t.SUBTITLE_TRACK_LOADED="hlsSubtitleTrackLoaded",t.SUBTITLE_FRAG_PROCESSED="hlsSubtitleFragProcessed",t.CUES_PARSED="hlsCuesParsed",t.NON_NATIVE_TEXT_TRACKS_FOUND="hlsNonNativeTextTracksFound",t.INIT_PTS_FOUND="hlsInitPtsFound",t.FRAG_LOADING="hlsFragLoading",t.FRAG_LOAD_EMERGENCY_ABORTED="hlsFragLoadEmergencyAborted",t.FRAG_LOADED="hlsFragLoaded",t.FRAG_DECRYPTED="hlsFragDecrypted",t.FRAG_PARSING_INIT_SEGMENT="hlsFragParsingInitSegment",t.FRAG_PARSING_USERDATA="hlsFragParsingUserdata",t.FRAG_PARSING_METADATA="hlsFragParsingMetadata",t.FRAG_PARSED="hlsFragParsed",t.FRAG_BUFFERED="hlsFragBuffered",t.FRAG_CHANGED="hlsFragChanged",t.FPS_DROP="hlsFpsDrop",t.FPS_DROP_LEVEL_CAPPING="hlsFpsDropLevelCapping",t.MAX_AUTO_LEVEL_UPDATED="hlsMaxAutoLevelUpdated",t.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached",t.STEERING_MANIFEST_LOADED="hlsSteeringManifestLoaded",t}({}),L=function(t){return t.NETWORK_ERROR="networkError",t.MEDIA_ERROR="mediaError",t.KEY_SYSTEM_ERROR="keySystemError",t.MUX_ERROR="muxError",t.OTHER_ERROR="otherError",t}({}),A=function(t){return t.KEY_SYSTEM_NO_KEYS="keySystemNoKeys",t.KEY_SYSTEM_NO_ACCESS="keySystemNoAccess",t.KEY_SYSTEM_NO_SESSION="keySystemNoSession",t.KEY_SYSTEM_NO_CONFIGURED_LICENSE="keySystemNoConfiguredLicense",t.KEY_SYSTEM_LICENSE_REQUEST_FAILED="keySystemLicenseRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED="keySystemServerCertificateRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED="keySystemServerCertificateUpdateFailed",t.KEY_SYSTEM_SESSION_UPDATE_FAILED="keySystemSessionUpdateFailed",t.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED="keySystemStatusOutputRestricted",t.KEY_SYSTEM_STATUS_INTERNAL_ERROR="keySystemStatusInternalError",t.MANIFEST_LOAD_ERROR="manifestLoadError",t.MANIFEST_LOAD_TIMEOUT="manifestLoadTimeOut",t.MANIFEST_PARSING_ERROR="manifestParsingError",t.MANIFEST_INCOMPATIBLE_CODECS_ERROR="manifestIncompatibleCodecsError",t.LEVEL_EMPTY_ERROR="levelEmptyError",t.LEVEL_LOAD_ERROR="levelLoadError",t.LEVEL_LOAD_TIMEOUT="levelLoadTimeOut",t.LEVEL_PARSING_ERROR="levelParsingError",t.LEVEL_SWITCH_ERROR="levelSwitchError",t.AUDIO_TRACK_LOAD_ERROR="audioTrackLoadError",t.AUDIO_TRACK_LOAD_TIMEOUT="audioTrackLoadTimeOut",t.SUBTITLE_LOAD_ERROR="subtitleTrackLoadError",t.SUBTITLE_TRACK_LOAD_TIMEOUT="subtitleTrackLoadTimeOut",t.FRAG_LOAD_ERROR="fragLoadError",t.FRAG_LOAD_TIMEOUT="fragLoadTimeOut",t.FRAG_DECRYPT_ERROR="fragDecryptError",t.FRAG_PARSING_ERROR="fragParsingError",t.FRAG_GAP="fragGap",t.REMUX_ALLOC_ERROR="remuxAllocError",t.KEY_LOAD_ERROR="keyLoadError",t.KEY_LOAD_TIMEOUT="keyLoadTimeOut",t.BUFFER_ADD_CODEC_ERROR="bufferAddCodecError",t.BUFFER_INCOMPATIBLE_CODECS_ERROR="bufferIncompatibleCodecsError",t.BUFFER_APPEND_ERROR="bufferAppendError",t.BUFFER_APPENDING_ERROR="bufferAppendingError",t.BUFFER_STALLED_ERROR="bufferStalledError",t.BUFFER_FULL_ERROR="bufferFullError",t.BUFFER_SEEK_OVER_HOLE="bufferSeekOverHole",t.BUFFER_NUDGE_ON_STALL="bufferNudgeOnStall",t.INTERNAL_EXCEPTION="internalException",t.INTERNAL_ABORTED="aborted",t.UNKNOWN="unknown",t}({}),R=function(){},k={trace:R,debug:R,log:R,warn:R,info:R,error:R},b=k;function D(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i"):R}(e)}))}function I(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){D(t,"debug","log","info","warn","error");try{b.log('Debug logs enabled for "'+e+'" in hls.js version 1.5.8')}catch(t){b=k}}else b=k}var w=b,C=/^(\d+)x(\d+)$/,_=/(.+?)=(".*?"|.*?)(?:,|$)/g,x=function(){function t(e){"string"==typeof e&&(e=t.parseAttrList(e)),o(this,e)}var e=t.prototype;return e.decimalInteger=function(t){var e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e},e.hexadecimalInteger=function(t){if(this[t]){var e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;for(var r=new Uint8Array(e.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:e},e.decimalFloatingPoint=function(t){return parseFloat(this[t])},e.optionalFloat=function(t,e){var r=this[t];return r?parseFloat(r):e},e.enumeratedString=function(t){return this[t]},e.bool=function(t){return"YES"===this[t]},e.decimalResolution=function(t){var e=C.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}},t.parseAttrList=function(t){var e,r={};for(_.lastIndex=0;null!==(e=_.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1].trim()]=i}return r},s(t,[{key:"clientAttrs",get:function(){return Object.keys(this).filter((function(t){return"X-"===t.substring(0,2)}))}}]),t}();function P(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}var F=function(){function t(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){var r=e.attr;for(var i in r)if(Object.prototype.hasOwnProperty.call(t,i)&&t[i]!==r[i]){w.warn('DATERANGE tag attribute: "'+i+'" does not match for tags with ID: "'+t.ID+'"'),this._badValueForSameId=i;break}t=o(new x({}),r,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){var n=new Date(this.attr["END-DATE"]);y(n.getTime())&&(this._endDate=n)}}return s(t,[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){if(this._endDate)return this._endDate;var t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var t=this.attr.decimalFloatingPoint("DURATION");if(y(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}},{key:"plannedDuration",get:function(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}},{key:"endOnNext",get:function(){return this.attr.bool("END-ON-NEXT")}},{key:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&y(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}]),t}(),M=function(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}},O="audio",N="video",U="audiovideo",B=function(){function t(t){var e;this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams=((e={})[O]=null,e[N]=null,e[U]=null,e),this.baseurl=t}return t.prototype.setByteRange=function(t,e){var r,i=t.split("@",2);r=1===i.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(i[1]),this._byteRange=[r,parseInt(i[0])+r]},s(t,[{key:"byteRange",get:function(){return this._byteRange?this._byteRange:[]}},{key:"byteRangeStartOffset",get:function(){return this.byteRange[0]}},{key:"byteRangeEndOffset",get:function(){return this.byteRange[1]}},{key:"url",get:function(){return!this._url&&this.baseurl&&this.relurl&&(this._url=p.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""},set:function(t){this._url=t}}]),t}(),G=function(t){function e(e,r){var i;return(i=t.call(this,r)||this)._decryptdata=null,i.rawProgramDateTime=null,i.programDateTime=null,i.tagList=[],i.duration=0,i.sn=0,i.levelkeys=void 0,i.type=void 0,i.loader=null,i.keyLoader=null,i.level=-1,i.cc=0,i.startPTS=void 0,i.endPTS=void 0,i.startDTS=void 0,i.endDTS=void 0,i.start=0,i.deltaPTS=void 0,i.maxStartPTS=void 0,i.minEndPTS=void 0,i.stats=new M,i.data=void 0,i.bitrateTest=!1,i.title=null,i.initSegment=null,i.endList=void 0,i.gap=void 0,i.urlId=0,i.type=e,i}l(e,t);var r=e.prototype;return r.setKeyFormat=function(t){if(this.levelkeys){var e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}},r.abortRequests=function(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()},r.setElementaryStreamInfo=function(t,e,r,i,n,a){void 0===a&&(a=!1);var s=this.elementaryStreams,o=s[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,r),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,n)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:n,partial:a}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[O]=null,t[N]=null,t[U]=null},s(e,[{key:"decryptdata",get:function(){if(!this.levelkeys&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){var t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{var e=Object.keys(this.levelkeys);if(1===e.length)return this._decryptdata=this.levelkeys[e[0]].getDecryptData(this.sn)}}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!y(this.programDateTime))return null;var t=y(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){var e=Object.keys(this.levelkeys),r=e.length;if(r>1||1===r&&this.levelkeys[e[0]].encrypted)return!0}return!1}}]),e}(B),K=function(t){function e(e,r,i,n,a){var s;(s=t.call(this,i)||this).fragOffset=0,s.duration=0,s.gap=!1,s.independent=!1,s.relurl=void 0,s.fragment=void 0,s.index=void 0,s.stats=new M,s.duration=e.decimalFloatingPoint("DURATION"),s.gap=e.bool("GAP"),s.independent=e.bool("INDEPENDENT"),s.relurl=e.enumeratedString("URI"),s.fragment=r,s.index=n;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return l(e,t),s(e,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var t=this.elementaryStreams;return!!(t.audio||t.video||t.audiovideo)}}]),e}(B),H=function(){function t(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}return t.prototype.reloaded=function(t){if(!t)return this.advanced=!0,void(this.updated=!0);var e=this.lastPartSn-t.lastPartSn,r=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!r||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay},s(t,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&y(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var t=this.driftEndTime-this.driftStartTime;return t>0?1e3*(this.driftEnd-this.driftStart)/t:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}]),t}();function V(t){return Uint8Array.from(atob(t),(function(t){return t.charCodeAt(0)}))}function Y(t){var e,r,i=t.split(":"),n=null;if("data"===i[0]&&2===i.length){var a=i[1].split(";"),s=a[a.length-1].split(",");if(2===s.length){var o="base64"===s[0],l=s[1];o?(a.splice(-1,1),n=V(l)):(e=W(l).subarray(0,16),(r=new Uint8Array(16)).set(e,16-e.length),n=r)}}return n}function W(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(function(t){return t.charCodeAt(0)}))}var j="undefined"!=typeof self?self:void 0,q={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},X="org.w3.clearkey",z="com.apple.streamingkeydelivery",Q="com.microsoft.playready",J="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function $(t){switch(t){case z:return q.FAIRPLAY;case Q:return q.PLAYREADY;case J:return q.WIDEVINE;case X:return q.CLEARKEY}}var Z="edef8ba979d64acea3c827dcd51d21ed";function tt(t){switch(t){case q.FAIRPLAY:return z;case q.PLAYREADY:return Q;case q.WIDEVINE:return J;case q.CLEARKEY:return X}}function et(t){var e=t.drmSystems,r=t.widevineLicenseUrl,i=e?[q.FAIRPLAY,q.WIDEVINE,q.PLAYREADY,q.CLEARKEY].filter((function(t){return!!e[t]})):[];return!i[q.WIDEVINE]&&r&&i.push(q.WIDEVINE),i}var rt,it=null!=j&&null!=(rt=j.navigator)&&rt.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function nt(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}var at,st=function(t,e){return e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},ot=function(t,e){return e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},lt=function(t,e){for(var r=e,i=0;st(t,e);)i+=10,i+=ut(t,e+6),ot(t,e+10)&&(i+=10),e+=i;if(i>0)return t.subarray(r,r+i)},ut=function(t,e){var r=0;return r=(127&t[e])<<21,r|=(127&t[e+1])<<14,r|=(127&t[e+2])<<7,r|=127&t[e+3]},ht=function(t,e){return st(t,e)&&ut(t,e+6)+10<=t.length-e},dt=function(t){for(var e=gt(t),r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(a);break;case 12:case 13:s=t[h++],u+=String.fromCharCode((31&a)<<6|63&s);break;case 14:s=t[h++],o=t[h++],u+=String.fromCharCode((15&a)<<12|(63&s)<<6|(63&o)<<0)}}return u};function St(){if(!navigator.userAgent.includes("PlayStation 4"))return at||void 0===self.TextDecoder||(at=new self.TextDecoder("utf-8")),at}var Lt=function(t){for(var e="",r=0;r>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function xt(t,e){var r=[];if(!e.length)return r;for(var i=t.byteLength,n=0;n1?n+a:i;if(bt(t.subarray(n+4,n+8))===e[0])if(1===e.length)r.push(t.subarray(n+8,s));else{var o=xt(t.subarray(n+8,s),e.slice(1));o.length&&Rt.apply(r,o)}n=s}return r}function Pt(t){var e=[],r=t[0],i=8,n=It(t,i);i+=4;var a=0,s=0;0===r?(a=It(t,i),s=It(t,i+4),i+=8):(a=wt(t,i),s=wt(t,i+8),i+=16),i+=2;var o=t.length+s,l=Dt(t,i);i+=2;for(var u=0;u>>31)return w.warn("SIDX has hierarchical references (not supported)"),null;var f=It(t,h);h+=4,e.push({referenceSize:c,subsegmentDuration:f,info:{duration:f/n,start:o,end:o+c-1}}),o+=c,i=h+=4}return{earliestPresentationTime:a,timescale:n,version:r,referencesCount:l,references:e}}function Ft(t){for(var e=[],r=xt(t,["moov","trak"]),n=0;n12){var h=4;if(3!==u[h++])break;h=Ot(u,h),h+=2;var d=u[h++];if(128&d&&(h+=2),64&d&&(h+=u[h++]),4!==u[h++])break;h=Ot(u,h);var c=u[h++];if(64!==c)break;if(n+="."+Nt(c),h+=12,5!==u[h++])break;h=Ot(u,h);var f=u[h++],g=(248&f)>>3;31===g&&(g+=1+((7&f)<<3)+((224&u[h])>>5)),n+="."+g}break;case"hvc1":case"hev1":var v=xt(r,["hvcC"])[0],m=v[1],p=["","A","B","C"][m>>6],y=31&m,E=It(v,2),T=(32&m)>>5?"H":"L",S=v[12],L=v.subarray(6,12);n+="."+p+y,n+="."+E.toString(16).toUpperCase(),n+="."+T+S;for(var A="",R=L.length;R--;){var k=L[R];(k||A)&&(A="."+k.toString(16).toUpperCase()+A)}n+=A;break;case"dvh1":case"dvhe":var b=xt(r,["dvcC"])[0],D=b[2]>>1&127,I=b[2]<<5&32|b[3]>>3&31;n+="."+Ut(D)+"."+Ut(I);break;case"vp09":var w=xt(r,["vpcC"])[0],C=w[4],_=w[5],x=w[6]>>4&15;n+="."+Ut(C)+"."+Ut(_)+"."+Ut(x);break;case"av01":var P=xt(r,["av1C"])[0],F=P[1]>>>5,M=31&P[1],O=P[2]>>>7?"H":"M",N=(64&P[2])>>6,U=(32&P[2])>>5,B=2===F&&N?U?12:10:N?10:8,G=(16&P[2])>>4,K=(8&P[2])>>3,H=(4&P[2])>>2,V=3&P[2];n+="."+F+"."+Ut(M)+O+"."+Ut(B)+"."+G+"."+K+H+V+"."+Ut(1)+"."+Ut(1)+"."+Ut(1)+".0"}return{codec:n,encrypted:a}}function Ot(t,e){for(var r=e+5;128&t[e++]&&e>1&63;return 39===r||40===r}return 6==(31&e)}function Yt(t,e,r,i){var n=Wt(t),a=0;a+=e;for(var s=0,o=0,l=0;a=n.length)break;s+=l=n[a++]}while(255===l);o=0;do{if(a>=n.length)break;o+=l=n[a++]}while(255===l);var u=n.length-a,h=a;if(ou){w.error("Malformed SEI payload. "+o+" is too small, only "+u+" bytes left to parse.");break}if(4===s){if(181===n[h++]){var d=Dt(n,h);if(h+=2,49===d){var c=It(n,h);if(h+=4,1195456820===c){var f=n[h++];if(3===f){var g=n[h++],v=64&g,m=v?2+3*(31&g):0,p=new Uint8Array(m);if(v){p[0]=g;for(var y=1;y16){for(var E=[],T=0;T<16;T++){var S=n[h++].toString(16);E.push(1==S.length?"0"+S:S),3!==T&&5!==T&&7!==T&&9!==T||E.push("-")}for(var L=o-16,A=new Uint8Array(L),R=0;R0?(a=new Uint8Array(4),e.length>0&&new DataView(a.buffer).setUint32(0,e.length,!1)):a=new Uint8Array;var l=new Uint8Array(4);return r&&r.byteLength>0&&new DataView(l.buffer).setUint32(0,r.byteLength,!1),function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(t,4),s=0,a=8;s>8*(15-r)&255;return e}(e);return new t(this.method,this.uri,"identity",this.keyFormatVersions,r)}var i=Y(this.uri);if(i)switch(this.keyFormat){case J:this.pssh=i,i.length>=22&&(this.keyId=i.subarray(i.length-22,i.length-6));break;case Q:var n=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=jt(n,null,i);var a=new Uint16Array(i.buffer,i.byteOffset,i.byteLength/2),s=String.fromCharCode.apply(null,Array.from(a)),o=s.substring(s.indexOf("<"),s.length),l=(new DOMParser).parseFromString(o,"text/xml").getElementsByTagName("KID")[0];if(l){var u=l.childNodes[0]?l.childNodes[0].nodeValue:l.getAttribute("VALUE");if(u){var h=V(u).subarray(0,16);!function(t){var e=function(t,e,r){var i=t[e];t[e]=t[r],t[r]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(h),this.keyId=h}}break;default:var d=i.subarray(0,16);if(16!==d.length){var c=new Uint8Array(16);c.set(d,16-d.length),d=c}this.keyId=d}if(!this.keyId||16!==this.keyId.byteLength){var f=qt[this.uri];if(!f){var g=Object.keys(qt).length%Number.MAX_SAFE_INTEGER;f=new Uint8Array(16),new DataView(f.buffer,12,4).setUint32(0,g),qt[this.uri]=f}this.keyId=f}return this},t}(),zt=/\{\$([a-zA-Z0-9-_]+)\}/g;function Qt(t){return zt.test(t)}function Jt(t,e,r){if(null!==t.variableList||t.hasVariableRefs)for(var i=r.length;i--;){var n=r[i],a=e[n];a&&(e[n]=$t(t,a))}}function $t(t,e){if(null!==t.variableList||t.hasVariableRefs){var r=t.variableList;return e.replace(zt,(function(e){var i=e.substring(2,e.length-1),n=null==r?void 0:r[i];return void 0===n?(t.playlistParsingError||(t.playlistParsingError=new Error('Missing preceding EXT-X-DEFINE tag for Variable Reference: "'+i+'"')),e):n}))}return e}function Zt(t,e,r){var i,n,a=t.variableList;if(a||(t.variableList=a={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{var s=new self.URL(r).searchParams;if(!s.has(i))throw new Error('"'+i+'" does not match any query parameter in URI: "'+r+'"');n=s.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error("EXT-X-DEFINE QUERYPARAM: "+e.message))}}else i=e.NAME,n=e.VALUE;i in a?t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE duplicate Variable Name declarations: "'+i+'"')):a[i]=n||""}function te(t,e,r){var i=e.IMPORT;if(r&&i in r){var n=t.variableList;n||(t.variableList=n={}),n[i]=r[i]}else t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "'+i+'"'))}function ee(t){if(void 0===t&&(t=!0),"undefined"!=typeof self)return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}var re={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function ie(t,e,r){return void 0===r&&(r=!0),!t.split(",").some((function(t){return!ne(t,e,r)}))}function ne(t,e,r){var i;void 0===r&&(r=!0);var n=ee(r);return null!=(i=null==n?void 0:n.isTypeSupported(ae(t,e)))&&i}function ae(t,e){return e+'/mp4;codecs="'+t+'"'}function se(t){if(t){var e=t.substring(0,4);return re.video[e]}return 2}function oe(t){return t.split(",").reduce((function(t,e){var r=re.video[e];return r?(2*r+t)/(t?3:2):(re.audio[e]+t)/(t?2:1)}),0)}var le={},ue=/flac|opus/i;function he(t,e){return void 0===e&&(e=!0),t.replace(ue,(function(t){return function(t,e){if(void 0===e&&(e=!0),le[t])return le[t];for(var r={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t],i=0;i0&&a.length0&&X.bool("CAN-SKIP-DATERANGES"),h.partHoldBack=X.optionalFloat("PART-HOLD-BACK",0),h.holdBack=X.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var z=new x(I);h.partTarget=z.decimalFloatingPoint("PART-TARGET");break;case"PART":var Q=h.partList;Q||(Q=h.partList=[]);var J=g>0?Q[Q.length-1]:void 0,$=g++,Z=new x(I);Jt(h,Z,["BYTERANGE","URI"]);var tt=new K(Z,E,e,$,J);Q.push(tt),E.duration+=tt.duration;break;case"PRELOAD-HINT":var et=new x(I);Jt(h,et,["URI"]),h.preloadHint=et;break;case"RENDITION-REPORT":var rt=new x(I);Jt(h,rt,["URI"]),h.renditionReports=h.renditionReports||[],h.renditionReports.push(rt);break;default:w.warn("line parsed but not handled: "+s)}}}p&&!p.relurl?(d.pop(),v-=p.duration,h.partList&&(h.fragmentHint=p)):h.partList&&(Le(E,p),E.cc=m,h.fragmentHint=E,u&&Re(E,u,h));var it=d.length,nt=d[0],at=d[it-1];if((v+=h.skippedSegments*h.targetduration)>0&&it&&at){h.averagetargetduration=v/it;var st=at.sn;h.endSN="initSegment"!==st?st:0,h.live||(at.endList=!0),nt&&(h.startCC=nt.cc)}else h.endSN=0,h.startCC=0;return h.fragmentHint&&(v+=h.fragmentHint.duration),h.totalduration=v,h.endCC=m,T>0&&function(t,e){for(var r=t[e],i=e;i--;){var n=t[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(d,T),h},t}();function ye(t,e,r){var i,n,a=new x(t);Jt(r,a,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);var s=null!=(i=a.METHOD)?i:"",o=a.URI,l=a.hexadecimalInteger("IV"),u=a.KEYFORMATVERSIONS,h=null!=(n=a.KEYFORMAT)?n:"identity";o&&a.IV&&!l&&w.error("Invalid IV: "+a.IV);var d=o?pe.resolve(o,e):"",c=(u||"1").split("/").map(Number).filter(Number.isFinite);return new Xt(s,d,h,c,l)}function Ee(t){var e=new x(t).decimalFloatingPoint("TIME-OFFSET");return y(e)?e:null}function Te(t,e){var r=(t||"").split(/[ ,]+/).filter((function(t){return t}));["video","audio","text"].forEach((function(t){var i=r.filter((function(e){return function(t,e){var r=re[e];return!!r&&!!r[t.slice(0,4)]}(e,t)}));i.length&&(e[t+"Codec"]=i.join(","),r=r.filter((function(t){return-1===i.indexOf(t)})))})),e.unknownCodecs=r}function Se(t,e,r){var i=e[r];i&&(t[r]=i)}function Le(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),y(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Ae(t,e,r,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=r,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function Re(t,e,r){t.levelkeys=e;var i=r.encryptedFragments;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((function(t){return e[t].isCommonEncryption}))||i.push(t)}var ke="manifest",be="level",De="audioTrack",Ie="subtitleTrack",we="main",Ce="audio",_e="subtitle";function xe(t){switch(t.type){case De:return Ce;case Ie:return _e;default:return we}}function Pe(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var Fe=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}var e=t.prototype;return e.startLoad=function(t){},e.stopLoad=function(){this.destroyInternalLoaders()},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,n=new(r||i)(e);return this.loaders[t.type]=n,n},e.getInternalLoader=function(t){return this.loaders[t.type]},e.resetInternalLoader=function(t){this.loaders[t]&&delete this.loaders[t]},e.destroyInternalLoaders=function(){for(var t in this.loaders){var e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}},e.destroy=function(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:ke,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,n=e.pathwayId,a=e.url,s=e.deliveryDirectives;this.load({id:r,level:i,pathwayId:n,responseType:"text",type:be,url:a,deliveryDirectives:s})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:De,url:n,deliveryDirectives:a})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:Ie,url:n,deliveryDirectives:a})},e.load=function(t){var e,r,i,n=this,a=this.hls.config,s=this.getInternalLoader(t);if(s){var l=s.context;if(l&&l.url===t.url&&l.level===t.level)return void w.trace("[playlist-loader]: playlist request ongoing");w.log("[playlist-loader]: aborting previous loader for type: "+t.type),s.abort()}if(r=t.type===ke?a.manifestLoadPolicy.default:o({},a.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),s=this.createInternalLoader(t),y(null==(e=t.deliveryDirectives)?void 0:e.part)&&(t.type===be&&null!==t.level?i=this.hls.levels[t.level].details:t.type===De&&null!==t.id?i=this.hls.audioTracks[t.id].details:t.type===Ie&&null!==t.id&&(i=this.hls.subtitleTracks[t.id].details),i)){var u=i.partTarget,h=i.targetduration;if(u&&h){var d=1e3*Math.max(3*u,.8*h);r=o({},r,{maxTimeToFirstByteMs:Math.min(d,r.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(d,r.maxTimeToFirstByteMs)})}}var c=r.errorRetry||r.timeoutRetry||{},f={loadPolicy:r,timeout:r.maxLoadTimeMs,maxRetry:c.maxNumRetry||0,retryDelay:c.retryDelayMs||0,maxRetryDelay:c.maxRetryDelayMs||0},g={onSuccess:function(t,e,r,i){var a=n.getInternalLoader(r);n.resetInternalLoader(r.type);var s=t.data;0===s.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),pe.isMediaPlaylist(s)?n.handleTrackOrLevelPlaylist(t,e,r,i||null,a):n.handleMasterPlaylist(t,e,r,i)):n.handleManifestParsingError(t,r,new Error("no EXTM3U delimiter"),i||null,e)},onError:function(t,e,r,i){n.handleNetworkError(e,r,!1,t,i)},onTimeout:function(t,e,r){n.handleNetworkError(e,r,!0,void 0,t)}};s.load(t,f,g)},e.handleMasterPlaylist=function(t,e,r,i){var n=this.hls,a=t.data,s=Pe(t,r),o=pe.parseMasterPlaylist(a,s);if(o.playlistParsingError)this.handleManifestParsingError(t,r,o.playlistParsingError,i,e);else{var l=o.contentSteering,u=o.levels,h=o.sessionData,d=o.sessionKeys,c=o.startTimeOffset,f=o.variableList;this.variableList=f;var g=pe.parseMasterPlaylistMedia(a,s,o),v=g.AUDIO,m=void 0===v?[]:v,p=g.SUBTITLES,y=g["CLOSED-CAPTIONS"];m.length&&(m.some((function(t){return!t.url}))||!u[0].audioCodec||u[0].attrs.AUDIO||(w.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),m.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new x({}),bitrate:0,url:""}))),n.trigger(S.MANIFEST_LOADED,{levels:u,audioTracks:m,subtitles:p,captions:y,contentSteering:l,url:s,stats:e,networkDetails:i,sessionData:h,sessionKeys:d,startTimeOffset:c,variableList:f})}},e.handleTrackOrLevelPlaylist=function(t,e,r,i,n){var a=this.hls,s=r.id,o=r.level,l=r.type,u=Pe(t,r),h=y(o)?o:y(s)?s:0,d=xe(r),c=pe.parseLevelPlaylist(t.data,u,h,d,0,this.variableList);if(l===ke){var f={attrs:new x({}),bitrate:0,details:c,name:"",url:u};a.trigger(S.MANIFEST_LOADED,{levels:[f],audioTracks:[],url:u,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),r.levelDetails=c,this.handlePlaylistLoaded(c,t,e,r,i,n)},e.handleManifestParsingError=function(t,e,r,i,n){this.hls.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.MANIFEST_PARSING_ERROR,fatal:e.type===ke,url:t.url,err:r,error:r,reason:r.message,response:t,context:e,networkDetails:i,stats:n})},e.handleNetworkError=function(t,e,r,n,a){void 0===r&&(r=!1);var s="A network "+(r?"timeout":"error"+(n?" (status "+n.code+")":""))+" occurred while loading "+t.type;t.type===be?s+=": "+t.level+" id: "+t.id:t.type!==De&&t.type!==Ie||(s+=" id: "+t.id+' group-id: "'+t.groupId+'"');var o=new Error(s);w.warn("[playlist-loader]: "+s);var l=A.UNKNOWN,u=!1,h=this.getInternalLoader(t);switch(t.type){case ke:l=r?A.MANIFEST_LOAD_TIMEOUT:A.MANIFEST_LOAD_ERROR,u=!0;break;case be:l=r?A.LEVEL_LOAD_TIMEOUT:A.LEVEL_LOAD_ERROR,u=!1;break;case De:l=r?A.AUDIO_TRACK_LOAD_TIMEOUT:A.AUDIO_TRACK_LOAD_ERROR,u=!1;break;case Ie:l=r?A.SUBTITLE_TRACK_LOAD_TIMEOUT:A.SUBTITLE_LOAD_ERROR,u=!1}h&&this.resetInternalLoader(t.type);var d={type:L.NETWORK_ERROR,details:l,fatal:u,url:t.url,loader:h,context:t,error:o,networkDetails:e,stats:a};if(n){var c=(null==e?void 0:e.url)||t.url;d.response=i({url:c,data:void 0},n)}this.hls.trigger(S.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i,n,a){var s=this.hls,o=i.type,l=i.level,u=i.id,h=i.groupId,d=i.deliveryDirectives,c=Pe(e,i),f=xe(i),g="number"==typeof i.level&&f===we?l:void 0;if(t.fragments.length){t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));var v=t.playlistParsingError;if(v)s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_PARSING_ERROR,fatal:!1,url:c,error:v,reason:v.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r});else switch(t.live&&a&&(a.getCacheAge&&(t.ageHeader=a.getCacheAge()||0),a.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case ke:case be:s.trigger(S.LEVEL_LOADED,{details:t,level:g||0,id:u||0,stats:r,networkDetails:n,deliveryDirectives:d});break;case De:s.trigger(S.AUDIO_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d});break;case Ie:s.trigger(S.SUBTITLE_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d})}}else{var m=new Error("No Segments found in Playlist");s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r})}},t}();function Me(t,e){var r;try{r=new Event("addtrack")}catch(t){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=t,e.dispatchEvent(r)}function Oe(t,e){var r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error("addCue is failed for: "+e)}catch(r){w.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(e.startTime,e.endTime,e.text);i.id=e.id,t.addCue(i)}catch(t){w.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+t)}}"disabled"===r&&(t.mode=r)}function Ne(t){var e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(var r=t.cues.length;r--;)t.removeCue(t.cues[r]);"disabled"===e&&(t.mode=e)}function Ue(t,e,r,i){var n=t.mode;if("disabled"===n&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],n=function(t,e){if(et[r].endTime)return-1;for(var i=0,n=r;i<=n;){var a=Math.floor((n+i)/2);if(et[a].startTime&&i-1)for(var a=n,s=t.length;a=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),s=0;sWe&&(d=We),d-h<=0&&(d=h+.25);for(var c=0;ce.startDate&&(!t||e.startDate.05&&this.forwardBufferLength>1){var l=Math.min(2,Math.max(1,a)),u=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(l,Math.max(1,u))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},e.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},e.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},s(t,[{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var t=this.config,e=this.levelDetails;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}},{key:"targetLatency",get:function(){var t=this.levelDetails;if(null===t)return null;var e=t.holdBack,r=t.partHoldBack,i=t.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var h=i;return u+Math.min(1*this.stallCount,h)}},{key:"liveSyncPosition",get:function(){var t=this.estimateLiveEdge(),e=this.targetLatency,r=this.levelDetails;if(null===t||null===e||null===r)return null;var i=r.edge,n=t-e-this.edgeStalled,a=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(a,n),s)}},{key:"drift",get:function(){var t=this.levelDetails;return null===t?1:t.drift}},{key:"edgeStalled",get:function(){var t=this.levelDetails;if(null===t)return 0;var e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}},{key:"forwardBufferLength",get:function(){var t=this.media,e=this.levelDetails;if(!t||!e)return 0;var r=t.buffered.length;return(r?t.buffered.end(r-1):e.edge)-this.currentTime}}]),t}(),ze=["NONE","TYPE-0","TYPE-1",null],Qe=["SDR","PQ","HLG"],Je="",$e="YES",Ze="v2";function tr(t){var e=t.canSkipUntil,r=t.canSkipDateRanges,i=t.age;return e&&it.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else e.sn>t.sn?t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration:e.start=Math.max(t.start-e.duration,0)}function ar(t,e,r,i,n,a){i-r<=0&&(w.warn("Fragment should have a positive duration",e),i=r+e.duration,a=n+e.duration);var s=r,o=i,l=e.startPTS,u=e.endPTS;if(y(l)){var h=Math.abs(l-r);y(e.deltaPTS)?e.deltaPTS=Math.max(h,e.deltaPTS):e.deltaPTS=h,s=Math.max(r,l),r=Math.min(r,l),n=Math.min(n,e.startDTS),o=Math.min(i,u),i=Math.max(i,u),a=Math.max(a,e.endDTS)}var d=r-e.start;0!==e.start&&(e.start=r),e.duration=i-e.start,e.startPTS=r,e.maxStartPTS=s,e.startDTS=n,e.endPTS=i,e.minEndPTS=o,e.endDTS=a;var c,f=e.sn;if(!t||ft.endSN)return 0;var g=f-t.startSN,v=t.fragments;for(v[g]=e,c=g;c>0;c--)nr(v[c],v[c-1]);for(c=g;c=0;n--){var a=i[n].initSegment;if(a){r=a;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var s,l,u,h,d,c=0;if(function(t,e,r){for(var i=e.skippedSegments,n=Math.max(t.startSN,e.startSN)-e.startSN,a=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,s=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,u=n;u<=a;u++){var h=l[s+u],d=o[u];i&&!d&&u=i.length||lr(e,i[r].start)}function lr(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;i499)}(n)||!!r);return t.shouldRetry?t.shouldRetry(t,e,r,i,a):a}var pr=function(t,e){for(var r=0,i=t.length-1,n=null,a=null;r<=i;){var s=e(a=t[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function yr(t,e,r,i){void 0===r&&(r=0),void 0===i&&(i=0);var n=null;if(t){n=e[t.sn-e[0].sn+1]||null;var a=t.endDTS-r;a>0&&a<15e-7&&(r+=15e-7)}else 0===r&&0===e[0].start&&(n=e[0]);if(n&&(!t||t.level===n.level)&&0===Er(r,i,n))return n;var s=pr(e,Er.bind(null,r,i));return!s||s===t&&n?n:s}function Er(t,e,r){if(void 0===t&&(t=0),void 0===e&&(e=0),r.start<=t&&r.start+r.duration>t)return 0;var i=Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=t?1:r.start-i>t&&r.start?-1:0}function Tr(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}var Sr=0,Lr=2,Ar=3,Rr=5,kr=0,br=1,Dr=2,Ir=function(){function t(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=w.log.bind(w,"[info]:"),this.warn=w.warn.bind(w,"[warning]:"),this.error=w.error.bind(w,"[error]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.ERROR,this.onError,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.ERROR,this.onError,this),t.off(S.ERROR,this.onErrorOut,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this))},e.destroy=function(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}},e.startLoad=function(t){},e.stopLoad=function(){this.playlistError=0},e.getVariantLevelIndex=function(t){return(null==t?void 0:t.type)===we?t.level:this.hls.loadLevel},e.onManifestLoading=function(){this.playlistError=0,this.penalizedRenditions={}},e.onLevelUpdated=function(){this.playlistError=0},e.onError=function(t,e){var r,i;if(!e.fatal){var n=this.hls,a=e.context;switch(e.details){case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case A.FRAG_PARSING_ERROR:if(null!=(r=e.frag)&&r.gap)return void(e.errorAction={action:Sr,flags:kr});case A.FRAG_GAP:case A.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Lr);case A.LEVEL_EMPTY_ERROR:case A.LEVEL_PARSING_ERROR:var s,o,l=e.parent===we?e.level:n.loadLevel;return void(e.details===A.LEVEL_EMPTY_ERROR&&null!=(s=e.context)&&null!=(o=s.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,l):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,l)));case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==a?void 0:a.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,a.level)));case A.AUDIO_TRACK_LOAD_ERROR:case A.AUDIO_TRACK_LOAD_TIMEOUT:case A.SUBTITLE_LOAD_ERROR:case A.SUBTITLE_TRACK_LOAD_TIMEOUT:if(a){var u=n.levels[n.loadLevel];if(u&&(a.type===De&&u.hasAudioGroup(a.groupId)||a.type===Ie&&u.hasSubtitleGroup(a.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.loadLevel),e.errorAction.action=Lr,void(e.errorAction.flags=br)}return;case A.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:var h=n.levels[n.loadLevel],d=null==h?void 0:h.attrs["HDCP-LEVEL"];return void(d?e.errorAction={action:Lr,flags:Dr,hdcpLevel:d}:this.keySystemError(e));case A.BUFFER_ADD_CODEC_ERROR:case A.REMUX_ALLOC_ERROR:case A.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:n.loadLevel));case A.INTERNAL_EXCEPTION:case A.BUFFER_APPENDING_ERROR:case A.BUFFER_FULL_ERROR:case A.LEVEL_SWITCH_ERROR:case A.BUFFER_STALLED_ERROR:case A.BUFFER_SEEK_OVER_HOLE:case A.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:Sr,flags:kr})}e.type===L.KEY_SYSTEM_ERROR&&this.keySystemError(e)}},e.keySystemError=function(t){var e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)},e.getPlaylistRetryOrSwitchAction=function(t,e){var r=fr(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(mr(r,i,cr(t),t.response))return{action:Rr,flags:kr,retryConfig:r,retryCount:i};var n=this.getLevelSwitchAction(t,e);return r&&(n.retryConfig=r,n.retryCount=i),n},e.getFragRetryOrSwitchAction=function(t){var e=this.hls,r=this.getVariantLevelIndex(t.frag),i=e.levels[r],n=e.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=fr(t.details.startsWith("key")?s:a,t),l=e.levels.reduce((function(t,e){return t+e.fragmentError}),0);if(i&&(t.details!==A.FRAG_GAP&&i.fragmentError++,mr(o,l,cr(t),t.response)))return{action:Rr,flags:kr,retryConfig:o,retryCount:l};var u=this.getLevelSwitchAction(t,r);return o&&(u.retryConfig=o,u.retryCount=l),u},e.getLevelSwitchAction=function(t,e){var r=this.hls;null==e&&(e=r.loadLevel);var i=this.hls.levels[e];if(i){var n,a,s=t.details;i.loadError++,s===A.BUFFER_APPEND_ERROR&&i.fragmentError++;var o=-1,l=r.levels,u=r.loadLevel,h=r.minAutoLevel,d=r.maxAutoLevel;r.autoLevelEnabled||(r.loadLevel=-1);for(var c,f=null==(n=t.frag)?void 0:n.type,g=(f===Ce&&s===A.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR))&&l.some((function(t){var e=t.audioCodec;return i.audioCodec!==e})),v="video"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR)&&l.some((function(t){var e=t.codecSet,r=t.audioCodec;return i.codecSet!==e&&i.audioCodec===r})),m=null!=(a=t.context)?a:{},p=m.type,y=m.groupId,E=function(){var e=(T+u)%l.length;if(e!==u&&e>=h&&e<=d&&0===l[e].loadError){var r,n,a=l[e];if(s===A.FRAG_GAP&&t.frag){var c=l[e].details;if(c){var m=yr(t.frag,c.fragments,t.frag.start);if(null!=m&&m.gap)return 0}}else{if(p===De&&a.hasAudioGroup(y)||p===Ie&&a.hasSubtitleGroup(y))return 0;if(f===Ce&&null!=(r=i.audioGroups)&&r.some((function(t){return a.hasAudioGroup(t)}))||f===_e&&null!=(n=i.subtitleGroups)&&n.some((function(t){return a.hasSubtitleGroup(t)}))||g&&i.audioCodec===a.audioCodec||!g&&i.audioCodec!==a.audioCodec||v&&i.codecSet===a.codecSet)return 0}return o=e,1}},T=l.length;T--&&(0===(c=E())||1!==c););if(o>-1&&r.loadLevel!==o)return t.levelRetry=!0,this.playlistError=0,{action:Lr,flags:kr,nextAutoLevel:o}}return{action:Lr,flags:br}},e.onErrorOut=function(t,e){var r;switch(null==(r=e.errorAction)?void 0:r.action){case Sr:break;case Lr:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===A.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn('MediaSource ended after "'+e.sourceBufferName+'" sourceBuffer append error. Attempting to recover from media error.'),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()},e.sendAlternateToPenaltyBox=function(t){var e=this.hls,r=t.errorAction;if(r){var i=r.flags,n=r.hdcpLevel,a=r.nextAutoLevel;switch(i){case kr:this.switchLevel(t,a);break;case Dr:n&&(e.maxHdcpLevel=ze[ze.indexOf(n)-1],r.resolved=!0),this.warn('Restricting playback to HDCP-LEVEL of "'+e.maxHdcpLevel+'" or lower')}r.resolved||this.switchLevel(t,a)}},e.switchLevel=function(t,e){void 0!==e&&t.errorAction&&(this.warn("switching to level "+e+" after "+t.details),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)},t}(),wr=function(){function t(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=w.log.bind(w,e+":"),this.warn=w.warn.bind(w,e+":"),this.hls=t}var e=t.prototype;return e.destroy=function(){this.clearTimer(),this.hls=this.log=this.warn=null},e.clearTimer=function(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)},e.startLoad=function(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()},e.stopLoad=function(){this.canLoad=!1,this.clearTimer()},e.switchParams=function(t,e,r){var i=null==e?void 0:e.renditionReports;if(i){for(var n=-1,a=0;a=0&&d>e.partTarget&&(h+=1)}var c=r&&tr(r);return new er(u,h>=0?h:void 0,c)}}},e.loadPlaylist=function(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())},e.shouldLoadPlaylist=function(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)},e.shouldReloadPlaylist=function(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)},e.playlistLoaded=function(t,e,r){var i=this,n=e.details,a=e.stats,s=self.performance.now(),o=a.loading.first?Math.max(0,s-a.loading.first):0;if(n.advancedDateTime=Date.now()-o,n.live||null!=r&&r.live){if(n.reloaded(r),r&&this.log("live playlist "+t+" "+(n.advanced?"REFRESHED "+n.lastPartSn+"-"+n.lastPartIndex:n.updated?"UPDATED":"MISSED")),r&&n.fragments.length>0&&sr(r,n),!this.canLoad||!n.live)return;var l,u=void 0,h=void 0;if(n.canBlockReload&&n.endSN&&n.advanced){var d=this.hls.config.lowLatencyMode,c=n.lastPartSn,f=n.endSN,g=n.lastPartIndex,v=c===f;-1!==g?(u=v?f+1:c,h=v?d?0:g:g+1):u=f+1;var m=n.age,p=m+n.ageHeader,y=Math.min(p-n.partTarget,1.5*n.targetduration);if(y>0){if(r&&y>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+y+" with playlist age: "+n.age),y=0;else{var E=Math.floor(y/n.targetduration);u+=E,void 0!==h&&(h+=Math.round(y%n.targetduration/n.partTarget)),this.log("CDN Tune-in age: "+n.ageHeader+"s last advanced "+m.toFixed(2)+"s goal: "+y+" skip sn "+E+" to part "+h)}n.tuneInGoal=y}if(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h),d||!v)return void this.loadPlaylist(l)}else(n.canBlockReload||n.canSkipUntil)&&(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h));var T=this.hls.mainForwardBufferInfo,S=T?T.end-T.len:0,L=function(t,e){void 0===e&&(e=1/0);var r=1e3*t.targetduration;if(t.updated){var i=t.fragments;if(i.length&&4*r>e){var n=1e3*i[i.length-1].duration;nthis.requestScheduled+L&&(this.requestScheduled=a.loading.start),void 0!==u&&n.canBlockReload?this.requestScheduled=a.loading.first+L-(1e3*n.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+L=u.maxNumRetry)return!1;if(i&&null!=(d=t.context)&&d.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var c=gr(u,l);this.timer=self.setTimeout((function(){return e.loadPlaylist()}),c),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+c+"ms")}t.levelRetry=!0,n.resolved=!0}return h},t}(),Cr=function(){function t(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0),this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=r}var e=t.prototype;return e.sample=function(t,e){var r=Math.pow(this.alpha_,t);this.estimate_=e*(1-r)+r*this.estimate_,this.totalWeight_+=t},e.getTotalWeight=function(){return this.totalWeight_},e.getEstimate=function(){if(this.alpha_){var t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_},t}(),_r=function(){function t(t,e,r,i){void 0===i&&(i=100),this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Cr(t),this.fast_=new Cr(e),this.defaultTTFB_=i,this.ttfb_=new Cr(t)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_,n=this.ttfb_;r.halfLife!==t&&(this.slow_=new Cr(t,r.getEstimate(),r.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Cr(e,i.getEstimate(),i.getTotalWeight())),n.halfLife!==t&&(this.ttfb_=new Cr(t,n.getEstimate(),n.getTotalWeight()))},e.sample=function(t,e){var r=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/r;this.fast_.sample(r,i),this.slow_.sample(r,i)},e.sampleTTFB=function(t){var e=t/1e3,r=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(r,Math.max(t,5))},e.canEstimate=function(){return this.fast_.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},e.destroy=function(){},t}(),xr={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},Pr={};function Fr(t,e,r){var n=t.videoCodec,a=t.audioCodec;if(!n||!a||!r)return Promise.resolve(xr);var s={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},o=t.videoRange;"SDR"!==o&&(s.transferFunction=o.toLowerCase());var l=n.split(",").map((function(t){return{type:"media-source",video:i(i({},s),{},{contentType:ae(t,"video")})}}));return a&&t.audioGroups&&t.audioGroups.forEach((function(t){var r;t&&(null==(r=e.groups[t])||r.tracks.forEach((function(e){if(e.groupId===t){var r=e.channels||"",i=parseFloat(r);y(i)&&i>2&&l.push.apply(l,a.split(",").map((function(t){return{type:"media-source",audio:{contentType:ae(t,"audio"),channels:""+i}}})))}})))})),Promise.all(l.map((function(t){var e=function(t){var e=t.audio,r=t.video,i=r||e;if(i){var n=i.contentType.split('"')[1];if(r)return"r"+r.height+"x"+r.width+"f"+Math.ceil(r.framerate)+(r.transferFunction||"sd")+"_"+n+"_"+Math.ceil(r.bitrate/1e5);if(e)return"c"+e.channels+(e.spatialRendering?"s":"n")+"_"+n}return""}(t);return Pr[e]||(Pr[e]=r.decodingInfo(t))}))).then((function(t){return{supported:!t.some((function(t){return!t.supported})),configurations:l,decodingInfoResults:t}})).catch((function(t){return{supported:!1,configurations:l,decodingInfoResults:[],error:t}}))}function Mr(t,e){var r=!1,i=[];return t&&(r="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||Qe.slice(0),i=(r=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){var t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}())?i.filter((function(t){return"SDR"!==t})):["SDR"]),{preferHDR:r,allowedVideoRanges:i}}function Or(t,e){w.log('[abr] start candidates with "'+t+'" ignored because '+e)}function Nr(t,e,r){if("attrs"in t){var i=e.indexOf(t);if(-1!==i)return i}for(var n=0;n-1,p=e.getBwEstimate(),E=i.levels,T=E[t.level],L=o.total||Math.max(o.loaded,Math.round(l*T.averageBitrate/8)),A=m?u-v:u;A<1&&m&&(A=Math.min(u,8*o.loaded/p));var R=m?1e3*o.loaded/A:0,k=R?(L-o.loaded)/R:8*L/p+c/1e3;if(!(k<=g)){var b,D=R?8*R:p,I=Number.POSITIVE_INFINITY;for(b=t.level-1;b>h;b--){var C=E[b].maxBitrate;if((I=e.getTimeToLoadFrag(c/1e3,D,l*C,!E[b].details))=k||I>10*l)){i.nextLoadLevel=i.nextAutoLevel=b,m?e.bwEstimator.sample(u-Math.min(c,v),o.loaded):e.bwEstimator.sampleTTFB(u);var _=E[b].maxBitrate;e.getBwEstimate()*e.hls.config.abrBandWidthUpFactor>_&&e.resetEstimator(_),e.clearTimer(),w.warn("[abr] Fragment "+t.sn+(r?" part "+r.index:"")+" of level "+t.level+" is loading too slowly;\n Time to underbuffer: "+g.toFixed(3)+" s\n Estimated load time for current fragment: "+k.toFixed(3)+" s\n Estimated load time for down switch fragment: "+I.toFixed(3)+" s\n TTFB estimate: "+(0|v)+" ms\n Current BW estimate: "+(y(p)?0|p:"Unknown")+" bps\n New BW estimate: "+(0|e.getBwEstimate())+" bps\n Switching to level "+b+" @ "+(0|_)+" bps"),i.trigger(S.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:r,stats:o})}}}}}}},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}var e=t.prototype;return e.resetEstimator=function(t){t&&(w.log("setting initial bwe to "+t),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()},e.initEstimator=function(){var t=this.hls.config;return new _r(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(S.ERROR,this.onError,this))},e.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null},e.onManifestLoading=function(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()},e.onLevelsUpdated=function(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null},e.onMaxAutoLevelUpdated=function(){this.firstSelection=-1,this.nextAutoLevelKey=""},e.onFragLoading=function(t,e){var r,i=e.frag;this.ignoreFragment(i)||(i.bitrateTest||(this.fragCurrent=i,this.partCurrent=null!=(r=e.part)?r:null),this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100))},e.onLevelSwitching=function(t,e){this.clearTimer()},e.onError=function(t,e){if(!e.fatal)switch(e.details){case A.BUFFER_ADD_CODEC_ERROR:case A.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case A.FRAG_LOAD_TIMEOUT:var r=e.frag,i=this.fragCurrent,n=this.partCurrent;if(r&&i&&r.sn===i.sn&&r.level===i.level){var a=performance.now(),s=n?n.stats:r.stats,o=a-s.loading.start,l=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&l>-1){var u=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(o-Math.min(u,l),s.loaded)}else this.bwEstimator.sampleTTFB(o)}}},e.getTimeToLoadFrag=function(t,e,r,i){return t+r/e+(i?this.lastLevelLoadSec:0)},e.onLevelLoaded=function(t,e){var r=this.hls.config,i=e.stats.loading,n=i.end-i.start;y(n)&&(this.lastLevelLoadSec=n/1e3),e.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD)},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part,n=i?i.stats:r.stats;if(r.type===we&&this.bwEstimator.sampleTTFB(n.loading.first-n.loading.start),!this.ignoreFragment(r)){if(this.clearTimer(),r.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){var a=i?i.duration:r.duration,s=this.hls.levels[r.level],o=(s.loaded?s.loaded.bytes:0)+n.loaded,l=(s.loaded?s.loaded.duration:0)+a;s.loaded={bytes:o,duration:l},s.realBitrate=Math.round(8*o/l)}if(r.bitrateTest){var u={stats:n,frag:r,part:i,id:r.type};this.onFragBuffered(S.FRAG_BUFFERED,u),r.bitrateTest=!1}else this.lastLoadedFragLevel=r.level}},e.onFragBuffered=function(t,e){var r=e.frag,i=e.part,n=null!=i&&i.stats.loaded?i.stats:r.stats;if(!n.aborted&&!this.ignoreFragment(r)){var a=n.parsing.end-n.loading.start-Math.min(n.loading.first-n.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(a,n.loaded),n.bwEstimate=this.getBwEstimate(),r.bitrateTest?this.bitrateTestDelay=a/1e3:this.bitrateTestDelay=0}},e.ignoreFragment=function(t){return t.type!==we||"initSegment"===t.sn},e.clearTimer=function(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)},e.getAutoLevelKey=function(){return this.getBwEstimate()+"_"+this.getStarvationDelay().toFixed(2)},e.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=e?e.duration:t?t.duration:0,o=this.getBwEstimate(),l=this.getStarvationDelay(),u=n.abrBandWidthFactor,h=n.abrBandWidthUpFactor;if(l){var d=this.findBestLevel(o,a,i,l,0,u,h);if(d>=0)return d}var c=s?Math.min(s,n.maxStarvationDelay):n.maxStarvationDelay;if(!l){var f=this.bitrateTestDelay;f&&(c=(s?Math.min(s,n.maxLoadingDelay):n.maxLoadingDelay)-f,w.info("[abr] bitrate test took "+Math.round(1e3*f)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*c)+" ms"),u=h=1)}var g=this.findBestLevel(o,a,i,l,c,u,h);if(w.info("[abr] "+(l?"rebuffering expected":"buffer is empty")+", optimal quality level "+g),g>-1)return g;var v=r.levels[a],m=r.levels[r.loadLevel];return(null==v?void 0:v.bitrate)<(null==m?void 0:m.bitrate)?a:r.loadLevel},e.getStarvationDelay=function(){var t=this.hls,e=t.media;if(!e)return 1/0;var r=e&&0!==e.playbackRate?Math.abs(e.playbackRate):1,i=t.mainForwardBufferInfo;return(i?i.len:0)/r},e.getBwEstimate=function(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate},e.findBestLevel=function(t,e,r,i,n,a,s){var o,l=this,u=i+n,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,c=this.fragCurrent,f=this.partCurrent,g=this.hls,v=g.levels,m=g.allAudioTracks,p=g.loadLevel,E=g.config;if(1===v.length)return 0;var T,S=v[d],L=!(null==S||null==(o=S.details)||!o.live),A=-1===p||-1===h,R="SDR",k=(null==S?void 0:S.frameRate)||0,b=E.audioPreference,D=E.videoPreference,I=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce((function(t,e){var r=t.groups[e.groupId];r||(r=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),r.tracks.push(e);var i=e.channels||"2";return r.channels[i]=(r.channels[i]||0)+1,r.hasDefault=r.hasDefault||e.default,r.hasAutoSelect=r.hasAutoSelect||e.autoselect,r.hasDefault&&(t.hasDefaultAudio=!0),r.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(A){if(-1!==this.firstSelection)return this.firstSelection;var C=this.codecTiers||(this.codecTiers=function(t,e,r,i){return t.slice(r,i+1).reduce((function(t,r){if(!r.codecSet)return t;var i=r.audioGroups,n=t[r.codecSet];n||(t[r.codecSet]=n={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),n.minBitrate=Math.min(n.minBitrate,r.bitrate);var a=Math.min(r.height,r.width);return n.minHeight=Math.min(n.minHeight,a),n.minFramerate=Math.min(n.minFramerate,r.frameRate),n.maxScore=Math.max(n.maxScore,r.score),n.fragmentError+=r.fragmentError,n.videoRanges[r.videoRange]=(n.videoRanges[r.videoRange]||0)+1,i&&i.forEach((function(t){if(t){var r=e.groups[t];r&&(n.hasDefaultAudio=n.hasDefaultAudio||e.hasDefaultAudio?r.hasDefault:r.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(r.channels).forEach((function(t){n.channels[t]=(n.channels[t]||0)+r.channels[t]})))}})),t}),{})}(v,I,e,r)),_=function(t,e,r,i,n){for(var a=Object.keys(t),s=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=s&&2===parseInt(s),u=!0,h=!1,d=1/0,c=1/0,f=1/0,g=0,v=[],m=Mr(e,n),p=m.preferHDR,E=m.allowedVideoRanges,T=function(){var e=t[a[S]];u=e.channels[2]>0,d=Math.min(d,e.minHeight),c=Math.min(c,e.minFramerate),f=Math.min(f,e.minBitrate);var r=E.filter((function(t){return e.videoRanges[t]>0}));r.length>0&&(h=!0,v=r)},S=a.length;S--;)T();d=y(d)?d:0,c=y(c)?c:0;var L=Math.max(1080,d),A=Math.max(30,c);return f=y(f)?f:r,r=Math.max(f,r),h||(e=void 0,v=[]),{codecSet:a.reduce((function(e,i){var n=t[i];if(i===e)return e;if(n.minBitrate>r)return Or(i,"min bitrate of "+n.minBitrate+" > current estimate of "+r),e;if(!n.hasDefaultAudio)return Or(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Or(i,'audio codec preference "'+o+'" not found'),e;if(s&&!l){if(!n.channels[s])return Or(i,"no renditions with "+s+" channel sound found (channels options: "+Object.keys(n.channels)+")"),e}else if((!o||l)&&u&&0===n.channels[2])return Or(i,"no renditions with stereo sound found"),e;return n.minHeight>L?(Or(i,"min resolution of "+n.minHeight+" > maximum of "+L),e):n.minFramerate>A?(Or(i,"min framerate of "+n.minFramerate+" > maximum of "+A),e):v.some((function(t){return n.videoRanges[t]>0}))?n.maxScore=oe(e)||n.fragmentError>t[e].fragmentError)?e:(g=n.maxScore,i):(Or(i,"no variants with VIDEO-RANGE of "+JSON.stringify(v)+" found"),e)}),void 0),videoRanges:v,preferHDR:p,minFramerate:c,minBitrate:f}}(C,R,t,b,D),x=_.codecSet,P=_.videoRanges,F=_.minFramerate,M=_.minBitrate,O=_.preferHDR;T=x,R=O?P[P.length-1]:P[0],k=F,t=Math.max(t,M),w.log("[abr] picked start tier "+JSON.stringify(_))}else T=null==S?void 0:S.codecSet,R=null==S?void 0:S.videoRange;for(var N,U=f?f.duration:c?c.duration:0,B=this.bwEstimator.getEstimateTTFB()/1e3,G=[],K=function(){var e,o=v[H],c=H>d;if(!o)return 0;if(E.useMediaCapabilities&&!o.supportedResult&&!o.supportedPromise){var g=navigator.mediaCapabilities;"function"==typeof(null==g?void 0:g.decodingInfo)&&function(t,e,r,i,n,a){var s=t.audioCodec?t.audioGroups:null,o=null==a?void 0:a.audioCodec,l=null==a?void 0:a.channels,u=l?parseInt(l):o?1/0:2,h=null;if(null!=s&&s.length)try{h=1===s.length&&s[0]?e.groups[s[0]].channels:s.reduce((function(t,r){if(r){var i=e.groups[r];if(!i)throw new Error("Audio track group "+r+" not found");Object.keys(i.channels).forEach((function(e){t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==r||t.bitrate>Math.max(n,8e6))||!!h&&y(u)&&Object.keys(h).some((function(t){return parseInt(t)>u}))}(o,I,R,k,t,b)?(o.supportedPromise=Fr(o,I,g),o.supportedPromise.then((function(t){if(l.hls){o.supportedResult=t;var e=l.hls.levels,r=e.indexOf(o);t.error?w.warn('[abr] MediaCapabilities decodingInfo error: "'+t.error+'" for level '+r+" "+JSON.stringify(t)):t.supported||(w.warn("[abr] Unsupported MediaCapabilities decodingInfo result for level "+r+" "+JSON.stringify(t)),r>-1&&e.length>1&&(w.log("[abr] Removing unsupported level "+r),l.hls.removeLevel(r)))}}))):o.supportedResult=xr}if(T&&o.codecSet!==T||R&&o.videoRange!==R||c&&k>o.frameRate||!c&&k>0&&k=2*U&&0===n?v[H].averageBitrate:v[H].maxBitrate,x=l.getTimeToLoadFrag(B,m,_*C,void 0===D);if(m>=_&&(H===h||0===o.loadError&&0===o.fragmentError)&&(x<=B||!y(x)||L&&!l.bitrateTestDelay||x"+H+" adjustedbw("+Math.round(m)+")-bitrate="+Math.round(m-_)+" ttfb:"+B.toFixed(1)+" avgDuration:"+C.toFixed(1)+" maxFetchDuration:"+u.toFixed(1)+" fetchDuration:"+x.toFixed(1)+" firstSelection:"+A+" codecSet:"+T+" videoRange:"+R+" hls.loadLevel:"+p)),A&&(l.firstSelection=H),{v:H}}},H=r;H>=e;H--)if(0!==(N=K())&&N)return N.v;return-1},s(t,[{key:"firstAutoLevel",get:function(){var t=this.hls,e=t.maxAutoLevel,r=t.minAutoLevel,i=this.getBwEstimate(),n=this.hls.config.maxStarvationDelay,a=this.findBestLevel(i,r,e,0,n,1,1);if(a>-1)return a;var s=this.hls.firstLevel,o=Math.min(Math.max(s,r),e);return w.warn("[abr] Could not find best starting auto level. Defaulting to first in playlist "+s+" clamped to "+o),o}},{key:"forcedAutoLevel",get:function(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}},{key:"nextAutoLevel",get:function(){var t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),r=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&r&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;var i=e&&r?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){var n=this.hls.levels;if(n.length>Math.max(t,i)&&n[t].loadError<=n[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i},set:function(t){var e=this.hls,r=e.maxAutoLevel,i=e.minAutoLevel,n=Math.min(Math.max(t,i),r);this._nextAutoLevel!==n&&(this.nextAutoLevelKey="",this._nextAutoLevel=n)}}]),t}(),Hr=function(){function t(){this._boundTick=void 0,this._tickTimer=null,this._tickInterval=null,this._tickCallCount=0,this._boundTick=this.tick.bind(this)}var e=t.prototype;return e.destroy=function(){this.onHandlerDestroying(),this.onHandlerDestroyed()},e.onHandlerDestroying=function(){this.clearNextTick(),this.clearInterval()},e.onHandlerDestroyed=function(){},e.hasInterval=function(){return!!this._tickInterval},e.hasNextTick=function(){return!!this._tickTimer},e.setInterval=function(t){return!this._tickInterval&&(this._tickCallCount=0,this._tickInterval=self.setInterval(this._boundTick,t),!0)},e.clearInterval=function(){return!!this._tickInterval&&(self.clearInterval(this._tickInterval),this._tickInterval=null,!0)},e.clearNextTick=function(){return!!this._tickTimer&&(self.clearTimeout(this._tickTimer),this._tickTimer=null,!0)},e.tick=function(){this._tickCallCount++,1===this._tickCallCount&&(this.doTick(),this._tickCallCount>1&&this.tickImmediate(),this._tickCallCount=0)},e.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},e.doTick=function(){},t}(),Vr="NOT_LOADED",Yr="APPENDING",Wr="PARTIAL",jr="OK",qr=function(){function t(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(S.BUFFER_APPENDED,this.onBufferAppended,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(S.BUFFER_APPENDED,this.onBufferAppended,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){var r=this.activePartLists[e];if(r)for(var i=r.length;i--;){var n=r[i];if(!n)break;var a=n.end;if(n.start<=t&&null!==a&&t<=a)return n}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),n=i.length;n--;){var a=r[i[n]];if((null==a?void 0:a.body.type)===e&&a.buffered){var s=a.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r,i){var n=this;this.timeRanges&&(this.timeRanges[t]=e);var a=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((function(i){var s=n.fragments[i];if(s&&!(a>=s.body.sn))if(s.buffered||s.loaded){var o=s.range[t];o&&o.time.some((function(t){var r=!n.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&n.removeFragment(s.body),r}))}else s.body.type===r&&n.removeFragment(s.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,n=t.part;if(r&&"initSegment"!==i.sn){var a=zr(i),s=this.fragments[a];if(!(!s||s.buffered&&i.gap)){var o=!i.relurl;Object.keys(r).forEach((function(t){var a=i.elementaryStreams[t];if(a){var l=r[t],u=o||!0===a.partial;s.range[t]=e.getBufferedTimes(i,n,u,l)}})),s.loaded=null,Object.keys(s.range).length?(s.buffered=!0,(s.body.endList=i.endList||s.body.endList)&&(this.endListFragments[s.body.type]=s),Xr(s)||this.removeParts(i.sn-1,i.type)):this.removeFragment(s.body)}}},e.removeParts=function(t,e){var r=this.activePartLists[e];r&&(this.activePartLists[e]=r.filter((function(e){return e.fragment.sn>=t})))},e.fragBuffered=function(t,e){var r=zr(t),i=this.fragments[r];!i&&e&&(i=this.fragments[r]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var n={time:[],partial:r},a=t.start,s=t.end,o=t.minEndPTS||s,l=t.maxStartPTS||a,u=0;u=h&&o<=d){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ah){var c=Math.max(a,i.start(u)),f=Math.min(s,i.end(u));f>c&&(n.partial=!0,n.time.push({startPTS:c,endPTS:f}))}else if(s<=h)break}return n},e.getPartialFragment=function(t){var e,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&Xr(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),a<=e&&(n=u.body,a=e)))})),n},e.isEndListAppended=function(t){var e=this.endListFragments[t];return void 0!==e&&(e.buffered||Xr(e))},e.getState=function(t){var e=zr(t),r=this.fragments[e];return r?r.buffered?Xr(r)?Wr:jr:Yr:Vr},e.isTimeBuffered=function(t,e,r){for(var i,n,a=0;a=i&&e<=n)return!0;if(e<=i)return!1}return!1},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if("initSegment"!==r.sn&&!r.bitrateTest){var n=i?null:e,a=zr(r);this.fragments[a]={body:r,appendedPTS:null,loaded:n,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,n=e.part,a=e.timeRanges;if("initSegment"!==i.sn){var s=i.type;if(n){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(n)}this.timeRanges=a,Object.keys(a).forEach((function(t){var e=a[t];r.detectEvictedFragments(t,e,s,n)}))}},e.onFragBuffered=function(t,e){this.detectPartialFragments(e)},e.hasFragment=function(t){var e=zr(t);return!!this.fragments[e]},e.hasParts=function(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)},e.removeFragmentsInRange=function(t,e,r,i,n){var a=this;i&&!this.hasGaps||Object.keys(this.fragments).forEach((function(s){var o=a.fragments[s];if(o){var l=o.body;l.type!==r||i&&!l.gap||l.startt&&(o.buffered||n)&&a.removeFragment(l)}}))},e.removeFragment=function(t){var e=zr(t);t.stats.loaded=0,t.clearElementaryStreamInfo();var r=this.activePartLists[t.type];if(r){var i=t.sn;this.activePartLists[t.type]=r.filter((function(t){return t.fragment.sn!==i}))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1},t}();function Xr(t){var e,r,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(r=t.range.audio)?void 0:r.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function zr(t){return t.type+"_"+t.level+"_"+t.sn}var Qr={length:0,start:function(){return 0},end:function(){return 0}},Jr=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),n=0;n=i.start(n)&&r<=i.end(n))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var n,a=t.getBuffered(e),s=[];for(n=0;ns&&(i[a-1].end=t[n].end):i.push(t[n])}else i.push(t[n])}else i=t;for(var o,l=0,u=e,h=e,d=0;d=c&&er.startCC||t&&t.cc>>8^255&m^99,t[f]=m,e[m]=f;var p=c[f],y=c[p],E=c[y],T=257*c[m]^16843008*m;i[f]=T<<24|T>>>8,n[f]=T<<16|T>>>16,a[f]=T<<8|T>>>24,s[f]=T,T=16843009*E^65537*y^257*p^16843008*f,l[m]=T<<24|T>>>8,u[m]=T<<16|T>>>16,h[m]=T<<8|T>>>24,d[m]=T,f?(f=p^c[c[c[E^p]]],g^=c[c[g]]):f=g=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;is.end){var h=a>u;(a0&&null!=a&&a.key&&a.iv&&"AES-128"===a.method){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer).catch((function(e){throw i.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((function(n){var a=self.performance.now();return i.trigger(S.FRAG_DECRYPTED,{frag:t,payload:n,stats:{tstart:s,tdecrypt:a}}),e.payload=n,r.completeInitSegmentLoad(e)}))}return r.completeInitSegmentLoad(e)})).catch((function(e){r.state!==gi&&r.state!==Ai&&(r.warn(e),r.resetFragmentLoading(t))}))},r.completeInitSegmentLoad=function(t){if(!this.levels)throw new Error("init load aborted, missing levels");var e=t.frag.stats;this.state=vi,t.frag.data=new Uint8Array(t.payload),e.parsing.start=e.buffering.start=self.performance.now(),e.parsing.end=e.buffering.end=self.performance.now(),this.tick()},r.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.sn!==e.sn||t.level!==e.level},r.fragBufferedComplete=function(t,e){var r,i,n,a,s=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+(this.playlistType===we?"level":"track")+" "+t.level+" (frag:["+(null!=(r=t.startPTS)?r:NaN).toFixed(3)+"-"+(null!=(i=t.endPTS)?i:NaN).toFixed(3)+"] > buffer:"+(s?fi(Jr.getBuffered(s)):"(detached)")+")"),"initSegment"!==t.sn){var o;if(t.type!==_e){var l=t.elementaryStreams;if(!Object.keys(l).some((function(t){return!!l[t]})))return void(this.state=vi)}var u=null==(o=this.levels)?void 0:o[t.level];null!=u&&u.fragmentError&&(this.log("Resetting level fragment error count of "+u.fragmentError+" on frag buffered"),u.fragmentError=0)}this.state=vi,s&&(!this.loadedmetadata&&t.type==we&&s.buffered.length&&(null==(n=this.fragCurrent)?void 0:n.sn)===(null==(a=this.fragPrevious)?void 0:a.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())},r.seekToStartPos=function(){},r._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,n=t.partsLoaded,a=!n||0===n.length||n.some((function(t){return!t})),s=new $r(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);e.flush(s)}},r._handleFragmentLoadProgress=function(t){},r._doFragLoad=function(t,e,r,i){var n,a=this;void 0===r&&(r=null);var s=null==e?void 0:e.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!t.encrypted||null!=(n=t.decryptdata)&&n.key?!t.encrypted&&s.encryptedFragments.length&&this.keyLoader.loadClear(t,s.encryptedFragments):(this.log("Loading key for "+t.sn+" of ["+s.startSN+"-"+s.endSN+"], "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level),this.state=mi,this.fragCurrent=t,o=this.keyLoader.load(t).then((function(t){if(!a.fragContextChanged(t.frag))return a.hls.trigger(S.KEY_LOADED,t),a.state===mi&&(a.state=vi),t})),this.hls.trigger(S.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(o=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),r=Math.max(t.start,r||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){var l=s.partList;if(l&&i){r>t.end&&s.fragmentHint&&(t=s.fragmentHint);var u=this.getNextPart(l,t,r);if(u>-1){var h,d=l[u];return this.log("Loading part sn: "+t.sn+" p: "+d.index+" cc: "+t.cc+" of playlist ["+s.startSN+"-"+s.endSN+"] parts [0-"+u+"-"+(l.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=d.start+d.duration,this.state=pi,h=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(t,d,e,i)})).catch((function(t){return a.handleFragLoadError(t)})):this.doFragPartsLoad(t,d,e,i).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,part:d,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(l,r))return Promise.resolve(null)}}this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(s?"of ["+s.startSN+"-"+s.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),y(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=pi;var c,f=this.config.progressive;return c=f&&o?o.then((function(e){return!e||a.fragContextChanged(null==e?void 0:e.frag)?null:a.fragmentLoader.load(t,i)})).catch((function(t){return a.handleFragLoadError(t)})):Promise.all([this.fragmentLoader.load(t,f?i:void 0),o]).then((function(t){var e=t[0];return!f&&e&&i&&i(e),e})).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):c},r.doFragPartsLoad=function(t,e,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList;!function e(o){n.fragmentLoader.loadPart(t,o,i).then((function(i){l[o.index]=i;var s=i.part;n.hls.trigger(S.FRAG_LOADED,i);var h=ur(r,t.sn,o.index+1)||hr(u,t.sn,o.index+1);if(!h)return a({frag:t,part:s,partsLoaded:l});e(h)})).catch(s)}(e)}))},r.handleFragLoadError=function(t){if("data"in t){var e=t.data;t.data&&e.details===A.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(S.ERROR,e)}else this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null},r._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===Ti){var r=e.frag,i=e.part,n=e.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a),this.updateLevelTiming(r,i,n,t.partial)}else this.fragCurrent||this.state===gi||this.state===Ai||(this.state=vi)},r.getCurrentContext=function(t){var e=this.levels,r=this.fragCurrent,i=t.level,n=t.sn,a=t.part;if(null==e||!e[i])return this.warn("Levels object was unset while buffering fragment "+n+" of level "+i+". The current chunk will not be buffered."),null;var s=e[i],o=a>-1?ur(s,n,a):null,l=o?o.fragment:function(t,e,r){if(null==t||!t.details)return null;var i=t.details,n=i.fragments[e-i.startSN];return n||((n=i.fragmentHint)&&n.sn===e?n:ea&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},r.getFwdBufferInfo=function(t,e){var r=this.getLoadPosition();return y(r)?this.getFwdBufferInfoAtPos(t,r,e):null},r.getFwdBufferInfoAtPos=function(t,e,r){var i=this.config.maxBufferHole,n=Jr.bufferInfo(t,e,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(e,r);if(a&&n.nextStart=r&&(e.maxMaxBufferLength/=2,this.warn("Reduce max buffer length to "+e.maxMaxBufferLength+"s"),!0)},r.getAppendedFrag=function(t,e){var r=this.fragmentTracker.getAppendedFrag(t,we);return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(t,e){var r=e.fragments,i=r.length;if(!i)return null;var n,a=this.config,s=r[0].start;if(e.live){var o=a.initialLiveManifestSize;if(ie},r.getNextFragmentLoopLoading=function(t,e,r,i,n){var a=t.gap,s=this.getNextFragment(this.nextLoadPosition,e);if(null===s)return s;if(t=s,a&&t&&!t.gap&&r.nextStart){var o=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i);if(null!==o&&r.len+o.len>=n)return this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+t.sn),null}return t},r.mapToInitFragWhenRequired=function(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment},r.getNextPart=function(t,e,r){for(var i=-1,n=!1,a=!0,s=0,o=t.length;s-1&&rr.start&&r.loaded},r.getInitialLiveFragment=function(t,e){var r=this.fragPrevious,i=null;if(r){if(t.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(t,e,r){if(null===e||!Array.isArray(t)||!t.length||!y(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;r=r||0;for(var i=0;i=t.startSN&&n<=t.endSN){var a=e[n-t.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return pr(t,(function(t){return t.cce?-1:0}))}(e,r.cc),i&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn))}}else{var s=this.hls.liveSyncPosition;null!==s&&(i=this.getFragmentAtPosition(s,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i},r.getFragmentAtPosition=function(t,e,r){var i,n=this.config,a=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=n.maxFragLookUpTolerance,h=r.partList,d=!!(n.lowLatencyMode&&null!=h&&h.length&&l);if(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),i=te-u?0:u):s[s.length-1]){var c=i.sn-r.startSN,f=this.fragmentTracker.getState(i);if((f===jr||f===Wr&&i.gap)&&(a=i),a&&i.sn===a.sn&&(!d||h[0].fragment.sn>i.sn)&&a&&i.level===a.level){var g=s[c+1];i=i.sn=a-e.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n"+t.startSN+" prev-sn: "+(o?o.sn:"na")+" fragments: "+i),l}return n},r.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},r.setStartPosition=function(t,e){var r=this.startPosition;if(r "+(null==(n=this.fragCurrent)?void 0:n.url))}else{var a=e.details===A.FRAG_GAP;a&&this.fragmentTracker.fragBuffered(i,!0);var s=e.errorAction,o=s||{},l=o.action,u=o.retryCount,h=void 0===u?0:u,d=o.retryConfig;if(s&&l===Rr&&d){this.resetStartWhenNotLoaded(this.levelLastLoaded);var c=gr(d,h);this.warn("Fragment "+i.sn+" of "+t+" "+i.level+" errored with "+e.details+", retrying loading "+(h+1)+"/"+d.maxNumRetry+" in "+c+"ms"),s.resolved=!0,this.retryDate=self.performance.now()+c,this.state=yi}else if(d&&s){if(this.resetFragmentErrors(t),!(h.5;i&&this.reduceMaxBufferLength(r.len);var n=!i;return n&&this.warn("Buffer full error while media.currentTime is not buffered, flush "+e+" buffer"),t.frag&&(this.fragmentTracker.removeFragment(t.frag),this.nextLoadPosition=t.frag.start),this.resetLoadingState(),n}return!1},r.resetFragmentErrors=function(t){t===Ce&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==gi&&(this.state=vi)},r.afterBufferFlushed=function(t,e,r){if(t){var i=Jr.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,r),this.state===Li&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=vi},r.resetStartWhenNotLoaded=function(t){if(!this.loadedmetadata){this.startFragRequested=!1;var e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(t){this.warn("The loading context changed while buffering fragment "+t.sn+" of level "+t.level+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()},r.removeUnbufferedFrags=function(t){void 0===t&&(t=0),this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(t,e,r,i){var n,a=this,s=r.details;if(s){if(!Object.keys(t.elementaryStreams).reduce((function(e,n){var o=t.elementaryStreams[n];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return a.warn("Could not parse fragment "+t.sn+" "+n+" duration reliably ("+l+")"),e||!1;var u=i?0:ar(s,t,o.startPTS,o.endPTS,o.startDTS,o.endDTS);return a.hls.trigger(S.LEVEL_PTS_UPDATED,{details:s,level:r,drift:u,type:n,frag:t,start:o.startPTS,end:o.endPTS}),!0}return e}),!1)&&null===(null==(n=this.transmuxer)?void 0:n.error)){var o=new Error("Found no media in fragment "+t.sn+" of level "+t.level+" resetting transmuxer to fallback to playlist timing");if(0===r.fragmentError&&(r.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(o.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:o,frag:t,reason:"Found no media in msn "+t.sn+' of level "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}this.state=Si,this.hls.trigger(S.FRAG_PARSED,{frag:t,part:e})}else this.warn("level.details undefined")},r.resetTransmuxer=function(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)},r.recoverWorkerError=function(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())},s(e,[{key:"state",get:function(){return this._state},set:function(t){var e=this._state;e!==t&&(this._state=t,this.log(e+"->"+t))}}]),e}(Hr),Di=function(){function t(){this.chunks=[],this.dataLength=0}var e=t.prototype;return e.push=function(t){this.chunks.push(t),this.dataLength+=t.length},e.flush=function(){var t,e=this.chunks,r=this.dataLength;return e.length?(t=1===e.length?e[0]:function(t,e){for(var r=new Uint8Array(e),i=0,n=0;n0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:Ge,duration:Number.POSITIVE_INFINITY});n>>5}function Fi(t,e){return e+1=t.length)return!1;var i=Pi(t,e);if(i<=r)return!1;var n=e+i;return n===t.length||Fi(t,n)}return!1}function Oi(t,e,r,i,n){if(!t.samplerate){var a=function(t,e,r,i){var n,a,s,o,l=navigator.userAgent.toLowerCase(),u=i,h=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];n=1+((192&e[r+2])>>>6);var d=(60&e[r+2])>>>2;if(!(d>h.length-1))return s=(1&e[r+2])<<2,s|=(192&e[r+3])>>>6,w.log("manifest codec:"+i+", ADTS type:"+n+", samplingIndex:"+d),/firefox/i.test(l)?d>=6?(n=5,o=new Array(4),a=d-3):(n=2,o=new Array(2),a=d):-1!==l.indexOf("android")?(n=2,o=new Array(2),a=d):(n=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&d>=6?a=d-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(d>=6&&1===s||/vivaldi/i.test(l))||!i&&1===s)&&(n=2,o=new Array(2)),a=d)),o[0]=n<<3,o[0]|=(14&d)>>1,o[1]|=(1&d)<<7,o[1]|=s<<3,5===n&&(o[1]|=(14&a)>>1,o[2]=(1&a)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:h[d],channelCount:s,codec:"mp4a.40."+n,manifestCodec:u};var c=new Error("invalid ADTS sampling index:"+d);t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!0,error:c,reason:c.message})}(e,r,i,n);if(!a)return;t.config=a.config,t.samplerate=a.samplerate,t.channelCount=a.channelCount,t.codec=a.codec,t.manifestCodec=a.manifestCodec,w.log("parsed codec:"+t.codec+", rate:"+a.samplerate+", channels:"+a.channelCount)}}function Ni(t){return 9216e4/t}function Ui(t,e,r,i,n){var a,s=i+n*Ni(t.samplerate),o=function(t,e){var r=xi(t,e);if(e+r<=t.length){var i=Pi(t,e)-r;if(i>0)return{headerLength:r,frameLength:i}}}(e,r);if(o){var l=o.frameLength,u=o.headerLength,h=u+l,d=Math.max(0,r+h-e.length);d?(a=new Uint8Array(h-u)).set(e.subarray(r+u,e.length),0):a=e.subarray(r+u,r+h);var c={unit:a,pts:s};return d||t.samples.push(c),{sample:c,length:h,missing:d}}var f=e.length-r;return(a=new Uint8Array(f)).set(e.subarray(r,e.length),0),{sample:{unit:a,pts:s},length:f,missing:-1}}var Bi=null,Gi=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],Ki=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],Hi=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Vi=[0,1,1,4];function Yi(t,e,r,i,n){if(!(r+24>e.length)){var a=Wi(e,r);if(a&&r+a.frameLength<=e.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:e.subarray(r,r+a.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=a.channelCount,t.samplerate=a.sampleRate,t.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function Wi(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,n=t[e+2]>>4&15,a=t[e+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Gi[14*(3===r?3-i:3===i?3:4)+n-1],u=Ki[3*(3===r?0:2===r?1:2)+a],h=3===o?1:2,d=Hi[r][i],c=Vi[i],f=8*d*c,g=Math.floor(d*l/u+s)*c;if(null===Bi){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Bi=v?parseInt(v[1]):0}return!!Bi&&Bi<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:h,frameLength:g,samplesPerFrame:f}}}function ji(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function qi(t,e){return e+18&&109===t[r+4]&&111===t[r+5]&&111===t[r+6]&&102===t[r+7])return!0;r=i>1?r+i:e}return!1}(t)},e.demux=function(t,e){this.timeOffset=e;var r=t,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=Kt(this.remainderData,t));var a=function(t){var e={valid:null,remainder:null},r=xt(t,["moof"]);if(r.length<2)return e.remainder=t,e;var i=r[r.length-1];return e.valid=nt(t,0,i.byteOffset-8),e.remainder=nt(t,i.byteOffset-8),e}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,e);return n.samples=Ht(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},e.flush=function(){var t=this.timeOffset,e=this.videoTrack,r=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(e,this.timeOffset);return r.samples=Ht(t,e),{videoTrack:e,audioTrack:Ii(),id3Track:i,textTrack:Ii()}},e.extractID3Track=function(t,e){var r=this.id3Track;if(t.samples.length){var i=xt(t.samples,["emsg"]);i&&i.forEach((function(t){var i=function(t){var e=t[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===e){for(;"\0"!==bt(t.subarray(u,u+1));)r+=bt(t.subarray(u,u+1)),u+=1;for(r+=bt(t.subarray(u,u+1)),u+=1;"\0"!==bt(t.subarray(u,u+1));)i+=bt(t.subarray(u,u+1)),u+=1;i+=bt(t.subarray(u,u+1)),u+=1,n=It(t,12),a=It(t,16),o=It(t,20),l=It(t,24),u=28}else if(1===e){n=It(t,u+=4);var h=It(t,u+=4),d=It(t,u+=4);for(u+=4,s=Math.pow(2,32)*h+d,E(s)||(s=Number.MAX_SAFE_INTEGER,w.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=It(t,u),l=It(t,u+=4),u+=4;"\0"!==bt(t.subarray(u,u+1));)r+=bt(t.subarray(u,u+1)),u+=1;for(r+=bt(t.subarray(u,u+1)),u+=1;"\0"!==bt(t.subarray(u,u+1));)i+=bt(t.subarray(u,u+1)),u+=1;i+=bt(t.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:t.subarray(u,t.byteLength)}}(t);if(Qi.test(i.schemeIdUri)){var n=y(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale,a=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;a<=.001&&(a=Number.POSITIVE_INFINITY);var s=i.payload;r.samples.push({data:s,len:s.byteLength,dts:n,pts:n,type:He,duration:a})}}))}return r},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}(),$i=function(t,e){var r=0,i=5;e+=i;for(var n=new Uint32Array(1),a=new Uint32Array(1),s=new Uint8Array(1);i>0;){s[0]=t[e];var o=Math.min(i,8),l=8-o;a[0]=4278190080>>>24+l<>l,r=r?r<e.length)return-1;if(11!==e[r]||119!==e[r+1])return-1;var a=e[r+4]>>6;if(a>=3)return-1;var s=[48e3,44100,32e3][a],o=63&e[r+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+a];if(r+l>e.length)return-1;var u=e[r+6]>>5,h=0;2===u?h+=2:(1&u&&1!==u&&(h+=2),4&u&&(h+=2));var d=(e[r+6]<<8|e[r+7])>>12-h&1,c=[2,1,2,3,3,4,4,5][u]+d,f=e[r+5]>>3,g=7&e[r+5],v=new Uint8Array([a<<6|f<<1|g>>2,(3&g)<<6|u<<3|d<<2|o>>4,o<<4&224]),m=i+n*(1536/s*9e4),p=e.subarray(r,r+l);return t.config=v,t.channelCount=c,t.samplerate=s,t.samples.push({unit:p,pts:m}),l}var en=function(){function t(){this.VideoSample=null}var e=t.prototype;return e.createVideoSample=function(t,e,r,i){return{key:t,frame:!1,pts:e,dts:r,units:[],debug:i,length:0}},e.getLastNalUnit=function(t){var e,r,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){var n=i.units;r=n[n.length-1]}return r},e.pushAccessUnit=function(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){var r=e.samples,i=r.length;if(!i)return void e.dropped++;var n=r[i-1];t.pts=n.pts,t.dts=n.dts}e.samples.push(t)}t.debug.length&&w.log(t.pts+"/"+t.dts+":"+t.debug)},t}(),rn=function(){function t(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}var e=t.prototype;return e.loadWord=function(){var t=this.data,e=this.bytesAvailable,r=t.byteLength-e,i=new Uint8Array(4),n=Math.min(4,e);if(0===n)throw new Error("no bytes available");i.set(t.subarray(r,r+n)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*n,this.bytesAvailable-=n},e.skipBits=function(t){var e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(t-=this.bitsAvailable,t-=(e=t>>3)<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)},e.readBits=function(t){var e=Math.min(this.bitsAvailable,t),r=this.word>>>32-e;if(t>32&&w.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(e=t-e)>0&&this.bitsAvailable?r<>>t))return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()},e.skipUEG=function(){this.skipBits(1+this.skipLZ())},e.skipEG=function(){this.skipBits(1+this.skipLZ())},e.readUEG=function(){var t=this.skipLZ();return this.readBits(t+1)-1},e.readEG=function(){var t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)},e.readBoolean=function(){return 1===this.readBits(1)},e.readUByte=function(){return this.readBits(8)},e.readUShort=function(){return this.readBits(16)},e.readUInt=function(){return this.readBits(32)},e.skipScalingList=function(t){for(var e=8,r=8,i=0;i4){var f=new rn(c).readSliceType();2!==f&&4!==f&&7!==f&&9!==f||(h=!0)}h&&null!=(d=l)&&d.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.frame=!0,l.key=h;break;case 5:a=!0,null!=(o=l)&&o.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.key=!0,l.frame=!0;break;case 6:a=!0,Yt(i.data,1,r.pts,e.samples);break;case 7:var g,v;a=!0,u=!0;var m=i.data,p=new rn(m).readSPS();if(!t.sps||t.width!==p.width||t.height!==p.height||(null==(g=t.pixelRatio)?void 0:g[0])!==p.pixelRatio[0]||(null==(v=t.pixelRatio)?void 0:v[1])!==p.pixelRatio[1]){t.width=p.width,t.height=p.height,t.pixelRatio=p.pixelRatio,t.sps=[m],t.duration=n;for(var y=m.subarray(1,4),E="avc1.",T=0;T<3;T++){var S=y[T].toString(16);S.length<2&&(S="0"+S),E+=S}t.codec=E}break;case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,l&&s.pushAccessUnit(l,t),l=s.VideoSample=s.createVideoSample(!1,r.pts,r.dts,"");break;case 12:a=!0;break;default:a=!1,l&&(l.debug+="unknown NAL "+i.type+" ")}l&&a&&l.units.push(i)})),i&&l&&(this.pushAccessUnit(l,t),this.VideoSample=null)},r.parseAVCNALu=function(t,e){var r,i,n=e.byteLength,a=t.naluState||0,s=a,o=[],l=0,u=-1,h=0;for(-1===a&&(u=0,h=31&e[0],a=0,l=1);l=0){var d={data:e.subarray(u,i),type:h};o.push(d)}else{var c=this.getLastNalUnit(t.samples);c&&(s&&l<=4-s&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-s)),i>0&&(c.data=Kt(c.data,e.subarray(0,i)),c.state=0))}l=0&&a>=0){var f={data:e.subarray(u,n),type:h,state:a};o.push(f)}if(0===o.length){var g=this.getLastNalUnit(t.samples);g&&(g.data=Kt(g.data,e))}return t.naluState=a,o},e}(en),an=function(){function t(t,e,r){this.keyData=void 0,this.decrypter=void 0,this.keyData=r,this.decrypter=new ci(e,{removePKCS7Padding:!1})}var e=t.prototype;return e.decryptBuffer=function(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)},e.decryptAacSample=function(t,e,r){var i=this,n=t[e].unit;if(!(n.length<=16)){var a=n.subarray(16,n.length-n.length%16),s=a.buffer.slice(a.byteOffset,a.byteOffset+a.length);this.decryptBuffer(s).then((function(a){var s=new Uint8Array(a);n.set(s,16),i.decrypter.isSync()||i.decryptAacSamples(t,e+1,r)}))}},e.decryptAacSamples=function(t,e,r){for(;;e++){if(e>=t.length)return void r();if(!(t[e].unit.length<32||(this.decryptAacSample(t,e,r),this.decrypter.isSync())))return}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,n=32;n=t.length)return void i();for(var n=t[e].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(t,e,r,i,a),this.decrypter.isSync())))return}}},t}(),sn=188,on=function(){function t(t,e,r){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.videoParser=new nn}t.probe=function(e){var r=t.syncOffset(e);return r>0&&w.warn("MPEG2-TS detected but first sync word found @ offset "+r),-1!==r},t.syncOffset=function(t){for(var e=t.length,r=Math.min(940,e-sn)+1,i=0;i1&&(0===a&&s>2||o+sn>r))return a}i++}return-1},t.createTrack=function(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:kt[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}};var e=t.prototype;return e.resetInitSegment=function(e,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=t.createTrack("video"),this._audioTrack=t.createTrack("audio",n),this._id3Track=t.createTrack("id3"),this._txtTrack=t.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i,this._duration=n},e.resetTimeStamp=function(){},e.resetContiguity=function(){var t=this._audioTrack,e=this._videoTrack,r=this._id3Track;t&&(t.pesData=null),e&&(e.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.remainderData=null},e.demux=function(e,r,i,n){var a;void 0===i&&(i=!1),void 0===n&&(n=!1),i||(this.sampleAes=null);var s=this._videoTrack,o=this._audioTrack,l=this._id3Track,u=this._txtTrack,h=s.pid,d=s.pesData,c=o.pid,f=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,E=e.length;if(this.remainderData&&(E=(e=Kt(this.remainderData,e)).length,this.remainderData=null),E>4>1){if((I=k+5+e[k+4])===k+sn)continue}else I=k+4;switch(D){case h:b&&(d&&(a=cn(d))&&this.videoParser.parseAVCPES(s,u,a,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(e.subarray(I,k+sn)),d.size+=k+sn-I);break;case c:if(b){if(g&&(a=cn(g)))switch(o.segmentCodec){case"aac":this.parseAACPES(o,a);break;case"mp3":this.parseMPEGPES(o,a);break;case"ac3":this.parseAC3PES(o,a)}g={data:[],size:0}}g&&(g.data.push(e.subarray(I,k+sn)),g.size+=k+sn-I);break;case f:b&&(v&&(a=cn(v))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(e.subarray(I,k+sn)),v.size+=k+sn-I);break;case 0:b&&(I+=e[I]+1),y=this._pmtId=un(e,I);break;case y:b&&(I+=e[I]+1);var C=hn(e,I,this.typeSupported,i);(h=C.videoPid)>0&&(s.pid=h,s.segmentCodec=C.segmentVideoCodec),(c=C.audioPid)>0&&(o.pid=c,o.segmentCodec=C.segmentAudioCodec),(f=C.id3Pid)>0&&(l.pid=f),null===m||p||(w.warn("MPEG-TS PMT found at "+k+" after unknown PID '"+m+"'. Backtracking to sync byte @"+T+" to parse all TS packets."),m=null,k=T-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=D}}else R++;if(R>0){var _=new Error("Found "+R+" TS packet/s that do not start with 0x47");this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:_,reason:_.message})}s.pesData=d,o.pesData=g,l.pesData=v;var x={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(x),x},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.videoTrack,n=t.id3Track,a=t.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(e=cn(s))?(this.videoParser.parseAVCPES(i,a,e,!0,this._duration),i.pesData=null):i.pesData=s,o&&(e=cn(o))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,e);break;case"mp3":this.parseMPEGPES(r,e);break;case"ac3":this.parseAC3PES(r,e)}r.pesData=null}else null!=o&&o.size&&w.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(e=cn(l))?(this.parseID3PES(n,e),n.pesData=null):n.pesData=l},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),n=this.sampleAes=new an(this.observer,this.config,e);return this.decrypt(i,n)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,n=t.videoTrack;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(function(){n.samples?e.decryptAvcSamples(n.samples,0,0,(function(){r(t)})):r(t)})):n.samples&&e.decryptAvcSamples(n.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._duration=0},e.parseAACPES=function(t,e){var r,i,n,a=0,s=this.aacOverFlow,o=e.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l)o=Kt(s.sample.unit,o);else{var h=u-l;s.sample.unit.set(o.subarray(0,l),h),t.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r0;)o+=n;else w.warn("[tsdemuxer]: AC3 PES unknown PTS")},e.parseID3PES=function(t,e){if(void 0!==e.pts){var r=o({},e,{type:this._videoTrack?He:Ge,duration:Number.POSITIVE_INFINITY});t.samples.push(r)}else w.warn("[tsdemuxer]: ID3 PES unknown PTS")},t}();function ln(t,e){return((31&t[e+1])<<8)+t[e+2]}function un(t,e){return(31&t[e+10])<<8|t[e+11]}function hn(t,e,r,i){var n={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},a=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0)for(var l=e+5,u=o;u>2;){106===t[l]&&(!0!==r.ac3?w.log("AC-3 audio found, not supported in this browser for now"):(n.audioPid=s,n.segmentAudioCodec="ac3"));var h=t[l+1]+2;l+=h,u-=h}break;case 194:case 135:w.warn("Unsupported EC-3 in M2TS found");break;case 36:w.warn("Unsupported HEVC in M2TS found")}e+=o+5}return n}function dn(t){w.log(t+" with AES-128-CBC encryption found in unencrypted stream")}function cn(t){var e,r,i,n,a,s=0,o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Kt(o[0],o[1]),o.splice(1,1);if(1===((e=o[0])[0]<<16)+(e[1]<<8)+e[2]){if((r=(e[4]<<8)+e[5])&&r>t.size-6)return null;var l=e[7];192&l&&(n=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?n-(a=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(w.warn(Math.round((n-a)/9e4)+"s delta between PTS and DTS, align them"),n=a):a=n);var u=(i=e[8])+9;if(t.size<=u)return null;t.size-=u;for(var h=new Uint8Array(t.size),d=0,c=o.length;df){u-=f;continue}e=e.subarray(u),f-=u,u=0}h.set(e,s),s+=f}return r&&(r-=i+3),{data:h,pts:n,dts:a,len:r}}return null}var fn=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,r,i,n),this._audioTrack={container:"audio/mpeg",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"mp3",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},e.probe=function(t){if(!t)return!1;var e=lt(t,0),r=(null==e?void 0:e.length)||0;if(e&&11===t[r]&&119===t[r+1]&&void 0!==dt(e)&&$i(t,r)<=16)return!1;for(var i=t.length;r1?r-1:0),n=1;n>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),a=0,e=8;a>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},t.mdia=function(e){return t.box(t.types.mdia,t.mdhd(e.timescale,e.duration),t.hdlr(e.type),t.minf(e))},t.mfhd=function(e){return t.box(t.types.mfhd,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e]))},t.minf=function(e){return"audio"===e.type?t.box(t.types.minf,t.box(t.types.smhd,t.SMHD),t.DINF,t.stbl(e)):t.box(t.types.minf,t.box(t.types.vmhd,t.VMHD),t.DINF,t.stbl(e))},t.moof=function(e,r,i){return t.box(t.types.moof,t.mfhd(e),t.traf(i,r))},t.moov=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trak(e[r]);return t.box.apply(null,[t.types.moov,t.mvhd(e[0].timescale,e[0].duration)].concat(i).concat(t.mvex(e)))},t.mvex=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trex(e[r]);return t.box.apply(null,[t.types.mvex].concat(i))},t.mvhd=function(e,r){r*=e;var i=Math.floor(r/(vn+1)),n=Math.floor(r%(vn+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return t.box(t.types.mvhd,a)},t.sdtp=function(e){var r,i,n=e.samples||[],a=new Uint8Array(4+n.length);for(r=0;r>>8&255),a.push(255&n),a=a.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&n),s=s.concat(Array.prototype.slice.call(i));var o=t.box(t.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|e.sps.length].concat(a).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,h=e.pixelRatio[0],d=e.pixelRatio[1];return t.box(t.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,l>>8&255,255&l,u>>8&255,255&u,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,t.box(t.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),t.box(t.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))},t.esds=function(t){var e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))},t.audioStsd=function(t){var e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])},t.mp4a=function(e){return t.box(t.types.mp4a,t.audioStsd(e),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){return t.box(t.types[".mp3"],t.audioStsd(e))},t.ac3=function(e){return t.box(t.types["ac-3"],t.audioStsd(e),t.box(t.types.dac3,e.config))},t.stsd=function(e){return"audio"===e.type?"mp3"===e.segmentCodec&&"mp3"===e.codec?t.box(t.types.stsd,t.STSD,t.mp3(e)):"ac3"===e.segmentCodec?t.box(t.types.stsd,t.STSD,t.ac3(e)):t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,n=e.width,a=e.height,s=Math.floor(i/(vn+1)),o=Math.floor(i%(vn+1));return t.box(t.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,r>>24&255,r>>16&255,r>>8&255,255&r,0,0,0,0,s>>24,s>>16&255,s>>8&255,255&s,o>>24,o>>16&255,o>>8&255,255&o,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,n>>8&255,255&n,0,0,a>>8&255,255&a,0,0]))},t.traf=function(e,r){var i=t.sdtp(e),n=e.id,a=Math.floor(r/(vn+1)),s=Math.floor(r%(vn+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),t.box(t.types.tfdt,new Uint8Array([1,0,0,0,a>>24,a>>16&255,a>>8&255,255&a,s>>24,s>>16&255,s>>8&255,255&s])),t.trun(e,i.length+16+20+8+16+8+8),i)},t.trak=function(e){return e.duration=e.duration||4294967295,t.box(t.types.trak,t.tkhd(e),t.mdia(e))},t.trex=function(e){var r=e.id;return t.box(t.types.trex,new Uint8Array([0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))},t.trun=function(e,r){var i,n,a,s,o,l,u=e.samples||[],h=u.length,d=12+16*h,c=new Uint8Array(d);for(r+=8+d,c.set(["video"===e.type?1:0,0,15,1,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,l>>>24&255,l>>>16&255,l>>>8&255,255&l],12+16*i);return t.box(t.types.trun,c)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e);return Kt(t.FTYP,r)},t}();mn.types=void 0,mn.HDLR_TYPES=void 0,mn.STTS=void 0,mn.STSC=void 0,mn.STCO=void 0,mn.STSZ=void 0,mn.VMHD=void 0,mn.SMHD=void 0,mn.STSD=void 0,mn.FTYP=void 0,mn.DINF=void 0;var pn=9e4;function yn(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=t*e*r;return i?Math.round(n):n}function En(t,e){return void 0===e&&(e=!1),yn(t,1e3,1/pn,e)}var Tn=null,Sn=null,Ln=function(){function t(t,e,r,i){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.ISGenerated=!1,null===Tn){var n=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Tn=n?parseInt(n[1]):0}if(null===Sn){var a=navigator.userAgent.match(/Safari\/(\d+)/i);Sn=a?parseInt(a[1]):0}}var e=t.prototype;return e.destroy=function(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null},e.resetTimeStamp=function(t){w.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t},e.resetNextTimestamp=function(){w.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},e.resetInitSegment=function(){w.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0},e.getVideoStartPts=function(t){var e=!1,r=t.reduce((function(t,r){var i=r.pts-t;return i<-4294967296?(e=!0,An(t,r.pts)):i>0?t:r.pts}),t[0].pts);return e&&w.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,n,a,s,o){var l,u,h,d,c,f,g=n,v=n,m=t.pid>-1,p=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=s&&y>0||y>1;if((!m||E)&&(!p||T)||this.ISGenerated||s){if(this.ISGenerated){var S,L,A,R,k=this.videoTrackConfig;!k||e.width===k.width&&e.height===k.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=k.pixelRatio)?void 0:L[0])&&(null==(A=e.pixelRatio)?void 0:A[1])===(null==(R=k.pixelRatio)?void 0:R[1])||this.resetInitSegment()}else h=this.generateIS(t,e,n,a);var b,D=this.isVideoContiguous,I=-1;if(T&&(I=function(t){for(var e=0;e0){w.warn("[mp4-remuxer]: Dropped "+I+" out of "+y+" video samples due to a missing keyframe");var C=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(I),e.dropped+=I,b=v+=(e.samples[0].pts-C)/e.inputTimeScale}else-1===I&&(w.warn("[mp4-remuxer]: No keyframe found out of "+y+" video samples"),f=!1);if(this.ISGenerated){if(E&&T){var _=this.getVideoStartPts(e.samples),x=(An(t.samples[0].pts,_)-_)/e.inputTimeScale;g+=Math.max(0,x),v+=Math.max(0,-x)}if(E){if(t.samplerate||(w.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),h=this.generateIS(t,e,n,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,a,p||T||o===Ce?v:void 0),T){var P=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(w.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),h=this.generateIS(t,e,n,a)),l=this.remuxVideo(e,v,D,P)}}else T&&(l=this.remuxVideo(e,v,D,0));l&&(l.firstKeyFrame=I,l.independent=-1!==I,l.firstKeyFramePTS=b)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(c=Rn(r,n,this._initPTS,this._initDTS)),i.samples.length&&(d=kn(i,n,this._initPTS))),{audio:u,video:l,initSegment:h,independent:f,text:d,id3:c}},e.generateIS=function(t,e,r,i){var n,a,s,o=t.samples,l=e.samples,u=this.typeSupported,h={},d=this._initPTS,c=!d||i,f="audio/mp4";if(c&&(n=a=1/0),t.config&&o.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}h.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&u.mpeg?new Uint8Array(0):mn.initSegment([t]),metadata:{channelCount:t.channelCount}},c&&(s=t.inputTimeScale,d&&s===d.timescale?c=!1:n=a=o[0].pts-Math.round(s*r))}if(e.sps&&e.pps&&l.length){if(e.timescale=e.inputTimeScale,h.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:mn.initSegment([e]),metadata:{width:e.width,height:e.height}},c)if(s=e.inputTimeScale,d&&s===d.timescale)c=!1;else{var g=this.getVideoStartPts(l),v=Math.round(s*r);a=Math.min(a,An(l[0].dts,g)-v),n=Math.min(n,g-v)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(h).length)return this.ISGenerated=!0,c?(this._initPTS={baseTime:n,timescale:s},this._initDTS={baseTime:a,timescale:s}):n=s=void 0,{tracks:h,initPTS:n,timescale:s}},e.remuxVideo=function(t,e,r,i){var n,a,s=t.inputTimeScale,l=t.samples,u=[],h=l.length,d=this._initPTS,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,v=Number.POSITIVE_INFINITY,m=Number.NEGATIVE_INFINITY,p=!1;if(!r||null===c){var y=e*s,E=l[0].pts-An(l[0].dts,l[0].pts);Tn&&null!==c&&Math.abs(y-E-c)<15e3?r=!0:c=y-E}for(var T=d.baseTime*s/d.timescale,R=0;R0?R-1:R].dts&&(p=!0)}p&&l.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),n=l[0].dts;var b=(a=l[l.length-1].dts)-n,D=b?Math.round(b/(h-1)):g||t.inputTimeScale/30;if(r){var I=n-c,C=I>D,_=I<-1;if((C||_)&&(C?w.warn("AVC: "+En(I,!0)+" ms ("+I+"dts) hole between fragments detected at "+e.toFixed(3)):w.warn("AVC: "+En(-I,!0)+" ms ("+I+"dts) overlapping between fragments detected at "+e.toFixed(3)),!_||c>=l[0].pts||Tn)){n=c;var x=l[0].pts-I;if(C)l[0].dts=n,l[0].pts=x;else for(var P=0;Px);P++)l[P].dts-=I,l[P].pts-=I;w.log("Video: Initial PTS/DTS adjusted: "+En(x,!0)+"/"+En(n,!0)+", delta: "+En(I,!0)+" ms")}}for(var F=0,M=0,O=n=Math.max(0,n),N=0;N0?$.dts-l[J-1].dts:D;if(st=J>0?$.pts-l[J-1].pts:D,ot.stretchShortVideoTrack&&null!==this.nextAudioPts){var ut=Math.floor(ot.maxBufferHole*s),ht=(i?v+i*s:this.nextAudioPts)-$.pts;ht>ut?((g=ht-lt)<0?g=lt:j=!0,w.log("[mp4-remuxer]: It is approximately "+ht/90+" ms to the next segment; using duration "+g/90+" ms for the last video frame.")):g=lt}else g=lt}var dt=Math.round($.pts-$.dts);q=Math.min(q,g),z=Math.max(z,g),X=Math.min(X,st),Q=Math.max(Q,st),u.push(new Dn($.key,g,tt,dt))}if(u.length)if(Tn){if(Tn<70){var ct=u[0].flags;ct.dependsOn=2,ct.isNonSync=0}}else if(Sn&&Q-X0&&(i&&Math.abs(p-m)<9e3||Math.abs(An(g[0].pts-y,p)-m)<20*u),g.forEach((function(t){t.pts=An(t.pts-y,p)})),!r||m<0){if(g=g.filter((function(t){return t.pts>=0})),!g.length)return;m=0===n?0:i&&!f?Math.max(0,p):g[0].pts}if("aac"===t.segmentCodec)for(var E=this.config.maxAudioFramesDrift,T=0,R=m;T=E*u&&I<1e4&&f){var C=Math.round(D/u);(R=b-C*u)<0&&(C--,R+=u),0===T&&(this.nextAudioPts=m=R),w.warn("[mp4-remuxer]: Injecting "+C+" audio frame @ "+(R/a).toFixed(3)+"s due to "+Math.round(1e3*D/a)+" ms gap.");for(var _=0;_0))return;N+=v;try{F=new Uint8Array(N)}catch(t){return void this.observer.emit(S.ERROR,S.ERROR,{type:L.MUX_ERROR,details:A.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:N,reason:"fail allocating audio mdat "+N})}d||(new DataView(F.buffer).setUint32(0,N),F.set(mn.types.mdat,4))}F.set(H,v);var Y=H.byteLength;v+=Y,c.push(new Dn(!0,l,Y,0)),O=V}var W=c.length;if(W){var j=c[c.length-1];this.nextAudioPts=m=O+s*j.duration;var q=d?new Uint8Array(0):mn.moof(t.sequenceNumber++,M/s,o({},t,{samples:c}));t.samples=[];var X=M/a,z=m/a,Q={data1:q,data2:F,startPTS:X,endPTS:z,startDTS:X,endDTS:z,type:"audio",hasAudio:!0,hasVideo:!1,nb:W};return this.isAudioContiguous=!0,Q}},e.remuxEmptyAudio=function(t,e,r,i){var n=t.inputTimeScale,a=n/(t.samplerate?t.samplerate:n),s=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,u=(null!==s?s:i.startDTS*n)+l,h=i.endDTS*n+l,d=1024*a,c=Math.ceil((h-u)/d),f=gn.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(w.warn("[mp4-remuxer]: remux empty Audio"),f){for(var g=[],v=0;v4294967296;)t+=r;return t}function Rn(t,e,r,i){var n=t.samples.length;if(n){for(var a=t.inputTimeScale,s=0;s0;n||(i=xt(e,["encv"])),i.forEach((function(t){xt(n?t.subarray(28):t.subarray(78),["sinf"]).forEach((function(t){var e=Bt(t);if(e){var i=e.subarray(8,24);i.some((function(t){return 0!==t}))||(w.log("[eme] Patching keyId in 'enc"+(n?"a":"v")+">sinf>>tenc' box: "+Lt(i)+" -> "+Lt(r)),e.set(r,8))}}))}))})),t}(t,i)),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Ft(t);i.audio&&(e=wn(i.audio,O)),i.video&&(r=wn(i.video,N));var n={};i.audio&&i.video?n.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?n.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?n.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:w.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=n},e.remux=function(t,e,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,h={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};y(u)||(u=this.lastEndTime=n||0);var d=e.samples;if(null==d||!d.length)return h;var c={initPTS:void 0,timescale:1},f=this.initData;if(null!=(s=f)&&s.length||(this.generateInitSegment(d),f=this.initData),null==(o=f)||!o.length)return w.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),h;this.emitInitSegment&&(c.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(t,e){for(var r=0,i=0,n=0,a=xt(t,["moof","traf"]),s=0;sn}(l,m,n,g)||c.timescale!==l.timescale&&a)&&(c.initPTS=m-n,l&&1===l.timescale&&w.warn("Adjusting initPTS by "+(c.initPTS-l.baseTime)),this.initPTS=l={baseTime:c.initPTS,timescale:1});var p=t?m-l.baseTime/l.timescale:u,E=p+g;!function(t,e,r){xt(e,["moof","traf"]).forEach((function(e){xt(e,["tfhd"]).forEach((function(i){var n=It(i,4),a=t[n];if(a){var s=a.timescale||9e4;xt(e,["tfdt"]).forEach((function(t){var e=t[0],i=r*s;if(i){var n=It(t,4);if(0===e)n-=i,_t(t,4,n=Math.max(n,0));else{n*=Math.pow(2,32),n+=It(t,8),n-=i,n=Math.max(n,0);var a=Math.floor(n/(At+1)),o=Math.floor(n%(At+1));_t(t,4,a),_t(t,8,o)}}}))}}))}))}(f,d,l.baseTime/l.timescale),g>0?this.lastEndTime=E:(w.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var T=!!f.audio,S=!!f.video,L="";T&&(L+="audio"),S&&(L+="video");var A={data1:d,startPTS:p,startDTS:p,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return h.audio="audio"===A.type?A:void 0,h.video="audio"!==A.type?A:void 0,h.initSegment=c,h.id3=Rn(r,n,l,l),i.samples.length&&(h.text=kn(i,n,l)),h},t}();function wn(t,e){var r=null==t?void 0:t.codec;if(r&&r.length>4)return r;if(e===O){if("ec-3"===r||"ac-3"===r||"alac"===r)return r;if("fLaC"===r||"Opus"===r)return he(r,!1);var i="mp4a.40.5";return w.info('Parsed audio codec "'+r+'" or audio object type not handled. Using "'+i+'"'),i}return w.warn('Unhandled video codec "'+r+'"'),"hvc1"===r||"hev1"===r?"hvc1.1.6.L120.90":"av01"===r?"av01.0.04M.08":"avc1.42e01e"}try{bn=self.performance.now.bind(self.performance)}catch(t){w.debug("Unable to use Performance API on this environment"),bn=null==j?void 0:j.Date.now}var Cn=[{demux:Ji,remux:In},{demux:on,remux:Ln},{demux:zi,remux:Ln},{demux:fn,remux:Ln}];Cn.splice(2,0,{demux:Zi,remux:Ln});var _n=function(){function t(t,e,r,i,n){this.async=!1,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.vendor=void 0,this.id=void 0,this.demuxer=void 0,this.remuxer=void 0,this.decrypter=void 0,this.probe=void 0,this.decryptionPromise=null,this.transmuxConfig=void 0,this.currentTransmuxState=void 0,this.observer=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=n}var e=t.prototype;return e.configure=function(t){this.transmuxConfig=t,this.decrypter&&this.decrypter.reset()},e.push=function(t,e,r,i){var n=this,a=r.transmuxing;a.executeStart=bn();var s=new Uint8Array(t),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,h=u.contiguous,d=u.discontinuity,c=u.trackSwitch,f=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,E=l.duration,T=l.initSegmentData,R=function(t,e){var r=null;return t.byteLength>0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(r=e),r}(s,e);if(R&&"AES-128"===R.method){var k=this.getDecrypter();if(!k.isSync())return this.decryptionPromise=k.webCryptoDecrypt(s,R.key.buffer,R.iv.buffer).then((function(t){var e=n.push(t,null,r);return n.decryptionPromise=null,e})),this.decryptionPromise;var b=k.softwareDecrypt(s,R.key.buffer,R.iv.buffer);if(r.part>-1&&(b=k.flush()),!b)return a.executeEnd=bn(),xn(r);s=new Uint8Array(b)}var D=this.needsProbing(d,c);if(D){var I=this.configureTransmuxer(s);if(I)return w.warn("[transmuxer] "+I.message),this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:I,reason:I.message}),a.executeEnd=bn(),xn(r)}(d||c||v||D)&&this.resetInitSegment(T,m,p,E,e),(d||v||D)&&this.resetInitialTimestamp(y),h||this.resetContiguity();var C=this.transmux(s,R,g,f,r),_=this.currentTransmuxState;return _.contiguous=!0,_.discontinuity=!1,_.trackSwitch=!1,a.executeEnd=bn(),C},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=bn();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return a.then((function(){return e.flush(t)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l,null,t))}var u=this.demuxer,h=this.remuxer;if(!u||!h)return r.executeEnd=bn(),[xn(t)];var d=u.flush(o);return Pn(d)?d.then((function(r){return e.flushRemux(s,r,t),s})):(this.flushRemux(s,d,t),s)},e.flushRemux=function(t,e,r){var i=e.audioTrack,n=e.videoTrack,a=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;w.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var h=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);t.push({remuxResult:h,chunkMeta:r}),r.transmuxing.executeEnd=bn()},e.resetInitialTimestamp=function(t){var e=this.demuxer,r=this.remuxer;e&&r&&(e.resetTimeStamp(t),r.resetTimeStamp(t))},e.resetContiguity=function(){var t=this.demuxer,e=this.remuxer;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())},e.resetInitSegment=function(t,e,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(t,e,r,i),s.resetInitSegment(t,e,r,n))},e.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},e.transmux=function(t,e,r,i,n){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,n):this.transmuxUnencrypted(t,r,i,n)},e.transmuxUnencrypted=function(t,e,r,i){var n=this.demuxer.demux(t,e,!1,!this.config.progressive),a=n.audioTrack,s=n.videoTrack,o=n.id3Track,l=n.textTrack;return{remuxResult:this.remuxer.remux(a,s,o,l,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,n){var a=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:a.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,r,i,!1,a.id),chunkMeta:n}}))},e.configureTransmuxer=function(t){for(var e,r=this.config,i=this.observer,n=this.typeSupported,a=this.vendor,s=0,o=Cn.length;s1&&l.id===(null==m?void 0:m.stats.chunkCount),L=!y&&(1===E||0===E&&(1===T||S&&T<=0)),A=self.performance.now();(y||E||0===n.stats.parsing.start)&&(n.stats.parsing.start=A),!a||!T&&L||(a.stats.parsing.start=A);var R=!(m&&(null==(h=n.initSegment)?void 0:h.url)===(null==(d=m.initSegment)?void 0:d.url)),k=new Mn(p,L,o,y,g,R);if(!L||p||R){w.log("[transmuxer-interface, "+n.type+"]: Starting new transmux session for sn: "+l.sn+" p: "+l.part+" level: "+l.level+" id: "+l.id+"\n discontinuity: "+p+"\n trackSwitch: "+y+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+g+"\n initSegmentChange: "+R);var b=new Fn(r,i,e,s,u);this.configureTransmuxer(b)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:v,chunkMeta:l,state:k},t instanceof ArrayBuffer?[t]:[]);else if(f){var D=f.push(t,v,l,k);Pn(D)?(f.async=!0,D.then((function(t){c.handleTransmuxComplete(t)})).catch((function(t){c.transmuxerError(t,l,"transmuxer-interface push error")}))):(f.async=!1,this.handleTransmuxComplete(D))}},r.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(r){var i=r.flush(t);Pn(i)||r.async?(Pn(i)||(i=Promise.resolve(i)),i.then((function(r){e.handleFlushResult(r,t)})).catch((function(r){e.transmuxerError(r,t,"transmuxer-interface flush error")}))):this.handleFlushResult(i,t)}},r.transmuxerError=function(t,e,r){this.hls&&(this.error=t,this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,chunkMeta:e,fatal:!1,error:t,err:t,reason:r}))},r.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},r.onWorkerMessage=function(t){var e=t.data,r=this.hls;switch(e.event){case"init":var i,n=null==(i=this.workerContext)?void 0:i.objectURL;n&&self.URL.revokeObjectURL(n);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":w[e.data.logType]&&w[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}},r.configureTransmuxer=function(t){var e=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)},r.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},e}();function Hn(t,e){if(t.length!==e.length)return!1;for(var r=0;r0&&-1===t?(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e,this.state=vi):(this.loadedmetadata=!1,this.state=Ei),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()},r.doTick=function(){switch(this.state){case vi:this.doTickIdle();break;case Ei:var e,r=this.levels,i=this.trackId,n=null==r||null==(e=r[i])?void 0:e.details;if(n){if(this.waitForCdnTuneIn(n))break;this.state=Ri}break;case yi:var a,s=performance.now(),o=this.retryDate;if(!o||s>=o||null!=(a=this.media)&&a.seeking){var l=this.levels,u=this.trackId;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==l?void 0:l[u])||null),this.state=vi}break;case Ri:var h=this.waitingData;if(h){var d=h.frag,c=h.part,f=h.cache,g=h.complete;if(void 0!==this.initPTS[d.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=pi;var v={frag:d,part:c,payload:f.flush(),networkDetails:null};this._handleFragmentLoadProgress(v),g&&t.prototype._handleFragmentLoadComplete.call(this,v)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log("Waiting fragment cc ("+d.cc+") cancelled because video is at cc "+this.videoTrackCC),this.clearWaitingFragment();else{var m=this.getLoadPosition(),p=Jr.bufferInfo(this.mediaBuffer,m,this.config.maxBufferHole);Er(p.end,this.config.maxFragLookUpTolerance,d)<0&&(this.log("Waiting fragment cc ("+d.cc+") @ "+d.start+" cancelled because another fragment at "+p.end+" is needed"),this.clearWaitingFragment())}}else this.state=vi}this.onTickEnd()},r.clearWaitingFragment=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=vi)},r.resetLoadingState=function(){this.clearWaitingFragment(),t.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var t=this.media;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)},r.doTickIdle=function(){var t=this.hls,e=this.levels,r=this.media,i=this.trackId,n=t.config;if((r||!this.startFragRequested&&n.startFragPrefetch)&&null!=e&&e[i]){var a=e[i],s=a.details;if(!s||s.live&&this.levelLastLoaded!==a||this.waitForCdnTuneIn(s))this.state=Ei;else{var o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,O,Ce));var l=this.getFwdBufferInfo(o,Ce);if(null!==l){var u=this.bufferedTrack,h=this.switchingTrack;if(!h&&this._streamEnded(l,s))return t.trigger(S.BUFFER_EOS,{type:"audio"}),void(this.state=Li);var d=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,we),c=l.len,f=this.getMaxBufferLength(null==d?void 0:d.len),g=s.fragments,v=g[0].start,m=this.flushing?this.getLoadPosition():l.end;if(h&&r){var p=this.getLoadPosition();u&&!Vn(h.attrs,u.attrs)&&(m=p),s.PTSKnown&&pv||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),r.currentTime=v+.05)}if(!(c>=f&&!h&&md.end+s.targetduration;if(T||(null==d||!d.len)&&l.len){var L=this.getAppendedFrag(y.start,we);if(null===L)return;if(E||(E=!!L.gap||!!T&&0===d.len),T&&!E||E&&l.nextStart&&l.nextStart-1)n=a[o];else{var l=Nr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={audioTracks:a};this.log("Updating audio tracks, "+a.length+" track(s) found in group(s): "+(null==r?void 0:r.join(","))),this.hls.trigger(S.AUDIO_TRACKS_UPDATED,h);var d=this.trackId;if(-1!==u&&-1===d)this.setAudioTrack(u);else if(a.length&&-1===d){var c,f=new Error("No audio track selected for current audio group-ID(s): "+(null==(c=this.groupIds)?void 0:c.join(","))+" track count: "+a.length);this.warn(f.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:f})}}else this.shouldReloadPlaylist(n)&&this.setAudioTrack(this.trackId)}},r.onError=function(t,e){!e.fatal&&e.context&&(e.context.type!==De||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))},r.setAudioOption=function(t){var e=this.hls;if(e.config.audioPreference=t,t){var r=this.allAudioTracks;if(this.selectDefaultTrack=!1,r.length){var i=this.currentTrack;if(i&&Ur(t,i,Br))return i;var n=Nr(t,this.tracksInGroup,Br);if(n>-1){var a=this.tracksInGroup[n];return this.setAudioTrack(n),a}if(i){var s=e.loadLevel;-1===s&&(s=e.firstAutoLevel);var o=function(t,e,r,i,n){var a=e[i],s=e.reduce((function(t,e,r){var i=e.uri;return(t[i]||(t[i]=[])).push(r),t}),{})[a.uri];s.length>1&&(i=Math.max.apply(Math,s));var o=a.videoRange,l=a.frameRate,u=a.codecSet.substring(0,4),h=Gr(e,i,(function(e){if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==u)return!1;var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Nr(t,a,n)>-1}));return h>-1?h:Gr(e,i,(function(e){var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Nr(t,a,n)>-1}))}(t,e.levels,r,s,Br);if(-1===o)return null;e.nextLoadLevel=o}if(t.channels||t.audioCodec){var l=Nr(t,r);if(l>-1)return r[l]}}}return null},r.setAudioTrack=function(t){var e=this.tracksInGroup;if(t<0||t>=e.length)this.warn("Invalid audio track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,n=e[t],a=n.details&&!n.details.live;if(!(t===this.trackId&&n===r&&a||(this.log("Switching to audio-track "+t+' "'+n.name+'" lang:'+n.lang+" group:"+n.groupId+" channels:"+n.channels),this.trackId=t,this.currentTrack=n,this.hls.trigger(S.AUDIO_TRACK_SWITCHING,i({},n)),a))){var s=this.switchParams(n.url,null==r?void 0:r.details,n.details);this.loadPlaylist(s)}}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=n[o].start&&s<=n[o].end){a=n[o];break}var l=r.start+r.duration;a?a.end=l:(a={start:s,end:l},n.push(a)),this.fragmentTracker.fragBuffered(r),this.fragBufferedComplete(r,null)}}},r.onBufferFlushing=function(t,e){var r=e.startOffset,i=e.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;e.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(t){for(var e=0;e=n.length||s!==i)&&o){this.log("Subtitle track "+s+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+",duration:"+a.totalduration),this.mediaBuffer=this.mediaBufferTimeRanges;var l=0;if(a.live||null!=(r=o.details)&&r.live){var u=this.mainDetails;if(a.deltaUpdateFailed||!u)return;var h,d=u.fragments[0];o.details?0===(l=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details))&&d&&lr(a,l=d.start):a.hasProgramDateTime&&u.hasProgramDateTime?(ii(a,u),l=a.fragments[0].start):d&&lr(a,l=d.start)}o.details=a,this.levelLastLoaded=o,this.startFragRequested||!this.mainDetails&&a.live||this.setStartPosition(this.mainDetails||a,l),this.tick(),a.live&&!this.fragCurrent&&this.media&&this.state===vi&&(yr(null,a.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0))}}else this.warn("Subtitle tracks were reset while loading level "+s)},r._handleFragmentLoadComplete=function(t){var e=this,r=t.frag,i=t.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&null!=n&&n.key&&n.iv&&"AES-128"===n.method){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer).catch((function(t){throw a.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:r}),t})).then((function(t){var e=performance.now();a.trigger(S.FRAG_DECRYPTED,{frag:r,payload:t,stats:{tstart:s,tdecrypt:e}})})).catch((function(t){e.warn(t.name+": "+t.message),e.state=vi}))}},r.doTick=function(){if(this.media){if(this.state===vi){var t=this.currentTrackId,e=this.levels,r=null==e?void 0:e[t];if(!r||!e.length||!r.details)return;var i=this.config,n=this.getLoadPosition(),a=Jr.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=this.getFwdBufferInfo(this.media,we),u=r.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+u.levelTargetDuration)return;var h=u.fragments,d=h.length,c=u.edge,f=null,g=this.fragPrevious;if(sc-v?0:v;!(f=yr(g,h,Math.max(h[0].start,s),m))&&g&&g.start>>=0)>i-1)throw new DOMException("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return t[r][e]};this.buffered={get length(){return t.length},end:function(r){return e("end",r,t.length)},start:function(r){return e("start",r,t.length)}}},zn=function(t){function e(e){var r;return(r=t.call(this,e,"[subtitle-track-controller]")||this).media=null,r.tracks=[],r.groupIds=null,r.tracksInGroup=[],r.trackId=-1,r.currentTrack=null,r.selectDefaultTrack=!0,r.queuedDefaultTrack=-1,r.asyncPollTrackChange=function(){return r.pollTrackChange(0)},r.useTextTrackPolling=!1,r.subtitlePollingInterval=-1,r._subtitleDisplay=!0,r.onTextTracksChanged=function(){if(r.useTextTrackPolling||self.clearInterval(r.subtitlePollingInterval),r.media&&r.hls.config.renderTextTracksNatively){for(var t=null,e=Be(r.media.textTracks),i=0;i-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))},r.pollTrackChange=function(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)},r.onMediaDetaching=function(){this.media&&(self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),Be(this.media.textTracks).forEach((function(t){Ne(t)})),this.subtitleTrack=-1,this.media=null)},r.onManifestLoading=function(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0},r.onManifestParsed=function(t,e){this.tracks=e.subtitleTracks},r.onSubtitleTrackLoaded=function(t,e){var r=e.id,i=e.groupId,n=e.details,a=this.tracksInGroup[r];if(a&&a.groupId===i){var s=a.details;a.details=e.details,this.log("Subtitle track "+r+' "'+a.name+'" lang:'+a.lang+" group:"+i+" loaded ["+n.startSN+"-"+n.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,e,s)}else this.warn("Subtitle track with id:"+r+" and group:"+i+" not found in active group "+(null==a?void 0:a.groupId))},r.onLevelLoading=function(t,e){this.switchLevel(e.level)},r.onLevelSwitching=function(t,e){this.switchLevel(e.level)},r.switchLevel=function(t){var e=this.hls.levels[t];if(e){var r=e.subtitleGroups||null,i=this.groupIds,n=this.currentTrack;if(!r||(null==i?void 0:i.length)!==(null==r?void 0:r.length)||null!=r&&r.some((function(t){return-1===(null==i?void 0:i.indexOf(t))}))){this.groupIds=r,this.trackId=-1,this.currentTrack=null;var a=this.tracks.filter((function(t){return!r||-1!==r.indexOf(t.groupId)}));if(a.length)this.selectDefaultTrack&&!a.some((function(t){return t.default}))&&(this.selectDefaultTrack=!1),a.forEach((function(t,e){t.id=e}));else if(!n&&!this.tracksInGroup.length)return;this.tracksInGroup=a;var s=this.hls.config.subtitlePreference;if(!n&&s){this.selectDefaultTrack=!1;var o=Nr(s,a);if(o>-1)n=a[o];else{var l=Nr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={subtitleTracks:a};this.log("Updating subtitle tracks, "+a.length+' track(s) found in "'+(null==r?void 0:r.join(","))+'" group-id'),this.hls.trigger(S.SUBTITLE_TRACKS_UPDATED,h),-1!==u&&-1===this.trackId&&this.setSubtitleTrack(u)}else this.shouldReloadPlaylist(n)&&this.setSubtitleTrack(this.trackId)}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=this.selectDefaultTrack,i=0;i-1){var n=this.tracksInGroup[i];return this.setSubtitleTrack(i),n}if(r)return null;var a=Nr(t,e);if(a>-1)return e[a]}}return null},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentTrack;if(this.shouldLoadPlaylist(r)&&r){var i=r.id,n=r.groupId,a=r.url;if(e)try{a=e.addDirectives(a)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}this.log("Loading subtitle playlist for id "+i),this.hls.trigger(S.SUBTITLE_TRACK_LOADING,{url:a,id:i,groupId:n,deliveryDirectives:e||null})}},r.toggleTrackModes=function(){var t=this.media;if(t){var e,r=Be(t.textTracks),i=this.currentTrack;if(i&&((e=r.filter((function(t){return Yn(i,t)}))[0])||this.warn('Unable to find subtitle TextTrack with name "'+i.name+'" and language "'+i.lang+'"')),[].slice.call(r).forEach((function(t){"disabled"!==t.mode&&t!==e&&(t.mode="disabled")})),e){var n=this.subtitleDisplay?"showing":"hidden";e.mode!==n&&(e.mode=n)}}},r.setSubtitleTrack=function(t){var e=this.tracksInGroup;if(this.media)if(t<-1||t>=e.length||!y(t))this.warn("Invalid subtitle track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),i){var n=!!i.details&&!i.details.live;if(t!==this.trackId||i!==r||!n){this.log("Switching to subtitle-track "+t+(i?' "'+i.name+'" lang:'+i.lang+" group:"+i.groupId:""));var a=i.id,s=i.groupId,o=void 0===s?"":s,l=i.name,u=i.type,h=i.url;this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:h});var d=this.switchParams(i.url,null==r?void 0:r.details,i.details);this.loadPlaylist(d)}}else this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:t})}else this.queuedDefaultTrack=t},s(e,[{key:"subtitleDisplay",get:function(){return this._subtitleDisplay},set:function(t){this._subtitleDisplay=t,this.trackId>-1&&this.toggleTrackModes()}},{key:"allSubtitleTracks",get:function(){return this.tracks}},{key:"subtitleTracks",get:function(){return this.tracksInGroup}},{key:"subtitleTrack",get:function(){return this.trackId},set:function(t){this.selectDefaultTrack=!1,this.setSubtitleTrack(t)}}]),e}(wr),Qn=function(){function t(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}var e=t.prototype;return e.append=function(t,e,r){var i=this.queues[e];i.push(t),1!==i.length||r||this.executeNext(e)},e.insertAbort=function(t,e){this.queues[e].unshift(t),this.executeNext(e)},e.appendBlocker=function(t){var e,r=new Promise((function(t){e=t})),i={execute:e,onStart:function(){},onComplete:function(){},onError:function(){}};return this.append(i,t),r},e.executeNext=function(t){var e=this.queues[t];if(e.length){var r=e[0];try{r.execute()}catch(e){w.warn('[buffer-operation-queue]: Exception executing "'+t+'" SourceBuffer operation: '+e),r.onError(e);var i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}},e.shiftAndExecuteNext=function(t){this.queues[t].shift(),this.executeNext(t)},e.current=function(t){return this.queues[t][0]},t}(),Jn=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/,$n=function(){function t(t){var e=this;this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=function(t){e.hls&&e.hls.pauseBuffering()},this._onStartStreaming=function(t){e.hls&&e.hls.resumeBuffering()},this._onMediaSourceOpen=function(){var t=e.media,r=e.mediaSource;e.log("Media source opened"),t&&(t.removeEventListener("emptied",e._onMediaEmptied),e.updateMediaElementDuration(),e.hls.trigger(S.MEDIA_ATTACHED,{media:t,mediaSource:r})),r&&r.removeEventListener("sourceopen",e._onMediaSourceOpen),e.checkPendingTracks()},this._onMediaSourceClose=function(){e.log("Media source closed")},this._onMediaSourceEnded=function(){e.log("Media source ended")},this._onMediaEmptied=function(){var t=e.mediaSrc,r=e._objectUrl;t!==r&&w.error("Media element src was set while attaching MediaSource ("+r+" > "+t+")")},this.hls=t;var r,i="[buffer-controller]";this.appendSource=(r=ee(t.config.preferManagedMediaSource),"undefined"!=typeof self&&r===self.ManagedMediaSource),this.log=w.log.bind(w,i),this.warn=w.warn.bind(w,i),this.error=w.error.bind(w,i),this._initSourceBuffer(),this.registerListeners()}var e=t.prototype;return e.hasSourceTypes=function(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null},e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.BUFFER_RESET,this.onBufferReset,this),t.on(S.BUFFER_APPENDING,this.onBufferAppending,this),t.on(S.BUFFER_CODECS,this.onBufferCodecs,this),t.on(S.BUFFER_EOS,this.onBufferEos,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(S.FRAG_PARSED,this.onFragParsed,this),t.on(S.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.BUFFER_RESET,this.onBufferReset,this),t.off(S.BUFFER_APPENDING,this.onBufferAppending,this),t.off(S.BUFFER_CODECS,this.onBufferCodecs,this),t.off(S.BUFFER_EOS,this.onBufferEos,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(S.FRAG_PARSED,this.onFragParsed,this),t.off(S.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new Qn(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null},e.onManifestLoading=function(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,this.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media,i=ee(this.appendSource);if(r&&i){var n,a=this.mediaSource=new i;this.log("created media source: "+(null==(n=a.constructor)?void 0:n.name)),a.addEventListener("sourceopen",this._onMediaSourceOpen),a.addEventListener("sourceended",this._onMediaSourceEnded),a.addEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(a.addEventListener("startstreaming",this._onStartStreaming),a.addEventListener("endstreaming",this._onEndStreaming));var s=this._objectUrl=self.URL.createObjectURL(a);if(this.appendSource)try{r.removeAttribute("src");var o=self.ManagedMediaSource;r.disableRemotePlayback=r.disableRemotePlayback||o&&a instanceof o,Zn(r),function(t,e){var r=self.document.createElement("source");r.type="video/mp4",r.src=e,t.appendChild(r)}(r,s),r.load()}catch(t){r.src=s}else r.src=s;r.addEventListener("emptied",this._onMediaEmptied)}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn("onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming)),t&&(t.removeEventListener("emptied",this._onMediaEmptied),r&&self.URL.revokeObjectURL(r),this.mediaSrc===r?(t.removeAttribute("src"),this.appendSource&&Zn(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(S.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){t.resetBuffer(e)})),this._initSourceBuffer()},e.resetBuffer=function(t){var e=this.sourceBuffer[t];try{var r;e&&(this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(r=this.mediaSource)&&r.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e))}catch(e){this.warn("onBufferReset "+t,e)}},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length,n=Object.keys(e);if(n.forEach((function(t){if(i){var n=r.tracks[t];if(n&&"function"==typeof n.buffer.changeType){var a,s=e[t],o=s.id,l=s.codec,u=s.levelCodec,h=s.container,d=s.metadata,c=de(n.codec,n.levelCodec),f=null==c?void 0:c.replace(Jn,"$1"),g=de(l,u),v=null==(a=g)?void 0:a.replace(Jn,"$1");if(g&&f!==v){"audio"===t.slice(0,5)&&(g=he(g,r.appendSource));var m=h+";codecs="+g;r.appendChangeType(t,m),r.log("switching codec "+c+" to "+g),r.tracks[t]={buffer:n.buffer,codec:l,container:h,levelCodec:u,metadata:d,id:o}}}}else r.pendingTracks[t]=e[t]})),!i){var a=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==a&&(this.log(a+" bufferCodec event(s) expected "+n.join(",")),this.bufferCodecEventsExpected=a),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,n={execute:function(){var n=r.sourceBuffer[t];n&&(r.log("changing "+t+" sourceBuffer type to "+e),n.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){r.warn("Failed to change "+t+" SourceBuffer type",e)}};i.append(n,t,!!this.pendingTracks[t])},e.onBufferAppending=function(t,e){var r=this,i=this.hls,n=this.operationQueue,a=this.tracks,s=e.data,o=e.type,l=e.frag,u=e.part,h=e.chunkMeta,d=h.buffering[o],c=self.performance.now();d.start=c;var f=l.stats.buffering,g=u?u.stats.buffering:null;0===f.start&&(f.start=c),g&&0===g.start&&(g.start=c);var v=a.audio,m=!1;"audio"===o&&"audio/mpeg"===(null==v?void 0:v.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);var p=l.start,y={execute:function(){if(d.executeStart=self.performance.now(),m){var t=r.sourceBuffer[o];if(t){var e=p-t.timestampOffset;Math.abs(e)>=.1&&(r.log("Updating audio SourceBuffer timestampOffset to "+p+" (delta: "+e+") sn: "+l.sn+")"),t.timestampOffset=p)}}r.appendExecutor(s,o)},onStart:function(){},onComplete:function(){var t=self.performance.now();d.executeEnd=d.end=t,0===f.first&&(f.first=t),g&&0===g.first&&(g.first=t);var e=r.sourceBuffer,i={};for(var n in e)i[n]=Jr.getBuffered(e[n]);r.appendErrors[o]=0,"audio"===o||"video"===o?r.appendErrors.audiovideo=0:(r.appendErrors.audio=0,r.appendErrors.video=0),r.hls.trigger(S.BUFFER_APPENDED,{type:o,frag:l,part:u,chunkMeta:h,parent:l.type,timeRanges:i})},onError:function(t){var e={type:L.MEDIA_ERROR,parent:l.type,details:A.BUFFER_APPEND_ERROR,sourceBufferName:o,frag:l,part:u,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=A.BUFFER_FULL_ERROR;else{var n=++r.appendErrors[o];e.details=A.BUFFER_APPEND_ERROR,r.warn("Failed "+n+"/"+i.config.appendErrorMaxRetry+' times to append segment in "'+o+'" sourceBuffer'),n>=i.config.appendErrorMaxRetry&&(e.fatal=!0)}i.trigger(S.ERROR,e)}};n.append(y,o,!!this.pendingTracks[o])},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,n=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(S.BUFFER_FLUSHED,{type:t})},onError:function(e){r.warn("Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(n(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(n(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,n=e.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[U]?a.push("audiovideo"):(s[O]&&a.push("audio"),s[N]&&a.push("video")),0===a.length&&this.warn("Fragments must have at least one ElementaryStreamType set. type: "+i.type+" level: "+i.level+" sn: "+i.sn),this.blockBuffers((function(){var t=self.performance.now();i.stats.buffering.end=t,n&&(n.stats.buffering.end=t);var e=n?n.stats:i.stats;r.hls.trigger(S.FRAG_BUFFERED,{frag:i,part:n,stats:e,id:i.type})}),a)},e.onFragChanged=function(t,e){this.trimBuffers()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var n=r.sourceBuffer[i];return!n||e.type&&e.type!==i||(n.ending=!0,n.ended||(n.ended=!0,r.log(i+" sourceBuffer now EOS"))),t&&!(n&&!n.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((function(){r.getSourceBufferTypes().forEach((function(t){var e=r.sourceBuffer[t];e&&(e.ending=!1)}));var t=r.mediaSource;t&&"open"===t.readyState?(r.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&r.log("Could not call mediaSource.endOfStream(). mediaSource.readyState: "+t.readyState)})))},e.onLevelUpdated=function(t,e){var r=e.details;r.fragments.length&&(this.details=r,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())},e.trimBuffers=function(){var t=this.hls,e=this.details,r=this.media;if(r&&null!==e&&this.getSourceBufferTypes().length){var i=t.config,n=r.currentTime,a=e.levelTargetDuration,s=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(y(s)&&s>0){var o=Math.max(s,a),l=Math.floor(n/a)*a-o;this.flushBackBuffer(n,a,l)}if(y(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){var u=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),h=Math.max(u,a),d=Math.floor(n/a)*a+h;this.flushFrontBuffer(n,a,d)}}},e.flushBackBuffer=function(t,e,r){var i=this,n=this.details,a=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(s){var o=a[s];if(o){var l=Jr.getBuffered(o);if(l.length>0&&r>l.start(0)){if(i.hls.trigger(S.BACK_BUFFER_REACHED,{bufferEnd:r}),null!=n&&n.live)i.hls.trigger(S.LIVE_BACK_BUFFER_REACHED,{bufferEnd:r});else if(o.ended&&l.end(l.length-1)-t<2*e)return void i.log("Cannot flush "+s+" back buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:r,type:s})}}}))},e.flushFrontBuffer=function(t,e,r){var i=this,n=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(a){var s=n[a];if(s){var o=Jr.getBuffered(s),l=o.length;if(l<2)return;var u=o.start(l-1),h=o.end(l-1);if(r>u||t>=u&&t<=h)return;if(s.ended&&t-h<2*e)return void i.log("Cannot flush "+a+" front buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:u,endOffset:1/0,type:a})}}))},e.updateMediaElementDuration=function(){if(this.details&&this.media&&this.mediaSource&&"open"===this.mediaSource.readyState){var t=this.details,e=this.hls,r=this.media,i=this.mediaSource,n=t.fragments[0].start+t.totalduration,a=r.duration,s=y(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(n>s&&n>a||!y(a))&&(this.log("Updating Media Source duration to "+n.toFixed(3)),i.duration=n)}},e.updateSeekableRange=function(t){var e=this.mediaSource,r=t.fragments;if(r.length&&t.live&&null!=e&&e.setLiveSeekableRange){var i=Math.max(0,r[0].start),n=Math.max(i,i+t.totalduration);this.log("Media Source duration is set to "+e.duration+". Setting seekable range to "+i+"-"+n+"."),e.setLiveSeekableRange(i,n)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&(!t||2===i||"audiovideo"in r)){this.createSourceBuffers(r),this.pendingTracks={};var n=this.getSourceBufferTypes();if(n.length)this.hls.trigger(S.BUFFER_CREATED,{tracks:this.tracks}),n.forEach((function(t){e.executeNext(t)}));else{var a=new Error("could not create source buffer for media codec(s)");this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:a,reason:a.message})}}},e.createSourceBuffers=function(t){var e=this,r=this.sourceBuffer,i=this.mediaSource;if(!i)throw Error("createSourceBuffers called when mediaSource was null");var n=function(n){if(!r[n]){var a,s=t[n];if(!s)throw Error("source buffer exists for track "+n+", however track does not");var o=-1===(null==(a=s.levelCodec)?void 0:a.indexOf(","))?s.levelCodec:s.codec;o&&"audio"===n.slice(0,5)&&(o=he(o,e.appendSource));var l=s.container+";codecs="+o;e.log("creating sourceBuffer("+l+")");try{var u=r[n]=i.addSourceBuffer(l),h=n;e.addBufferListener(h,"updatestart",e._onSBUpdateStart),e.addBufferListener(h,"updateend",e._onSBUpdateEnd),e.addBufferListener(h,"error",e._onSBUpdateError),e.appendSource&&e.addBufferListener(h,"bufferedchange",(function(t,r){var i=r.removedRanges;null!=i&&i.length&&e.hls.trigger(S.BUFFER_FLUSHED,{type:n})})),e.tracks[n]={buffer:u,codec:o,container:s.container,levelCodec:s.levelCodec,metadata:s.metadata,id:s.id}}catch(t){e.error("error while trying to add sourceBuffer: "+t.message),e.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:n,mimeType:l})}}};for(var a in t)n(a)},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e;if("closed"!==(null==(e=this.mediaSource)?void 0:e.readyState)){var r=this.operationQueue;r.current(t).onComplete(),r.shiftAndExecuteNext(t)}else this.resetBuffer(t)},e._onSBUpdateError=function(t,e){var r,i=new Error(t+" SourceBuffer error. MediaSource readyState: "+(null==(r=this.mediaSource)?void 0:r.readyState));this.error(""+i,e),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});var n=this.operationQueue.current(t);n&&n.onError(i)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,a=this.operationQueue,s=this.sourceBuffer[t];if(!i||!n||!s)return this.warn("Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void a.shiftAndExecuteNext(t);var o=y(i.duration)?i.duration:1/0,l=y(n.duration)?n.duration:1/0,u=Math.max(0,e),h=Math.min(r,o,l);h>u&&(!s.ending||s.ended)?(s.ended=!1,this.log("Removing ["+u+","+h+"] from the "+t+" SourceBuffer"),s.remove(u,h)):a.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.sourceBuffer[e];if(r)r.ended=!1,r.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error("Attempting to append to the "+e+" SourceBuffer, but it does not exist")},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);var i=this.operationQueue,n=e.map((function(t){return i.appendBlocker(t)}));Promise.all(n).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];null!=e&&e.updating||i.shiftAndExecuteNext(t)}))}))},e.getSourceBufferTypes=function(){return Object.keys(this.sourceBuffer)},e.addBufferListener=function(t,e,r){var i=this.sourceBuffer[t];if(i){var n=r.bind(this,t);this.listeners[t].push({event:e,listener:n}),i.addEventListener(e,n)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},s(t,[{key:"mediaSrc",get:function(){var t,e=(null==(t=this.media)?void 0:t.firstChild)||this.media;return null==e?void 0:e.src}}]),t}();function Zn(t){var e=t.querySelectorAll("source");[].slice.call(e).forEach((function(e){t.removeChild(e)}))}var ta={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},ea=function(t){var e=t;return ta.hasOwnProperty(t)&&(e=ta[t]),String.fromCharCode(e)},ra=15,ia=100,na={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},aa={17:2,18:4,21:6,22:8,23:10,19:13,20:15},sa={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},oa={25:2,26:4,29:6,30:8,31:10,27:13,28:15},la=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],ua=function(){function t(){this.time=null,this.verboseLevel=0}return t.prototype.log=function(t,e){if(this.verboseLevel>=t){var r="function"==typeof e?e():e;w.log(this.time+" ["+t+"] "+r)}},t}(),ha=function(t){for(var e=[],r=0;ria&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=ia)},e.moveCursor=function(t){var e=this.pos+t;if(t>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=ea(t);this.pos>=ia?this.logger.log(0,(function(){return"Cannot insert "+t.toString(16)+" ("+r+") at position "+e.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},e.clearFromPos=function(t){var e;for(e=t;e0&&(r=t?"["+e.join(" | ")+"]":e.join("\n")),r},e.getTextAndFormat=function(){return this.rows},t}(),va=function(){function t(t,e,r){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new ga(r),this.nonDisplayedMemory=new ga(r),this.lastOutputScreen=new ga(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var e=t.prototype;return e.reset=function(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null},e.getHandler=function(){return this.outputFilter},e.setHandler=function(t){this.outputFilter=t},e.setPAC=function(t){this.writeScreen.setPAC(t)},e.setBkgData=function(t){this.writeScreen.setBkgData(t)},e.setMode=function(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(function(){return"MODE="+t})),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)},e.insertChars=function(t){for(var e=this,r=0;r=46,e.italics)e.foreground="white";else{var r=Math.floor(t/2)-16;e.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)},e.outputDataUpdate=function(t){void 0===t&&(t=!1);var e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))},e.cueSplitAtTime=function(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))},t}(),ma=function(){function t(t,e,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;var i=this.logger=new ua;this.channels=[null,new va(t,e,i),new va(t+1,r,i)]}var e=t.prototype;return e.getHandler=function(t){return this.channels[t].getHandler()},e.setHandler=function(t,e){this.channels[t].setHandler(e)},e.addData=function(t,e){var r,i,n,a=!1;this.logger.time=t;for(var s=0;s ("+ha([i,n])+")"),(r=this.parseCmd(i,n))||(r=this.parseMidrow(i,n)),r||(r=this.parsePAC(i,n)),r||(r=this.parseBackgroundAttributes(i,n)),!r&&(a=this.parseChars(i,n))){var o=this.currentChannel;o&&o>0?this.channels[o].insertChars(a):this.logger.log(2,"No channel found yet. TEXT-MODE?")}r||a||this.logger.log(2,"Couldn't parse cleaned data "+ha([i,n])+" orig: "+ha([e[s],e[s+1]]))}},e.parseCmd=function(t,e){var r=this.cmdHistory;if(!((20===t||28===t||21===t||29===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=33&&e<=35))return!1;if(ya(t,e,r))return pa(null,null,r),this.logger.log(3,"Repeated command ("+ha([t,e])+") is dropped"),!0;var i=20===t||21===t||23===t?1:2,n=this.channels[i];return 20===t||21===t||28===t||29===t?32===e?n.ccRCL():33===e?n.ccBS():34===e?n.ccAOF():35===e?n.ccAON():36===e?n.ccDER():37===e?n.ccRU(2):38===e?n.ccRU(3):39===e?n.ccRU(4):40===e?n.ccFON():41===e?n.ccRDC():42===e?n.ccTR():43===e?n.ccRTD():44===e?n.ccEDM():45===e?n.ccCR():46===e?n.ccENM():47===e&&n.ccEOC():n.ccTO(e-32),pa(t,e,r),this.currentChannel=i,!0},e.parseMidrow=function(t,e){var r=0;if((17===t||25===t)&&e>=32&&e<=47){if((r=17===t?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(e),this.logger.log(3,"MIDROW ("+ha([t,e])+")"),!0)}return!1},e.parsePAC=function(t,e){var r,i=this.cmdHistory;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127||(16===t||24===t)&&e>=64&&e<=95))return!1;if(ya(t,e,i))return pa(null,null,i),!0;var n=t<=23?1:2;r=e>=64&&e<=95?1===n?na[t]:sa[t]:1===n?aa[t]:oa[t];var a=this.channels[n];return!!a&&(a.setPAC(this.interpretPAC(r,e)),pa(t,e,i),this.currentChannel=n,!0)},e.interpretPAC=function(t,e){var r,i={color:null,italics:!1,indent:null,underline:!1,row:t};return r=e>95?e-96:e-64,i.underline=1==(1&r),r<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(r/2)]:r<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((r-16)/2),i},e.parseChars=function(t,e){var r,i,n=null,a=null;if(t>=25?(r=2,a=t-8):(r=1,a=t),a>=17&&a<=19?(i=17===a?e+80:18===a?e+112:e+144,this.logger.log(2,"Special char '"+ea(i)+"' in channel "+r),n=[i]):t>=32&&t<=127&&(n=0===e?[t]:[t,e]),n){var s=ha(n);this.logger.log(3,"Char codes = "+s.join(",")),pa(t,e,this.cmdHistory)}return n},e.parseBackgroundAttributes=function(t,e){var r;if(!((16===t||24===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=45&&e<=47))return!1;var i={};16===t||24===t?(r=Math.floor((e-32)/2),i.background=la[r],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));var n=t<=23?1:2;return this.channels[n].setBkgData(i),pa(t,e,this.cmdHistory),!0},e.reset=function(){for(var t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},e.reset=function(){this.cueRanges=[],this.startTime=null},t}(),Ta=function(){if(null!=j&&j.VTTCue)return self.VTTCue;var t=["","lr","rl"],e=["start","middle","end","left","right"];function r(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;var r=e.toLowerCase();return!!~t.indexOf(r)&&r}function i(t){return r(e,t)}function n(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return T},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return L},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),Sa=function(){function t(){}return t.prototype.decode=function(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))},t}();function La(t){function e(t,e,r,i){return 3600*(0|t)+60*(0|e)+(0|r)+parseFloat(i||0)}var r=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?e(r[2],r[3],0,r[4]):e(r[1],r[2],r[3],r[4]):null}var Aa=function(){function t(){this.values=Object.create(null)}var e=t.prototype;return e.set=function(t,e){this.get(t)||""===e||(this.values[t]=e)},e.get=function(t,e,r){return r?this.has(t)?this.values[t]:e[r]:this.has(t)?this.values[t]:e},e.has=function(t){return t in this.values},e.alt=function(t,e,r){for(var i=0;i=0&&r<=100)return this.set(t,r),!0}return!1},t}();function Ra(t,e,r,i){var n=i?t.split(i):[t];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&e(s[0],s[1])}}var ka=new Ta(0,0,""),ba="middle"===ka.align?"middle":"center";function Da(t,e,r){var i=t;function n(){var e=La(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function a(){t=t.replace(/^\s+/,"")}if(a(),e.startTime=n(),a(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),a(),e.endTime=n(),a(),function(t,e){var i=new Aa;Ra(t,(function(t,e){var n;switch(t){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===e){i.set(t,r[a].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":n=e.split(","),i.integer(t,n[0]),i.percent(t,n[0])&&i.set("snapToLines",!1),i.alt(t,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",ba,"end"]);break;case"position":n=e.split(","),i.percent(t,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",ba,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",ba,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===ka.line&&(n=-1),e.line=n,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",ba);var a=i.get("position","auto");"auto"===a&&50===ka.position&&(a="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=a}(t,e)}function Ia(t){return t.replace(//gi,"\n")}var wa=function(){function t(){this.state="INITIAL",this.buffer="",this.decoder=new Sa,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var e=t.prototype;return e.parse=function(t){var e=this;function r(){var t=e.buffer,r=0;for(t=Ia(t);r>>0).toString()};function Pa(t,e,r){return xa(t.toString())+xa(e.toString())+xa(r)}function Fa(t,e,r,i,n,a,s){var o,l,u,h=new wa,d=Tt(new Uint8Array(t)).trim().replace(Ca,"\n").split("\n"),c=[],f=e?(o=e.baseTime,void 0===(l=e.timescale)&&(l=1),yn(o,pn,1/l)):0,g="00:00.000",v=0,m=0,p=!0;h.oncue=function(t){var a=r[i],s=r.ccOffset,o=(v-f)/9e4;if(null!=a&&a.new&&(void 0!==m?s=r.ccOffset=a.start:function(t,e,r){var i=t[e],n=t[i.prevCC];if(!n||!n.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(a=n)&&a.new;){var a;t.ccOffset+=i.start-n.start,i.new=!1,n=t[(i=n).prevCC]}t.presentationOffset=r}(r,i,o)),o){if(!e)return void(u=new Error("Missing initPTS for VTT MPEGTS"));s=o-r.presentationOffset}var l=t.endTime-t.startTime,h=An(9e4*(t.startTime+s-m),9e4*n)/9e4;t.startTime=Math.max(h,0),t.endTime=Math.max(h+l,0);var d=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(d)),t.id||(t.id=Pa(t.startTime,t.endTime,d)),t.endTime>0&&c.push(t)},h.onparsingerror=function(t){u=t},h.onflush=function(){u?s(u):a(c)},d.forEach((function(t){if(p){if(_a(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((function(t){_a(t,"LOCAL:")?g=t.slice(6):_a(t,"MPEGTS:")&&(v=parseInt(t.slice(7)))}));try{m=function(t){var e=parseInt(t.slice(-3)),r=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),n=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(y(e)&&y(r)&&y(i)&&y(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+t);return e+=1e3*r,(e+=6e4*i)+36e5*n}(g)/1e3}catch(t){u=t}return}""===t&&(p=!1)}h.parse(t+"\n")})),h.flush()}var Ma="stpp.ttml.im1t",Oa=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,Na=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Ua={left:"start",center:"center",right:"end",start:"start",end:"end"};function Ba(t,e,r,i){var n=xt(new Uint8Array(t),["mdat"]);if(0!==n.length){var a,s,l,u,h=n.map((function(t){return Tt(t)})),d=(a=e.baseTime,s=1,void 0===(l=e.timescale)&&(l=1),void 0===u&&(u=!1),yn(a,s,1/l,u));try{h.forEach((function(t){return r(function(t,e){var r=(new DOMParser).parseFromString(t,"text/xml"),i=r.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");var n={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},a=Object.keys(n).reduce((function(t,e){return t[e]=i.getAttribute("ttp:"+e)||n[e],t}),{}),s="preserve"!==i.getAttribute("xml:space"),l=Ka(Ga(i,"styling","style")),u=Ka(Ga(i,"layout","region")),h=Ga(i,"body","[begin]");return[].map.call(h,(function(t){var r=Ha(t,s);if(!r||!t.hasAttribute("begin"))return null;var i=Wa(t.getAttribute("begin"),a),n=Wa(t.getAttribute("dur"),a),h=Wa(t.getAttribute("end"),a);if(null===i)throw Ya(t);if(null===h){if(null===n)throw Ya(t);h=i+n}var d=new Ta(i-e,h-e,r);d.id=Pa(d.startTime,d.endTime,d.text);var c=function(t,e,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=Va(e,i,a)||Va(t,i,a)||Va(n,i,a);return s&&(r[a]=s),r}),{})}(u[t.getAttribute("region")],l[t.getAttribute("style")],l),f=c.textAlign;if(f){var g=Ua[f];g&&(d.lineAlign=g),d.align=f}return o(d,c),d})).filter((function(t){return null!==t}))}(t,d))}))}catch(t){i(t)}}else i(new Error("Could not parse IMSC1 mdat"))}function Ga(t,e,r){var i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function Ka(t){return t.reduce((function(t,e){var r=e.getAttribute("xml:id");return r&&(t[r]=e),t}),{})}function Ha(t,e){return[].slice.call(t.childNodes).reduce((function(t,r,i){var n;return"br"===r.nodeName&&i?t+"\n":null!=(n=r.childNodes)&&n.length?Ha(r,e):e?t+r.textContent.trim().replace(/\s+/g," "):t+r.textContent}),"")}function Va(t,e,r){return t&&t.hasAttributeNS(e,r)?t.getAttributeNS(e,r):null}function Ya(t){return new Error("Could not parse ttml timestamp "+t)}function Wa(t,e){if(!t)return null;var r=La(t);return null===r&&(Oa.test(t)?r=function(t,e){var r=Oa.exec(t),i=(0|r[4])+(0|r[5])/e.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/e.frameRate}(t,e):Na.test(t)&&(r=function(t,e){var r=Na.exec(t),i=Number(r[1]);switch(r[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),r}var ja=function(){function t(t){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this)}var e=t.prototype;return e.destroy=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0},e.initCea608Parsers=function(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){var t=new Ea(this,"textTrack1"),e=new Ea(this,"textTrack2"),r=new Ea(this,"textTrack3"),i=new Ea(this,"textTrack4");this.cea608Parser1=new ma(1,t,e),this.cea608Parser2=new ma(3,r,i)}},e.addCues=function(t,e,r,i,n){for(var a,s,o,l,u=!1,h=n.length;h--;){var d=n[h],c=(a=d[0],s=d[1],o=e,l=r,Math.min(s,l)-Math.max(a,o));if(c>=0&&(d[0]=Math.min(d[0],e),d[1]=Math.max(d[1],r),u=!0,c/(r-e)>.5))return}if(u||n.push([e,r]),this.config.renderTextTracksNatively){var f=this.captionsTracks[t];this.Cues.newCue(f,e,r,i)}else{var g=this.Cues.newCue(null,e,r,i);this.hls.trigger(S.CUES_PARSED,{type:"captions",cues:g,track:t})}},e.onInitPtsFound=function(t,e){var r=this,i=e.frag,n=e.id,a=e.initPTS,s=e.timescale,o=this.unparsedVttFrags;"main"===n&&(this.initPTS[i.cc]={baseTime:a,timescale:s}),o.length&&(this.unparsedVttFrags=[],o.forEach((function(t){r.onFragLoaded(S.FRAG_LOADED,t)})))},e.getExistingTrack=function(t,e){var r=this.media;if(r)for(var i=0;ii.cc||l.trigger(S.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:e})}))}else s.push(t)},e._fallbackToIMSC1=function(t,e){var r=this,i=this.tracks[t.level];i.textCodec||Ba(e,this.initPTS[t.cc],(function(){i.textCodec=Ma,r._parseIMSC1(t,e)}),(function(){i.textCodec="wvtt"}))},e._appendCues=function(t,e){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[e];if(!i||"disabled"===i.mode)return;t.forEach((function(t){return Oe(i,t)}))}else{var n=this.tracks[e];if(!n)return;var a=n.default?"default":"subtitles"+e;r.trigger(S.CUES_PARSED,{type:"subtitles",cues:t,track:a})}},e.onFragDecrypted=function(t,e){e.frag.type===_e&&this.onFragLoaded(S.FRAG_LOADED,e)},e.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},e.onFragParsingUserdata=function(t,e){this.initCea608Parsers();var r=this.cea608Parser1,i=this.cea608Parser2;if(this.enabled&&r&&i){var n=e.frag,a=e.samples;if(n.type!==we||"NONE"!==this.closedCaptionsForLevel(n))for(var s=0;sthis.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},e.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(t,i){return r.isLevelAllowed(t)&&i<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},e.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},e.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},e.getDimensions=function(){if(this.clientRect)return this.clientRect;var t=this.media,e={width:0,height:0};if(t){var r=t.getBoundingClientRect();e.width=r.width,e.height=r.height,e.width||e.height||(e.width=r.right-r.left||t.width||0,e.height=r.bottom-r.top||t.height||0)}return this.clientRect=e,e},e.isLevelAllowed=function(t){return!this.restrictedLevels.some((function(e){return t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height}))},t.getMaxLevelByMediaSize=function(t,e,r){if(null==t||!t.length)return-1;for(var i,n,a=t.length-1,s=Math.max(e,r),o=0;o=s||l.height>=s)&&(i=l,!(n=t[o+1])||i.width!==n.width||i.height!==n.height)){a=o;break}}return a},s(t,[{key:"mediaWidth",get:function(){return this.getDimensions().width*this.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*this.contentScaleFactor}},{key:"contentScaleFactor",get:function(){var t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}}]),t}(),Qa=function(){function t(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}var e=t.prototype;return e.setStreamController=function(t){this.streamController=t},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},e.onMediaAttaching=function(t,e){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=i,i&&"function"==typeof i.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),r.fpsDroppedMonitoringPeriod)}},e.checkFPS=function(t,e,r){var i=performance.now();if(e){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=e-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(S.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;w.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(S.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=e}},e.checkFPSInterval=function(){var t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){var e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)},t}(),Ja="[eme]",$a=function(){function t(e){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=t.CDMCleanupPromise?[t.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=w.debug.bind(w,Ja),this.log=w.log.bind(w,Ja),this.warn=w.warn.bind(w,Ja),this.error=w.error.bind(w,Ja),this.hls=e,this.config=e.config,this.registerListeners()}var e=t.prototype;return e.destroy=function(){this.unregisterListeners(),this.onMediaDetached();var t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.getLicenseServerUrl=function(t){var e=this.config,r=e.drmSystems,i=e.widevineLicenseUrl,n=r[t];if(n)return n.licenseUrl;if(t===q.WIDEVINE&&i)return i;throw new Error('no license server URL configured for key-system "'+t+'"')},e.getServerCertificateUrl=function(t){var e=this.config.drmSystems[t];if(e)return e.serverCertificateUrl;this.log('No Server Certificate in config.drmSystems["'+t+'"]')},e.attemptKeySystemAccess=function(t){var e=this,r=this.hls.levels,i=function(t,e,r){return!!t&&r.indexOf(t)===e},n=r.map((function(t){return t.audioCodec})).filter(i),a=r.map((function(t){return t.videoCodec})).filter(i);return n.length+a.length===0&&a.push("avc1.42e01e"),new Promise((function(r,i){!function t(s){var o=s.shift();e.getMediaKeysPromise(o,n,a).then((function(t){return r({keySystem:o,mediaKeys:t})})).catch((function(e){s.length?t(s):i(e instanceof is?e:new is({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))}(t)}))},e.requestMediaKeySystemAccess=function(t,e){var r=this.config.requestMediaKeySystemAccessFunc;if("function"!=typeof r){var i="Configured requestMediaKeySystemAccess is not a function "+r;return null===it&&"http:"===self.location.protocol&&(i="navigator.requestMediaKeySystemAccess is not available over insecure protocol "+location.protocol),Promise.reject(new Error(i))}return r(t,e)},e.getMediaKeysPromise=function(t,e,r){var i=this,n=function(t,e,r,i){var n;switch(t){case q.FAIRPLAY:n=["cenc","sinf"];break;case q.WIDEVINE:case q.PLAYREADY:n=["cenc"];break;case q.CLEARKEY:n=["cenc","keyids"];break;default:throw new Error("Unknown key-system: "+t)}return function(t,e,r,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((function(t){return{contentType:'audio/mp4; codecs="'+t+'"',robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}})),videoCapabilities:r.map((function(t){return{contentType:'video/mp4; codecs="'+t+'"',robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null}}))}]}(n,e,r,i)}(t,e,r,this.config.drmSystemOptions),a=this.keySystemAccessPromises[t],s=null==a?void 0:a.keySystemAccess;if(!s){this.log('Requesting encrypted media "'+t+'" key-system access with config: '+JSON.stringify(n)),s=this.requestMediaKeySystemAccess(t,n);var o=this.keySystemAccessPromises[t]={keySystemAccess:s};return s.catch((function(e){i.log('Failed to obtain access to key-system "'+t+'": '+e)})),s.then((function(e){i.log('Access for key-system "'+e.keySystem+'" obtained');var r=i.fetchServerCertificate(t);return i.log('Create media-keys for "'+t+'"'),o.mediaKeys=e.createMediaKeys().then((function(e){return i.log('Media-keys created for "'+t+'"'),r.then((function(r){return r?i.setMediaKeysServerCertificate(e,t,r):e}))})),o.mediaKeys.catch((function(e){i.error('Failed to create media-keys for "'+t+'"}: '+e)})),o.mediaKeys}))}return s.then((function(){return a.mediaKeys}))},e.createMediaKeySessionContext=function(t){var e=t.decryptdata,r=t.keySystem,i=t.mediaKeys;this.log('Creating key-system session "'+r+'" keyId: '+Lt(e.keyId||[]));var n=i.createSession(),a={decryptdata:e,keySystem:r,mediaKeys:i,mediaKeysSession:n,keyStatus:"status-pending"};return this.mediaKeySessions.push(a),a},e.renewKeySession=function(t){var e=t.decryptdata;if(e.pssh){var r=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e);this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(r,"cenc",e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)},e.getKeyIdString=function(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return Lt(t.keyId)},e.updateKeySession=function(t,e){var r,i=t.mediaKeysSession;return this.log('Updating key-session "'+i.sessionId+'" for keyID '+Lt((null==(r=t.decryptdata)?void 0:r.keyId)||[])+"\n } (data length: "+(e?e.byteLength:e)+")"),i.update(e)},e.selectKeySystemFormat=function(t){var e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log("Selecting key-system from fragment (sn: "+t.sn+" "+t.type+": "+t.level+") key formats "+e.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise},e.getKeyFormatPromise=function(t){var e=this;return new Promise((function(r,i){var n=et(e.config),a=t.map($).filter((function(t){return!!t&&-1!==n.indexOf(t)}));return e.getKeySystemSelectionPromise(a).then((function(t){var e=t.keySystem,n=tt(e);n?r(n):i(new Error('Unable to find format for key-system "'+e+'"'))})).catch(i)}))},e.loadKey=function(t){var e=this,r=t.keyInfo.decryptdata,i=this.getKeyIdString(r),n="(keyId: "+i+' format: "'+r.keyFormat+'" method: '+r.method+" uri: "+r.uri+")";this.log("Starting session for key "+n);var a=this.keyIdToKeySessionPromise[i];return a||(a=this.keyIdToKeySessionPromise[i]=this.getKeySystemForKeyPromise(r).then((function(i){var a=i.keySystem,s=i.mediaKeys;return e.throwIfDestroyed(),e.log("Handle encrypted media sn: "+t.frag.sn+" "+t.frag.type+": "+t.frag.level+" using key "+n),e.attemptSetMediaKeys(a,s).then((function(){e.throwIfDestroyed();var t=e.createMediaKeySessionContext({keySystem:a,mediaKeys:s,decryptdata:r});return e.generateRequestWithPreferredKeySession(t,"cenc",r.pssh,"playlist-key")}))}))).catch((function(t){return e.handleError(t)})),a},e.throwIfDestroyed=function(t){if(!this.hls)throw new Error("invalid state")},e.handleError=function(t){this.hls&&(this.error(t.message),t instanceof is?this.hls.trigger(S.ERROR,t.data):this.hls.trigger(S.ERROR,{type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))},e.getKeySystemForKeyPromise=function(t){var e=this.getKeyIdString(t),r=this.keyIdToKeySessionPromise[e];if(!r){var i=$(t.keyFormat),n=i?[i]:et(this.config);return this.attemptKeySystemAccess(n)}return r},e.getKeySystemSelectionPromise=function(t){if(t.length||(t=et(this.config)),0===t.length)throw new is({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},"Missing key-system license configuration options "+JSON.stringify({drmSystems:this.config.drmSystems}));return this.attemptKeySystemAccess(t)},e._onMediaEncrypted=function(t){var e=this,r=t.initDataType,i=t.initData;if(this.debug('"'+t.type+'" event: init data type: "'+r+'"'),null!==i){var n,a;if("sinf"===r&&this.config.drmSystems[q.FAIRPLAY]){var s=bt(new Uint8Array(i));try{var o=V(JSON.parse(s).sinf),l=Bt(new Uint8Array(o));if(!l)return;n=l.subarray(8,24),a=q.FAIRPLAY}catch(t){return void this.warn('Failed to parse sinf "encrypted" event message initData')}}else{var u=function(t){if(!(t instanceof ArrayBuffer)||t.byteLength<32)return null;var e={version:0,systemId:"",kids:null,data:null},r=new DataView(t),i=r.getUint32(0);if(t.byteLength!==i&&i>44)return null;if(1886614376!==r.getUint32(4))return null;if(e.version=r.getUint32(8)>>>24,e.version>1)return null;e.systemId=Lt(new Uint8Array(t,12,16));var n=r.getUint32(28);if(0===e.version){if(i-320)for(var a,s=0,o=n.length;s in key message");return W(atob(f))},e.setupLicenseXHR=function(t,e,r,i){var n=this,a=this.config.licenseXhrSetup;return a?Promise.resolve().then((function(){if(!r.decryptdata)throw new Error("Key removed");return a.call(n.hls,t,e,r,i)})).catch((function(s){if(!r.decryptdata)throw s;return t.open("POST",e,!0),a.call(n.hls,t,e,r,i)})).then((function(r){return t.readyState||t.open("POST",e,!0),{xhr:t,licenseChallenge:r||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))},e.requestLicense=function(t,e){var r=this,i=this.config.keyLoadPolicy.default;return new Promise((function(n,a){var s=r.getLicenseServerUrl(t.keySystem);r.log("Sending license request to URL: "+s);var o=new XMLHttpRequest;o.responseType="arraybuffer",o.onreadystatechange=function(){if(!r.hls||!t.mediaKeysSession)return a(new Error("invalid state"));if(4===o.readyState)if(200===o.status){r._requestLicenseFailureCount=0;var l=o.response;r.log("License received "+(l instanceof ArrayBuffer?l.byteLength:l));var u=r.config.licenseResponseCallback;if(u)try{l=u.call(r.hls,o,s,t)}catch(t){r.error(t)}n(l)}else{var h=i.errorRetry,d=h?h.maxNumRetry:0;if(r._requestLicenseFailureCount++,r._requestLicenseFailureCount>d||o.status>=400&&o.status<500)a(new is({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:o,response:{url:s,data:void 0,code:o.status,text:o.statusText}},"License Request XHR failed ("+s+"). Status: "+o.status+" ("+o.statusText+")"));else{var c=d-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+c+" attempts left"),r.requestLicense(t,e).then(n,a)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=o,r.setupLicenseXHR(o,s,t,e).then((function(e){var i=e.xhr,n=e.licenseChallenge;t.keySystem==q.PLAYREADY&&(n=r.unpackPlayReadyKeyMessage(i,n)),i.send(n)}))}))},e.onMediaAttached=function(t,e){if(this.config.emeEnabled){var r=e.media;this.media=r,r.addEventListener("encrypted",this.onMediaEncrypted),r.addEventListener("waitingforkey",this.onWaitingForKey)}},e.onMediaDetached=function(){var e=this,r=this.media,i=this.mediaKeySessions;r&&(r.removeEventListener("encrypted",this.onMediaEncrypted),r.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},Xt.clearKeyUriToKeyIdMap();var n=i.length;t.CDMCleanupPromise=Promise.all(i.map((function(t){return e.removeSession(t)})).concat(null==r?void 0:r.setMediaKeys(null).catch((function(t){e.log("Could not clear media keys: "+t)})))).then((function(){n&&(e.log("finished closing key sessions and clearing media keys"),i.length=0)})).catch((function(t){e.log("Could not close sessions and clear media keys: "+t)}))},e.onManifestLoading=function(){this.keyFormatPromise=null},e.onManifestLoaded=function(t,e){var r=e.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(t,e){return-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},e.removeSession=function(t){var e=this,r=t.mediaKeysSession,i=t.licenseXhr;if(r){this.log("Remove licenses and keys and close session "+r.sessionId),t._onmessage&&(r.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(r.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;var n=this.mediaKeySessions.indexOf(t);return n>-1&&this.mediaKeySessions.splice(n,1),r.remove().catch((function(t){e.log("Could not remove session: "+t)})).then((function(){return r.close()})).catch((function(t){e.log("Could not close session: "+t)}))}},t}();$a.CDMCleanupPromise=void 0;var Za,ts,es,rs,is=function(t){function e(e,r){var i;return(i=t.call(this,r)||this).data=void 0,e.error||(e.error=new Error(r)),i.data=e,e.err=e.error,i}return l(e,t),e}(c(Error));!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(Za||(Za={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}(ts||(ts={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}(es||(es={}));var ns=((rs={})[es.OBJECT]=["br","d","ot","tb"],rs[es.REQUEST]=["bl","dl","mtp","nor","nrr","su"],rs[es.SESSION]=["cid","pr","sf","sid","st","v"],rs[es.STATUS]=["bs","rtp"],rs),as=function t(e,r){this.value=void 0,this.params=void 0,Array.isArray(e)&&(e=e.map((function(e){return e instanceof t?e:new t(e)}))),this.value=e,this.params=r},ss=function(t){this.description=void 0,this.description=t},os="Dict";function ls(t,e,r,i){return new Error("failed to "+t+' "'+(n=e,(Array.isArray(n)?JSON.stringify(n):n instanceof Map?"Map{}":n instanceof Set?"Set{}":"object"==typeof n?JSON.stringify(n):String(n))+'" as ')+r,{cause:i});var n}var us="Bare Item",hs="Boolean",ds="Byte Sequence",cs="Decimal",fs="Integer",gs=/[\x00-\x1f\x7f]+/,vs="Token",ms="Key";function ps(t,e,r){return ls("serialize",t,e,r)}function ys(t){if(!1===ArrayBuffer.isView(t))throw ps(t,ds);return":"+(e=t,btoa(String.fromCharCode.apply(String,e))+":");var e}function Es(t){if(function(t){return t<-999999999999999||99999999999999912)throw ps(t,cs);var r=e.toString();return r.includes(".")?r:r+".0"}var Ls="String";function As(t){var e,r=(e=t).description||e.toString().slice(7,-1);if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(r))throw ps(r,vs);return r}function Rs(t){switch(typeof t){case"number":if(!y(t))throw ps(t,us);return Number.isInteger(t)?Es(t):Ss(t);case"string":return function(t){if(gs.test(t))throw ps(t,Ls);return'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}(t);case"symbol":return As(t);case"boolean":return function(t){if("boolean"!=typeof t)throw ps(t,hs);return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return"@"+Es(t.getTime()/1e3)}(t);if(t instanceof Uint8Array)return ys(t);if(t instanceof ss)return As(t);default:throw ps(t,us)}}function ks(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw ps(t,ms);return t}function bs(t){return null==t?"":Object.entries(t).map((function(t){var e=t[0],r=t[1];return!0===r?";"+ks(e):";"+ks(e)+"="+Rs(r)})).join("")}function Ds(t){return t instanceof as?""+Rs(t.value)+bs(t.params):Rs(t)}function Is(t,e){var r;if(void 0===e&&(e={whitespace:!0}),"object"!=typeof t)throw ps(t,os);var i=t instanceof Map?t.entries():Object.entries(t),n=null!=(r=e)&&r.whitespace?" ":"";return Array.from(i).map((function(t){var e=t[0],r=t[1];r instanceof as==0&&(r=new as(r));var i,n=ks(e);return!0===r.value?n+=bs(r.params):(n+="=",Array.isArray(r.value)?n+="("+(i=r).value.map(Ds).join(" ")+")"+bs(i.params):n+=Ds(r)),n})).join(","+n)}var ws=function(t){return"ot"===t||"sf"===t||"st"===t},Cs=function(t){return"number"==typeof t?y(t):null!=t&&""!==t&&!1!==t},_s=function(t){return Math.round(t)},xs=function(t){return 100*_s(t/100)},Ps={br:_s,d:_s,bl:xs,dl:xs,mtp:xs,nor:function(t,e){return null!=e&&e.baseUrl&&(t=function(t,e){var r=new URL(t),i=new URL(e);if(r.origin!==i.origin)return t;for(var n=r.pathname.split("/").slice(1),a=i.pathname.split("/").slice(1,-1);n[0]===a[0];)n.shift(),a.shift();for(;a.length;)a.shift(),n.unshift("..");return n.join("/")}(t,e.baseUrl)),encodeURIComponent(t)},rtp:xs,tb:_s};function Fs(t,e){return void 0===e&&(e={}),t?function(t,e){return Is(t,e)}(function(t,e){var r={};if(null==t||"object"!=typeof t)return r;var i=Object.keys(t).sort(),n=o({},Ps,null==e?void 0:e.formatters),a=null==e?void 0:e.filter;return i.forEach((function(i){if(null==a||!a(i)){var s=t[i],o=n[i];o&&(s=o(s,e)),"v"===i&&1===s||"pr"==i&&1===s||Cs(s)&&(ws(i)&&"string"==typeof s&&(s=new ss(s)),r[i]=s)}})),r}(t,e),o({whitespace:!1},e)):""}function Ms(t,e,r){return o(t,function(t,e){var r;if(void 0===e&&(e={}),!t)return{};var i=Object.entries(t),n=Object.entries(ns).concat(Object.entries((null==(r=e)?void 0:r.customHeaderMap)||{})),a=i.reduce((function(t,e){var r,i=e[0],a=e[1],s=(null==(r=n.find((function(t){return t[1].includes(i)})))?void 0:r[0])||es.REQUEST;return null!=t[s]||(t[s]={}),t[s][i]=a,t}),{});return Object.entries(a).reduce((function(t,r){var i=r[0],n=r[1];return t[i]=Fs(n,e),t}),{})}(e,r))}var Os="CMCD",Ns=/CMCD=[^&#]+/;function Us(t,e,r){var i=function(t,e){if(void 0===e&&(e={}),!t)return"";var r=Fs(t,e);return Os+"="+encodeURIComponent(r)}(e,r);if(!i)return t;if(Ns.test(t))return t.replace(Ns,i);var n=t.includes("?")?"&":"?";return""+t+n+i}var Bs=function(){function t(t){var e=this;this.hls=void 0,this.config=void 0,this.media=void 0,this.sid=void 0,this.cid=void 0,this.useHeaders=!1,this.includeKeys=void 0,this.initialized=!1,this.starved=!1,this.buffering=!0,this.audioBuffer=void 0,this.videoBuffer=void 0,this.onWaiting=function(){e.initialized&&(e.starved=!0),e.buffering=!0},this.onPlaying=function(){e.initialized||(e.initialized=!0),e.buffering=!1},this.applyPlaylistData=function(t){try{e.apply(t,{ot:Za.MANIFEST,su:!e.initialized})}catch(t){w.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=function(t){try{var r=t.frag,i=e.hls.levels[r.level],n=e.getObjectType(r),a={d:1e3*r.duration,ot:n};n!==Za.VIDEO&&n!==Za.AUDIO&&n!=Za.MUXED||(a.br=i.bitrate/1e3,a.tb=e.getTopBandwidth(n)/1e3,a.bl=e.getBufferLength(n)),e.apply(t,a)}catch(t){w.warn("Could not generate segment CMCD data.",t)}},this.hls=t;var r=this.config=t.config,i=r.cmcd;null!=i&&(r.pLoader=this.createPlaylistLoader(),r.fLoader=this.createFragmentLoader(),this.sid=i.sessionId||function(){try{return crypto.randomUUID()}catch(i){try{var t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){var e=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==t?e:3&e|8).toString(16)}))}}}(),this.cid=i.contentId,this.useHeaders=!0===i.useHeaders,this.includeKeys=i.includeKeys,this.registerListeners())}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHED,this.onMediaDetached,this),t.on(S.BUFFER_CREATED,this.onBufferCreated,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHED,this.onMediaDetached,this),t.off(S.BUFFER_CREATED,this.onBufferCreated,this)},e.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null},e.onMediaAttached=function(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},e.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},e.onBufferCreated=function(t,e){var r,i;this.audioBuffer=null==(r=e.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer},e.createData=function(){var t;return{v:1,sf:ts.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},e.apply=function(t,e){void 0===e&&(e={}),o(e,this.createData());var r=e.ot===Za.INIT||e.ot===Za.VIDEO||e.ot===Za.MUXED;this.starved&&r&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);var i=this.includeKeys;i&&(e=Object.keys(e).reduce((function(t,r){return i.includes(r)&&(t[r]=e[r]),t}),{})),this.useHeaders?(t.headers||(t.headers={}),Ms(t.headers,e)):t.url=Us(t.url,e)},e.getObjectType=function(t){var e=t.type;return"subtitle"===e?Za.TIMED_TEXT:"initSegment"===t.sn?Za.INIT:"audio"===e?Za.AUDIO:"main"===e?this.hls.audioTracks.length?Za.VIDEO:Za.MUXED:void 0},e.getTopBandwidth=function(t){var e,r=0,i=this.hls;if(t===Za.AUDIO)e=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;e=i.levels.slice(0,a)}for(var s,o=g(e);!(s=o()).done;){var l=s.value;l.bitrate>r&&(r=l.bitrate)}return r>0?r:NaN},e.getBufferLength=function(t){var e=this.hls.media,r=t===Za.AUDIO?this.audioBuffer:this.videoBuffer;return r&&e?1e3*Jr.bufferInfo(r,e.currentTime,this.config.maxBufferHole).len:NaN},e.createPlaylistLoader=function(){var t=this.config.pLoader,e=this.applyPlaylistData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},e.createFragmentLoader=function(){var t=this.config.fLoader,e=this.applyFragmentData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},t}(),Gs=function(){function t(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=w.log.bind(w,"[content-steering]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.ERROR,this.onError,this))},e.startLoad=function(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){var t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}},e.stopLoad=function(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()},e.clearTimeout=function(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)},e.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null},e.removeLevel=function(t){var e=this.levels;e&&(this.levels=e.filter((function(e){return e!==t})))},e.onManifestLoading=function(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null},e.onManifestLoaded=function(t,e){var r=e.contentSteering;null!==r&&(this.pathwayId=r.pathwayId,this.uri=r.uri,this.started&&this.startLoad())},e.onManifestParsed=function(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks},e.onError=function(t,e){var r=e.errorAction;if((null==r?void 0:r.action)===Lr&&r.flags===br){var i=this.levels,n=this.pathwayPriority,a=this.pathwayId;if(e.context){var s=e.context,o=s.groupId,l=s.pathwayId,u=s.type;o&&i?a=this.getPathwayForGroupId(o,u,a):l&&(a=l)}a in this.penalizedPathways||(this.penalizedPathways[a]=performance.now()),!n&&i&&(n=i.reduce((function(t,e){return-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t}),[])),n&&n.length>1&&(this.updatePathwayPriority(n),r.resolved=this.pathwayId!==a),r.resolved||w.warn("Could not resolve "+e.details+' ("'+e.error.message+'") with content-steering for Pathway: '+a+" levels: "+(i?i.length:i)+" priorities: "+JSON.stringify(n)+" penalized: "+JSON.stringify(this.penalizedPathways))}},e.filterParsedLevels=function(t){this.levels=t;var e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){var r=t[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),e=this.getLevelsForPathway(r),this.pathwayId=r}return e.length!==t.length?(this.log("Found "+e.length+"/"+t.length+' levels in Pathway "'+this.pathwayId+'"'),e):t},e.getLevelsForPathway=function(t){return null===this.levels?[]:this.levels.filter((function(e){return t===e.pathwayId}))},e.updatePathwayPriority=function(t){var e;this.pathwayPriority=t;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(t){i-r[t]>3e5&&delete r[t]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,dr(e),this.hls.trigger(S.LEVELS_UPDATED,{levels:e});var l=this.hls.levels[s];o&&l&&this.levels&&(l.attrs["STABLE-VARIANT-ID"]!==o.attrs["STABLE-VARIANT-ID"]&&l.bitrate!==o.bitrate&&this.log("Unstable Pathways change from bitrate "+o.bitrate+" to "+l.bitrate),this.hls.nextLoadLevel=s);break}}}},e.getPathwayForGroupId=function(t,e,r){for(var i=this.getLevelsForPathway(r).concat(this.levels||[]),n=0;n=2&&(0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start),n.timeout!==n.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),n.timeout=n.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),n.loadPolicy.maxLoadTimeMs-(r.loading.first-r.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;var a=e.status,s="text"!==e.responseType;if(a>=200&&a<300&&(s&&e.response||null!==e.responseText)){r.loading.end=Math.max(self.performance.now(),r.loading.first);var o=s?e.response:e.responseText,l="arraybuffer"===e.responseType?o.byteLength:o.length;if(r.loaded=r.total=l,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first),!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,o,e),!this.callbacks)return;var h={url:e.responseURL,data:o,code:a};this.callbacks.onSuccess(h,r,t,e)}else{var d=n.loadPolicy.errorRetry;mr(d,r.retry,!1,{url:t.url,data:void 0,code:a})?this.retry(d):(w.error(a+" while loading "+t.url),this.callbacks.onError({code:a,text:e.statusText},t,e,r))}}}},e.loadtimeout=function(){var t,e=null==(t=this.config)?void 0:t.loadPolicy.timeoutRetry;if(mr(e,this.stats.retry,!0))this.retry(e);else{var r;w.warn("timeout while loading "+(null==(r=this.context)?void 0:r.url));var i=this.callbacks;i&&(this.abortInternal(),i.onTimeout(this.stats,this.context,this.loader))}},e.retry=function(t){var e=this.context,r=this.stats;this.retryDelay=gr(t,r.retry),r.retry++,w.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+(null==e?void 0:e.url)+", retrying "+r.retry+"/"+t.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},e.loadprogress=function(t){var e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)},e.getCacheAge=function(){var t=null;if(this.loader&&Vs.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.loader&&new RegExp("^"+t+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null},t}(),Ws=/(\d+)-(\d+)\/(\d+)/,js=function(){function t(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||qs,this.controller=new self.AbortController,this.stats=new M}var e=t.prototype;return e.destroy=function(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null},e.abortInternal=function(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},e.load=function(t,e,r){var i=this,n=this.stats;if(n.loading.start)throw new Error("Loader can only be used once.");n.loading.start=self.performance.now();var a=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(o({},t.headers))};return t.rangeEnd&&r.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)),r}(t,this.controller.signal),s=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length",h=e.loadPolicy,d=h.maxTimeToFirstByteMs,c=h.maxLoadTimeMs;this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,a),self.clearTimeout(this.requestTimeout),e.timeout=d&&y(d)?d:c,this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(a){i.response=i.loader=a;var o=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),e.timeout=c,i.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),c-(o-n.loading.start)),!a.ok){var u=a.status,h=a.statusText;throw new zs(h||"fetch, bad network response",u,a)}return n.loading.first=o,n.total=function(t){var e=t.get("Content-Range");if(e){var r=function(t){var e=Ws.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(y(r))return r}var i=t.get("Content-Length");if(i)return parseInt(i)}(a.headers)||n.total,s&&y(e.highWaterMark)?i.loadProgressively(a,n,t,e.highWaterMark,s):l?a.arrayBuffer():"json"===t.responseType?a.json():a.text()})).then((function(a){var o=i.response;if(!o)throw new Error("loader destroyed");self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first);var l=a[u];l&&(n.loaded=n.total=l);var h={url:o.url,data:a,code:o.status};s&&!y(e.highWaterMark)&&s(n,t,a,o),r.onSuccess(h,n,t,o)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e&&e.code||0,s=e?e.message:null;r.onError({code:a,text:s},t,e?e.details:null,n)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.response?this.response.headers.get(t):null},e.loadProgressively=function(t,e,r,i,n){void 0===i&&(i=0);var a=new Di,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(e,r,a.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&n(e,r,a.flush(),t)):n(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function qs(t,e){return new self.Request(t.url,e)}var Xs,zs=function(t){function e(e,r,i){var n;return(n=t.call(this,e)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return l(e,t),e}(c(Error)),Qs=/\s/,Js=i(i({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:Ys,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:Kr,bufferController:$n,capLevelController:za,errorController:Ir,fpsController:Qa,stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:it,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:{newCue:function(t,e,r,i){for(var n,a,s,o,l,u=[],h=self.VTTCue||self.TextTrackCue,d=0;d=16?o--:o++;var g=Ia(l.trim()),v=Pa(e,r,g);null!=t&&null!=(c=t.cues)&&c.getCueById(v)||((a=new h(e,r,g)).id=v,a.line=d+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return t&&u.length&&(u.sort((function(t,e){return"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line})),u.forEach((function(e){return Oe(t,e)}))),u}},enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:qn,subtitleTrackController:zn,timelineController:ja,audioStreamController:Wn,audioTrackController:jn,emeController:$a,cmcdController:Bs,contentSteeringController:Gs});function $s(t){return t&&"object"==typeof t?Array.isArray(t)?t.map($s):Object.keys(t).reduce((function(e,r){return e[r]=$s(t[r]),e}),{}):t}function Zs(t){var e=t.loader;e!==js&&e!==Ys?(w.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1}()&&(t.loader=js,t.progressive=!0,t.enableSoftwareAES=!0,w.log("[config]: Progressive streaming enabled, using FetchLoader"))}var to=function(t){function e(e,r){var i;return(i=t.call(this,e,"[level-controller]")||this)._levels=[],i._firstLevel=-1,i._maxAutoLevel=-1,i._startLevel=void 0,i.currentLevel=null,i.currentLevelIndex=-1,i.manualLevelIndex=-1,i.steering=void 0,i.onParsedComplete=void 0,i.steering=r,i._registerListeners(),i}l(e,t);var r=e.prototype;return r._registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.ERROR,this.onError,this)},r._unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),t.prototype.destroy.call(this)},r.stopLoad=function(){this._levels.forEach((function(t){t.loadError=0,t.fragmentError=0})),t.prototype.stopLoad.call(this)},r.resetLevels=function(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1},r.onManifestLoading=function(t,e){this.resetLevels()},r.onManifestLoaded=function(t,e){var r=this.hls.config.preferManagedMediaSource,i=[],n={},a={},s=!1,o=!1,l=!1;e.levels.forEach((function(t){var e,u,h=t.attrs,d=t.audioCodec,c=t.videoCodec;-1!==(null==(e=d)?void 0:e.indexOf("mp4a.40.34"))&&(Xs||(Xs=/chrome|firefox/i.test(navigator.userAgent)),Xs&&(t.audioCodec=d=void 0)),d&&(t.audioCodec=d=he(d,r)),0===(null==(u=c)?void 0:u.indexOf("avc1"))&&(c=t.videoCodec=function(t){var e=t.split(".");if(e.length>2){var r=e.shift()+".";return(r+=parseInt(e.shift()).toString(16))+("000"+parseInt(e.shift()).toString(16)).slice(-4)}return t}(c));var f=t.width,g=t.height,v=t.unknownCodecs;if(s||(s=!(!f||!g)),o||(o=!!c),l||(l=!!d),!(null!=v&&v.length||d&&!ie(d,"audio",r)||c&&!ie(c,"video",r))){var m=h.CODECS,p=h["FRAME-RATE"],y=h["HDCP-LEVEL"],E=h["PATHWAY-ID"],T=h.RESOLUTION,S=h["VIDEO-RANGE"],L=(E||".")+"-"+t.bitrate+"-"+T+"-"+p+"-"+m+"-"+S+"-"+y;if(n[L])if(n[L].uri===t.url||t.attrs["PATHWAY-ID"])n[L].addGroupId("audio",h.AUDIO),n[L].addGroupId("text",h.SUBTITLES);else{var A=a[L]+=1;t.attrs["PATHWAY-ID"]=new Array(A+1).join(".");var R=new rr(t);n[L]=R,i.push(R)}else{var k=new rr(t);n[L]=k,a[L]=1,i.push(k)}}})),this.filterAndSortMediaOptions(i,e,s,o,l)},r.filterAndSortMediaOptions=function(t,e,r,i,n){var a=this,s=[],o=[],l=t;if((r||i)&&n&&(l=l.filter((function(t){var e,r=t.videoCodec,i=t.videoRange,n=t.width,a=t.height;return(!!r||!(!n||!a))&&!!(e=i)&&Qe.indexOf(e)>-1}))),0!==l.length){if(e.audioTracks){var u=this.hls.config.preferManagedMediaSource;eo(s=e.audioTracks.filter((function(t){return!t.audioCodec||ie(t.audioCodec,"audio",u)})))}e.subtitles&&eo(o=e.subtitles);var h=l.slice(0);l.sort((function(t,e){if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(r&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return Qe.indexOf(t.videoRange)-Qe.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){var i=se(t.videoCodec),n=se(e.videoCodec);if(i!==n)return n-i}if(t.uri===e.uri&&t.codecSet!==e.codecSet){var a=oe(t.codecSet),s=oe(e.codecSet);if(a!==s)return s-a}return t.averageBitrate!==e.averageBitrate?t.averageBitrate-e.averageBitrate:0}));var d=h[0];if(this.steering&&(l=this.steering.filterParsedLevels(l)).length!==h.length)for(var c=0;cm&&m===Js.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=p)}break}var y=n&&!i,E={levels:l,audioTracks:s,subtitleTracks:o,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:n,video:i,altAudio:!y&&s.some((function(t){return!!t.url}))};this.hls.trigger(S.MANIFEST_PARSED,E),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}else Promise.resolve().then((function(){if(a.hls){e.levels.length&&a.warn("One or more CODECS in variant not supported: "+JSON.stringify(e.levels[0].attrs));var t=new Error("no level with compatible codecs found in manifest");a.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}))},r.onError=function(t,e){!e.fatal&&e.context&&e.context.type===be&&e.context.level===this.level&&this.checkRetry(e)},r.onFragBuffered=function(t,e){var r=e.frag;if(void 0!==r&&r.type===we){var i=r.elementaryStreams;if(!Object.keys(i).some((function(t){return!!i[t]})))return;var n=this._levels[r.level];null!=n&&n.loadError&&(this.log("Resetting level error count of "+n.loadError+" on frag buffered"),n.loadError=0)}},r.onLevelLoaded=function(t,e){var r,i,n=e.level,a=e.details,s=this._levels[n];if(!s)return this.warn("Invalid level index "+n),void(null!=(i=e.deliveryDirectives)&&i.skip&&(a.deltaUpdateFailed=!0));n===this.currentLevelIndex?(0===s.fragmentError&&(s.loadError=0),this.playlistLoaded(n,e,s.details)):null!=(r=e.deliveryDirectives)&&r.skip&&(a.deltaUpdateFailed=!0)},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentLevelIndex,i=this.currentLevel;if(i&&this.shouldLoadPlaylist(i)){var n=i.uri;if(e)try{n=e.addDirectives(n)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}var a=i.attrs["PATHWAY-ID"];this.log("Loading level index "+r+(void 0!==(null==e?void 0:e.msn)?" at sn "+e.msn+" part "+e.part:"")+" with"+(a?" Pathway "+a:"")+" "+n),this.clearTimer(),this.hls.trigger(S.LEVEL_LOADING,{url:n,level:r,pathwayId:i.attrs["PATHWAY-ID"],id:0,deliveryDirectives:e||null})}},r.removeLevel=function(t){var e,r=this,i=this._levels.filter((function(e,i){return i!==t||(r.steering&&r.steering.removeLevel(e),e===r.currentLevel&&(r.currentLevel=null,r.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((function(t){return t.level=-1}))),!1)}));dr(i),this._levels=i,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(S.LEVELS_UPDATED,{levels:i})},r.onLevelsUpdated=function(t,e){var r=e.levels;this._levels=r},r.checkMaxAutoUpdated=function(){var t=this.hls,e=t.autoLevelCapping,r=t.maxAutoLevel,i=t.maxHdcpLevel;this._maxAutoLevel!==r&&(this._maxAutoLevel=r,this.hls.trigger(S.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:e,levels:this.levels,maxAutoLevel:r,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:i}))},s(e,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e=this._levels;if(0!==e.length){if(t<0||t>=e.length){var r=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:r,reason:r.message}),i)return;t=Math.min(t,e.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,o=e[t],l=o.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=o,n!==t||!o.details||!a||s!==l){this.log("Switching to level "+t+" ("+(o.height?o.height+"p ":"")+(o.videoRange?o.videoRange+" ":"")+(o.codecSet?o.codecSet+" ":"")+"@"+o.bitrate+")"+(l?" with Pathway "+l:"")+" from level "+n+(s?" with Pathway "+s:""));var u={level:t,attrs:o.attrs,details:o.details,bitrate:o.bitrate,averageBitrate:o.averageBitrate,maxBitrate:o.maxBitrate,realBitrate:o.realBitrate,width:o.width,height:o.height,codecSet:o.codecSet,audioCodec:o.audioCodec,videoCodec:o.videoCodec,audioGroups:o.audioGroups,subtitleGroups:o.subtitleGroups,loaded:o.loaded,loadError:o.loadError,fragmentError:o.fragmentError,name:o.name,id:o.id,uri:o.uri,url:o.url,urlId:0,audioGroupIds:o.audioGroupIds,textGroupIds:o.textGroupIds};this.hls.trigger(S.LEVEL_SWITCHING,u);var h=o.details;if(!h||h.live){var d=this.switchParams(o.uri,null==a?void 0:a.details,h);this.loadPlaylist(d)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(t){this._firstLevel=t}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel},set:function(t){this._startLevel=t}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}}]),e}(wr);function eo(t){var e={};t.forEach((function(t){var r=t.groupId||"";t.id=e[r]=e[r]||0,e[r]++}))}var ro=function(){function t(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}var e=t.prototype;return e.abort=function(t){for(var e in this.keyUriToKeyInfo){var r=this.keyUriToKeyInfo[e].loader;if(r){var i;if(t&&t!==(null==(i=r.context)?void 0:i.frag.type))return;r.abort()}}},e.detach=function(){for(var t in this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}},e.destroy=function(){for(var t in this.detach(),this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}},e.createKeyLoadError=function(t,e,r,i,n){return void 0===e&&(e=A.KEY_LOAD_ERROR),new li({type:L.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:n,error:r,networkDetails:i})},e.loadClear=function(t,e){var r=this;if(this.emeController&&this.config.emeEnabled)for(var i=t.sn,n=t.cc,a=function(){var t=e[s];if(n<=t.cc&&("initSegment"===i||"initSegment"===t.sn||i2,c=!h||e&&e.start<=a||h-a>2&&!this.fragmentTracker.getPartialFragment(a);if(d||c)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var f;if(!(u.len>0||h))return;var g=Math.max(h,u.start||0)-a,v=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,m=(null==v||null==(f=v.details)?void 0:f.live)?2*v.details.targetduration:2,p=this.fragmentTracker.getPartialFragment(a);if(g>0&&(g<=m||p))return void(i.paused||this._trySkipBufferHole(p))}var y=self.performance.now();if(null!==n){var E=y-n;if(s||!(E>=250)||(this._reportStall(u),this.media)){var T=Jr.bufferInfo(i,a,r.maxBufferHole);this._tryFixBufferStall(T,E)}}else this.stalled=y}else if(this.moved=!0,s||(this.nudgeRetry=0),null!==n){if(this.stallReported){var S=self.performance.now()-n;w.warn("playback not stuck anymore @"+a+", after "+Math.round(S)+"ms"),this.stallReported=!1}this.stalled=null}}},e._tryFixBufferStall=function(t,e){var r=this.config,i=this.fragmentTracker,n=this.media;if(null!==n){var a=n.currentTime,s=i.getPartialFragment(a);if(s&&(this._trySkipBufferHole(s)||!this.media))return;(t.len>r.maxBufferHole||t.nextStart&&t.nextStart-a1e3*r.highBufferWatchdogPeriod&&(w.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}},e._reportStall=function(t){var e=this.hls,r=this.media;if(!this.stallReported&&r){this.stallReported=!0;var i=new Error("Playback stalling at @"+r.currentTime+" due to low buffer ("+JSON.stringify(t)+")");w.warn(i.message),e.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}},e._trySkipBufferHole=function(t){var e=this.config,r=this.hls,i=this.media;if(null===i)return 0;var n=i.currentTime,a=Jr.bufferInfo(i,n,0),s=n0&&a.len<1&&i.readyState<3,u=s-n;if(u>0&&(o||l)){if(u>e.maxBufferHole){var h=this.fragmentTracker,d=!1;if(0===n){var c=h.getAppendedFrag(0,we);c&&s1?(i=0,this.bitrateTest=!0):i=r.firstAutoLevel),r.nextLoadLevel=i,this.level=r.loadLevel,this.loadedmetadata=!1}e>0&&-1===t&&(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e),this.state=vi,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=gi},r.stopLoad=function(){this._forceStartLoad=!1,t.prototype.stopLoad.call(this)},r.doTick=function(){switch(this.state){case ki:var t=this.levels,e=this.level,r=null==t?void 0:t[e],i=null==r?void 0:r.details;if(i&&(!i.live||this.levelLastLoaded===r)){if(this.waitForCdnTuneIn(i))break;this.state=vi;break}if(this.hls.nextLoadLevel!==this.level){this.state=vi;break}break;case yi:var n,a=self.performance.now(),s=this.retryDate;if(!s||a>=s||null!=(n=this.media)&&n.seeking){var o=this.levels,l=this.level,u=null==o?void 0:o[l];this.resetStartWhenNotLoaded(u||null),this.state=vi}}this.state===vi&&this.doTickIdle(),this.onTickEnd()},r.onTickEnd=function(){t.prototype.onTickEnd.call(this),this.checkBuffer(),this.checkFragmentChanged()},r.doTickIdle=function(){var t=this.hls,e=this.levelLastLoaded,r=this.levels,i=this.media;if(null!==e&&(i||!this.startFragRequested&&t.config.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)){var n=t.nextLoadLevel;if(null!=r&&r[n]){var a=r[n],s=this.getMainFwdBufferInfo();if(null!==s){var o=this.getLevelDetails();if(o&&this._streamEnded(s,o)){var l={};return this.altAudio&&(l.type="video"),this.hls.trigger(S.BUFFER_EOS,l),void(this.state=Li)}t.loadLevel!==n&&-1===t.manualLevel&&this.log("Adapting to level "+n+" from level "+this.level),this.level=t.nextLoadLevel=n;var u=a.details;if(!u||this.state===ki||u.live&&this.levelLastLoaded!==a)return this.level=n,void(this.state=ki);var h=s.len,d=this.getMaxBufferLength(a.maxBitrate);if(!(h>=d)){this.backtrackFragment&&this.backtrackFragment.start>s.end&&(this.backtrackFragment=null);var c=this.backtrackFragment?this.backtrackFragment.start:s.end,f=this.getNextFragment(c,u);if(this.couldBacktrack&&!this.fragPrevious&&f&&"initSegment"!==f.sn&&this.fragmentTracker.getState(f)!==jr){var g,v=(null!=(g=this.backtrackFragment)?g:f).sn-u.startSN,m=u.fragments[v-1];m&&f.cc===m.cc&&(f=m,this.fragmentTracker.removeFragment(m))}else this.backtrackFragment&&s.len&&(this.backtrackFragment=null);if(f&&this.isLoopLoading(f,c)){if(!f.gap){var p=this.audioOnly&&!this.altAudio?O:N,y=(p===N?this.videoBuffer:this.mediaBuffer)||this.media;y&&this.afterBufferFlushed(y,p,we)}f=this.getNextFragmentLoopLoading(f,u,s,we,d)}f&&(!f.initSegment||f.initSegment.data||this.bitrateTest||(f=f.initSegment),this.loadFragment(f,a,c))}}}}},r.loadFragment=function(e,r,i){var n=this.fragmentTracker.getState(e);this.fragCurrent=e,n===Vr||n===Wr?"initSegment"===e.sn?this._loadInitSegment(e,r):this.bitrateTest?(this.log("Fragment "+e.sn+" of level "+e.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(e,r)):(this.startFragRequested=!0,t.prototype.loadFragment.call(this,e,r,i)):this.clearTrackerIfNeeded(e)},r.getBufferedFrag=function(t){return this.fragmentTracker.getBufferedFrag(t,we)},r.followingBufferedFrag=function(t){return t?this.getBufferedFrag(t.end+.5):null},r.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},r.nextLevelSwitch=function(){var t=this.levels,e=this.media;if(null!=e&&e.readyState){var r,i=this.getAppendedFrag(e.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);var n=this.getLevelDetails();if(null!=n&&n.live){var a=this.getMainFwdBufferInfo();if(!a||a.len<2*n.targetduration)return}if(!e.paused&&t){var s=t[this.hls.nextLoadLevel],o=this.fragLastKbps;r=o&&this.fragCurrent?this.fragCurrent.duration*s.maxBitrate/(1e3*o)+1:0}else r=0;var l=this.getBufferedFrag(e.currentTime+r);if(l){var u=this.followingBufferedFrag(l);if(u){this.abortCurrentFrag();var h=u.maxStartPTS?u.maxStartPTS:u.start,d=u.duration,c=Math.max(l.end,h+Math.min(Math.max(d-this.config.maxFragLookUpTolerance,d*(this.couldBacktrack?.5:.125)),d*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(c,Number.POSITIVE_INFINITY)}}}},r.abortCurrentFrag=function(){var t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case mi:case pi:case yi:case Ti:case Si:this.state=vi}this.nextLoadPosition=this.getLoadPosition()},r.flushMainBuffer=function(e,r){t.prototype.flushMainBuffer.call(this,e,r,this.altAudio?"video":null)},r.onMediaAttached=function(e,r){t.prototype.onMediaAttached.call(this,e,r);var i=r.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),i.addEventListener("playing",this.onvplaying),i.addEventListener("seeked",this.onvseeked),this.gapController=new ao(this.config,i,this.fragmentTracker,this.hls)},r.onMediaDetaching=function(){var e=this.media;e&&this.onvplaying&&this.onvseeked&&(e.removeEventListener("playing",this.onvplaying),e.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),t.prototype.onMediaDetaching.call(this)},r.onMediaPlaying=function(){this.tick()},r.onMediaSeeked=function(){var t=this.media,e=t?t.currentTime:null;y(e)&&this.log("Media seeked to "+e.toFixed(3));var r=this.getMainFwdBufferInfo();null!==r&&0!==r.len?this.tick():this.warn('Main forward buffer length on "seeked" event '+(r?r.len:"empty")+")")},r.onManifestLoading=function(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(S.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=this.fragLastKbps=0,this.levels=this.fragPlaying=this.backtrackFragment=this.levelLastLoaded=null,this.altAudio=this.audioOnly=this.startFragRequested=!1},r.onManifestParsed=function(t,e){var r,i,n=!1,a=!1;e.levels.forEach((function(t){var e=t.audioCodec;e&&(n=n||-1!==e.indexOf("mp4a.40.2"),a=a||-1!==e.indexOf("mp4a.40.5"))})),this.audioCodecSwitch=n&&a&&!("function"==typeof(null==(i=io())||null==(r=i.prototype)?void 0:r.changeType)),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1},r.onLevelLoading=function(t,e){var r=this.levels;if(r&&this.state===vi){var i=r[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(i.details))&&(this.state=ki)}},r.onLevelLoaded=function(t,e){var r,i=this.levels,n=e.level,a=e.details,s=a.totalduration;if(i){this.log("Level "+n+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+", cc ["+a.startCC+", "+a.endCC+"] duration:"+s);var o=i[n],l=this.fragCurrent;!l||this.state!==pi&&this.state!==yi||l.level!==e.level&&l.loader&&this.abortCurrentFrag();var u=0;if(a.live||null!=(r=o.details)&&r.live){var h;if(this.checkLiveUpdate(a),a.deltaUpdateFailed)return;u=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details)}if(o.details=a,this.levelLastLoaded=o,this.hls.trigger(S.LEVEL_UPDATED,{details:a,level:n}),this.state===ki){if(this.waitForCdnTuneIn(a))return;this.state=vi}this.startFragRequested?a.live&&this.synchronizeToLiveEdge(a):this.setStartPosition(a,u),this.tick()}else this.warn("Levels were reset while loading level "+n)},r._handleFragmentLoadProgress=function(t){var e,r=t.frag,i=t.part,n=t.payload,a=this.levels;if(a){var s=a[r.level],o=s.details;if(!o)return this.warn("Dropping fragment "+r.sn+" of level "+r.level+" after level details were reset"),void this.fragmentTracker.removeFragment(r);var l=s.videoCodec,u=o.PTSKnown||!o.live,h=null==(e=r.initSegment)?void 0:e.data,d=this._getAudioCodec(s),c=this.transmuxer=this.transmuxer||new Kn(this.hls,we,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,v=new $r(r.level,r.sn,r.stats.chunkCount,n.byteLength,f,g),m=this.initPTS[r.cc];c.push(n,h,d,l,r,i,o.totalduration,u,v,m)}else this.warn("Levels were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},r.onAudioTrackSwitching=function(t,e){var r=this.altAudio;if(!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;var i=this.fragCurrent;i&&(this.log("Switching to main audio track, cancel main fragment load"),i.abortRequests(),this.fragmentTracker.removeFragment(i)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();var n=this.hls;r&&(n.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),n.trigger(S.AUDIO_TRACK_SWITCHED,e)}},r.onAudioTrackSwitched=function(t,e){var r=e.id,i=!!this.hls.audioTracks[r].url;if(i){var n=this.videoBuffer;n&&this.mediaBuffer!==n&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=n)}this.altAudio=i,this.tick()},r.onBufferCreated=function(t,e){var r,i,n=e.tracks,a=!1;for(var s in n){var o=n[s];if("main"===o.id){if(i=s,r=o,"video"===s){var l=n[s];l&&(this.videoBuffer=l.buffer)}}else a=!0}a&&r?(this.log("Alternate track found, use "+i+".buffered to schedule main fragment loading"),this.mediaBuffer=r.buffer):this.mediaBuffer=this.media},r.onFragBuffered=function(t,e){var r=e.frag,i=e.part;if(!r||r.type===we){if(this.fragContextChanged(r))return this.warn("Fragment "+r.sn+(i?" p: "+i.index:"")+" of level "+r.level+" finished buffering, but was aborted. state: "+this.state),void(this.state===Si&&(this.state=vi));var n=i?i.stats:r.stats;this.fragLastKbps=Math.round(8*n.total/(n.buffering.end-n.loading.first)),"initSegment"!==r.sn&&(this.fragPrevious=r),this.fragBufferedComplete(r,i)}},r.onError=function(t,e){var r;if(e.fatal)this.state=Ai;else switch(e.details){case A.FRAG_GAP:case A.FRAG_PARSING_ERROR:case A.FRAG_DECRYPT_ERROR:case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(we,e);break;case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:case A.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==ki||(null==(r=e.context)?void 0:r.type)!==be||(this.state=vi);break;case A.BUFFER_APPEND_ERROR:case A.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;if(e.details===A.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case A.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}},r.checkBuffer=function(){var t=this.media,e=this.gapController;if(t&&e&&t.readyState){if(this.loadedmetadata||!Jr.getBuffered(t).length){var r=this.state!==vi?this.fragCurrent:null;e.poll(this.lastCurrentTime,r)}this.lastCurrentTime=t.currentTime}},r.onFragLoadEmergencyAborted=function(){this.state=vi,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()},r.onBufferFlushed=function(t,e){var r=e.type;if(r!==O||this.audioOnly&&!this.altAudio){var i=(r===N?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(i,r,we),this.tick()}},r.onLevelsUpdated=function(t,e){this.level>-1&&this.fragCurrent&&(this.level=this.fragCurrent.level),this.levels=e.levels},r.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},r.seekToStartPos=function(){var t=this.media;if(t){var e=t.currentTime,r=this.startPosition;if(r>=0&&e0&&(nT.cc;if(!1!==n.independent){var R=h.startPTS,k=h.endPTS,b=h.startDTS,D=h.endDTS;if(l)l.elementaryStreams[h.type]={startPTS:R,endPTS:k,startDTS:b,endDTS:D};else if(h.firstKeyFrame&&h.independent&&1===a.id&&!A&&(this.couldBacktrack=!0),h.dropped&&h.independent){var I=this.getMainFwdBufferInfo(),w=(I?I.end:this.getLoadPosition())+this.config.maxBufferHole,C=h.firstKeyFramePTS?h.firstKeyFramePTS:R;if(!L&&w2&&(o.gap=!0);o.setElementaryStreamInfo(h.type,R,k,b,D),this.backtrackFragment&&(this.backtrackFragment=o),this.bufferFragmentData(h,o,l,a,L||A)}else{if(!L&&!A)return void this.backtrack(o);o.gap=!0}}if(v){var _=v.startPTS,x=v.endPTS,P=v.startDTS,F=v.endDTS;l&&(l.elementaryStreams[O]={startPTS:_,endPTS:x,startDTS:P,endDTS:F}),o.setElementaryStreamInfo(O,_,x,P,F),this.bufferFragmentData(v,o,l,a)}if(g&&null!=c&&null!=(e=c.samples)&&e.length){var M={id:r,frag:o,details:g,samples:c.samples};i.trigger(S.FRAG_PARSING_METADATA,M)}if(g&&d){var N={id:r,frag:o,details:g,samples:d.samples};i.trigger(S.FRAG_PARSING_USERDATA,N)}}}else this.resetWhenMissingContext(a)},r._bufferInitSegment=function(t,e,r,i){var n=this;if(this.state===Ti){this.audioOnly=!!e.audio&&!e.video,this.altAudio&&!this.audioOnly&&delete e.audio;var a=e.audio,s=e.video,o=e.audiovideo;if(a){var l=t.audioCodec,u=navigator.userAgent.toLowerCase();this.audioCodecSwitch&&(l&&(l=-1!==l.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5"),1!==a.metadata.channelCount&&-1===u.indexOf("firefox")&&(l="mp4a.40.5")),l&&-1!==l.indexOf("mp4a.40.5")&&-1!==u.indexOf("android")&&"audio/mpeg"!==a.container&&(l="mp4a.40.2",this.log("Android: force audio codec to "+l)),t.audioCodec&&t.audioCodec!==l&&this.log('Swapping manifest audio codec "'+t.audioCodec+'" for "'+l+'"'),a.levelCodec=l,a.id="main",this.log("Init audio buffer, container:"+a.container+", codecs[selected/level/parsed]=["+(l||"")+"/"+(t.audioCodec||"")+"/"+a.codec+"]")}s&&(s.levelCodec=t.videoCodec,s.id="main",this.log("Init video buffer, container:"+s.container+", codecs[level/parsed]=["+(t.videoCodec||"")+"/"+s.codec+"]")),o&&this.log("Init audiovideo buffer, container:"+o.container+", codecs[level/parsed]=["+t.codecs+"/"+o.codec+"]"),this.hls.trigger(S.BUFFER_CODECS,e),Object.keys(e).forEach((function(t){var a=e[t].initSegment;null!=a&&a.byteLength&&n.hls.trigger(S.BUFFER_APPENDING,{type:t,data:a,frag:r,part:null,chunkMeta:i,parent:r.type})})),this.tickImmediate()}},r.getMainFwdBufferInfo=function(){return this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:this.media,we)},r.backtrack=function(t){this.couldBacktrack=!0,this.backtrackFragment=t,this.resetTransmuxer(),this.flushBufferGap(t),this.fragmentTracker.removeFragment(t),this.fragPrevious=null,this.nextLoadPosition=t.start,this.state=vi},r.checkFragmentChanged=function(){var t=this.media,e=null;if(t&&t.readyState>1&&!1===t.seeking){var r=t.currentTime;if(Jr.isBuffered(t,r)?e=this.getAppendedFrag(r):Jr.isBuffered(t,r+.1)&&(e=this.getAppendedFrag(r+.1)),e){this.backtrackFragment=null;var i=this.fragPlaying,n=e.level;i&&e.sn===i.sn&&i.level===n||(this.fragPlaying=e,this.hls.trigger(S.FRAG_CHANGED,{frag:e}),i&&i.level===n||this.hls.trigger(S.LEVEL_SWITCHED,{level:n}))}}},s(e,[{key:"nextLevel",get:function(){var t=this.nextBufferedFrag;return t?t.level:-1}},{key:"currentFrag",get:function(){var t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}},{key:"currentProgramDateTime",get:function(){var t=this.media;if(t){var e=t.currentTime,r=this.currentFrag;if(r&&y(e)&&y(r.programDateTime)){var i=r.programDateTime+1e3*(e-r.start);return new Date(i)}}return null}},{key:"currentLevel",get:function(){var t=this.currentFrag;return t?t.level:-1}},{key:"nextBufferedFrag",get:function(){var t=this.currentFrag;return t?this.followingBufferedFrag(t):null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}]),e}(bi),oo=function(){function t(e){void 0===e&&(e={}),this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this.started=!1,this._emitter=new Nn,this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,this.triggeringException=void 0,I(e.debug||!1,"Hls instance");var r=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');var r=$s(t),n=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((function(t){var i=("level"===t?"playlist":t)+"LoadPolicy",a=void 0===e[i],s=[];n.forEach((function(n){var o=t+"Loading"+n,l=e[o];if(void 0!==l&&a){s.push(o);var u=r[i].default;switch(e[i]={default:u},n){case"TimeOut":u.maxLoadTimeMs=l,u.maxTimeToFirstByteMs=l;break;case"MaxRetry":u.errorRetry.maxNumRetry=l,u.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":u.errorRetry.retryDelayMs=l,u.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":u.errorRetry.maxRetryDelayMs=l,u.timeoutRetry.maxRetryDelayMs=l}}})),s.length&&w.warn('hls.js config: "'+s.join('", "')+'" setting(s) are deprecated, use "'+i+'": '+JSON.stringify(e[i]))})),i(i({},r),e)}(t.DefaultConfig,e);this.userConfig=e,r.progressive&&Zs(r);var n=r.abrController,a=r.bufferController,s=r.capLevelController,o=r.errorController,l=r.fpsController,u=new o(this),h=this.abrController=new n(this),d=this.bufferController=new a(this),c=this.capLevelController=new s(this),f=new l(this),g=new Fe(this),v=new qe(this),m=r.contentSteeringController,p=m?new m(this):null,y=this.levelController=new to(this,p),E=new qr(this),T=new ro(this.config),L=this.streamController=new so(this,E,T);c.setStreamController(L),f.setStreamController(L);var A=[g,y,L];p&&A.splice(1,0,p),this.networkControllers=A;var R=[h,d,c,f,v,E];this.audioTrackController=this.createController(r.audioTrackController,A);var k=r.audioStreamController;k&&A.push(new k(this,E,T)),this.subtitleTrackController=this.createController(r.subtitleTrackController,A);var b=r.subtitleStreamController;b&&A.push(new b(this,E,T)),this.createController(r.timelineController,R),T.emeController=this.emeController=this.createController(r.emeController,R),this.cmcdController=this.createController(r.cmcdController,R),this.latencyController=this.createController(Xe,R),this.coreComponents=R,A.push(u);var D=u.onErrorOut;"function"==typeof D&&this.on(S.ERROR,D,u)}t.isMSESupported=function(){return no()},t.isSupported=function(){return function(){if(!no())return!1;var t=ee();return"function"==typeof(null==t?void 0:t.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((function(e){return t.isTypeSupported(ae(e,"video"))}))||["mp4a.40.2","fLaC"].some((function(e){return t.isTypeSupported(ae(e,"audio"))})))}()},t.getMediaSource=function(){return ee()};var e=t.prototype;return e.createController=function(t,e){if(t){var r=new t(this);return e&&e.push(r),r}return null},e.on=function(t,e,r){void 0===r&&(r=this),this._emitter.on(t,e,r)},e.once=function(t,e,r){void 0===r&&(r=this),this._emitter.once(t,e,r)},e.removeAllListeners=function(t){this._emitter.removeAllListeners(t)},e.off=function(t,e,r,i){void 0===r&&(r=this),this._emitter.off(t,e,r,i)},e.listeners=function(t){return this._emitter.listeners(t)},e.emit=function(t,e,r){return this._emitter.emit(t,e,r)},e.trigger=function(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){if(w.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),!this.triggeringException){this.triggeringException=!0;var r=t===S.ERROR;this.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,fatal:r,event:t,error:e}),this.triggeringException=!1}}return!1},e.listenerCount=function(t){return this._emitter.listenerCount(t)},e.destroy=function(){w.log("destroy"),this.trigger(S.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((function(t){return t.destroy()})),this.networkControllers.length=0,this.coreComponents.forEach((function(t){return t.destroy()})),this.coreComponents.length=0;var t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null},e.attachMedia=function(t){w.log("attachMedia"),this._media=t,this.trigger(S.MEDIA_ATTACHING,{media:t})},e.detachMedia=function(){w.log("detachMedia"),this.trigger(S.MEDIA_DETACHING,void 0),this._media=null},e.loadSource=function(t){this.stopLoad();var e=this.media,r=this.url,i=this.url=p.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,w.log("loadSource:"+i),e&&r&&(r!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(S.MANIFEST_LOADING,{url:t})},e.startLoad=function(t){void 0===t&&(t=-1),w.log("startLoad("+t+")"),this.started=!0,this.networkControllers.forEach((function(e){e.startLoad(t)}))},e.stopLoad=function(){w.log("stopLoad"),this.started=!1,this.networkControllers.forEach((function(t){t.stopLoad()}))},e.resumeBuffering=function(){this.started&&this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.startLoad(-1)}))},e.pauseBuffering=function(){this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.stopLoad()}))},e.swapAudioCodec=function(){w.log("swapAudioCodec"),this.streamController.swapAudioCodec()},e.recoverMediaError=function(){w.log("recoverMediaError");var t=this._media;this.detachMedia(),t&&this.attachMedia(t)},e.removeLevel=function(t){this.levelController.removeLevel(t)},e.setAudioOption=function(t){var e;return null==(e=this.audioTrackController)?void 0:e.setAudioOption(t)},e.setSubtitleOption=function(t){var e;return null==(e=this.subtitleTrackController)||e.setSubtitleOption(t),null},s(t,[{key:"levels",get:function(){var t=this.levelController.levels;return t||[]}},{key:"currentLevel",get:function(){return this.streamController.currentLevel},set:function(t){w.log("set currentLevel:"+t),this.levelController.manualLevel=t,this.streamController.immediateLevelSwitch()}},{key:"nextLevel",get:function(){return this.streamController.nextLevel},set:function(t){w.log("set nextLevel:"+t),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}},{key:"loadLevel",get:function(){return this.levelController.level},set:function(t){w.log("set loadLevel:"+t),this.levelController.manualLevel=t}},{key:"nextLoadLevel",get:function(){return this.levelController.nextLoadLevel},set:function(t){this.levelController.nextLoadLevel=t}},{key:"firstLevel",get:function(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)},set:function(t){w.log("set firstLevel:"+t),this.levelController.firstLevel=t}},{key:"startLevel",get:function(){var t=this.levelController.startLevel;return-1===t&&this.abrController.forcedAutoLevel>-1?this.abrController.forcedAutoLevel:t},set:function(t){w.log("set startLevel:"+t),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}},{key:"capLevelToPlayerSize",get:function(){return this.config.capLevelToPlayerSize},set:function(t){var e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}},{key:"autoLevelCapping",get:function(){return this._autoLevelCapping},set:function(t){this._autoLevelCapping!==t&&(w.log("set autoLevelCapping:"+t),this._autoLevelCapping=t,this.levelController.checkMaxAutoUpdated())}},{key:"bandwidthEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimate():NaN},set:function(t){this.abrController.resetEstimator(t)}},{key:"ttfbEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimateTTFB():NaN}},{key:"maxHdcpLevel",get:function(){return this._maxHdcpLevel},set:function(t){(function(t){return ze.indexOf(t)>-1})(t)&&this._maxHdcpLevel!==t&&(this._maxHdcpLevel=t,this.levelController.checkMaxAutoUpdated())}},{key:"autoLevelEnabled",get:function(){return-1===this.levelController.manualLevel}},{key:"manualLevel",get:function(){return this.levelController.manualLevel}},{key:"minAutoLevel",get:function(){var t=this.levels,e=this.config.minAutoBitrate;if(!t)return 0;for(var r=t.length,i=0;i=e)return i;return 0}},{key:"maxAutoLevel",get:function(){var t,e=this.levels,r=this.autoLevelCapping,i=this.maxHdcpLevel;if(t=-1===r&&null!=e&&e.length?e.length-1:r,i)for(var n=t;n--;){var a=e[n].attrs["HDCP-LEVEL"];if(a&&a<=i)return n}return t}},{key:"firstAutoLevel",get:function(){return this.abrController.firstAutoLevel}},{key:"nextAutoLevel",get:function(){return this.abrController.nextAutoLevel},set:function(t){this.abrController.nextAutoLevel=t}},{key:"playingDate",get:function(){return this.streamController.currentProgramDateTime}},{key:"mainForwardBufferInfo",get:function(){return this.streamController.getMainFwdBufferInfo()}},{key:"allAudioTracks",get:function(){var t=this.audioTrackController;return t?t.allAudioTracks:[]}},{key:"audioTracks",get:function(){var t=this.audioTrackController;return t?t.audioTracks:[]}},{key:"audioTrack",get:function(){var t=this.audioTrackController;return t?t.audioTrack:-1},set:function(t){var e=this.audioTrackController;e&&(e.audioTrack=t)}},{key:"allSubtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.allSubtitleTracks:[]}},{key:"subtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTracks:[]}},{key:"subtitleTrack",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTrack:-1},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}},{key:"media",get:function(){return this._media}},{key:"subtitleDisplay",get:function(){var t=this.subtitleTrackController;return!!t&&t.subtitleDisplay},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}},{key:"lowLatencyMode",get:function(){return this.config.lowLatencyMode},set:function(t){this.config.lowLatencyMode=t}},{key:"liveSyncPosition",get:function(){return this.latencyController.liveSyncPosition}},{key:"latency",get:function(){return this.latencyController.latency}},{key:"maxLatency",get:function(){return this.latencyController.maxLatency}},{key:"targetLatency",get:function(){return this.latencyController.targetLatency}},{key:"drift",get:function(){return this.latencyController.drift}},{key:"forceStartLoad",get:function(){return this.streamController.forceStartLoad}}],[{key:"version",get:function(){return"1.5.8"}},{key:"Events",get:function(){return S}},{key:"ErrorTypes",get:function(){return L}},{key:"ErrorDetails",get:function(){return A}},{key:"DefaultConfig",get:function(){return t.defaultConfig?t.defaultConfig:Js},set:function(e){t.defaultConfig=e}}]),t}();return oo.defaultConfig=void 0,oo},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(r="undefined"!=typeof globalThis?globalThis:r||self).Hls=i()}(!1); +//# sourceMappingURL=hls.min.js.map diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/index.html b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/index.html new file mode 100644 index 0000000000..0dd53297e4 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/index.html @@ -0,0 +1,18 @@ + + + + LWS Example + + + + +
+ + Hello from the minimal http server example. +
+ You can confirm the 404 page handler by going to this + nonexistant page. +

+ View Media Library + + diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/libwebsockets.org-logo.svg b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/libwebsockets.org-logo.svg new file mode 100644 index 0000000000..ef241b37c1 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/libwebsockets.org-logo.svg @@ -0,0 +1,66 @@ + + + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.css b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.css new file mode 100644 index 0000000000..8dd37af7be --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.css @@ -0,0 +1,73 @@ +body { + font-family: Arial, sans-serif; + background-color: #1a1a1a; + color: #ffffff; + margin: 0; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; +} +.player-container { + width: 100%; + max-width: 800px; + margin-top: 20px; + background-color: #000; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0,0,0,0.5); +} +video { + width: 100%; + height: auto; + display: block; +} +.header { + width: 100%; + max-width: 800px; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} +.back-link { + color: #4CAF50; + text-decoration: none; + font-weight: bold; +} +.back-link:hover { + text-decoration: underline; +} +#auth-status { + float: right; + margin-top: 10px; + margin-right: 20px; +} + +.debug-panel { + width: 100%; + max-width: 800px; + margin-top: 20px; + background-color: #2b2b2b; + border-radius: 8px; + padding: 15px; + box-sizing: border-box; +} + +.debug-panel h3 { + margin-top: 0; + color: #4CAF50; + font-size: 16px; + border-bottom: 1px solid #444; + padding-bottom: 8px; +} + +.debug-logs { + max-height: 200px; + overflow-y: auto; + font-family: monospace; + font-size: 12px; + color: #00FF00; + line-height: 1.5; +} + diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.html b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.html new file mode 100644 index 0000000000..d1ecd8a695 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.html @@ -0,0 +1,29 @@ + + + + + LWS Media Player + + + + + + +
+
+ +

LWS Media Player

+ ← Back to Library +
+ +
+ +
+ +
+

Player Logs

+
Ready.
+
+ + + diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.js b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.js new file mode 100644 index 0000000000..dd963555ae --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/player.js @@ -0,0 +1,125 @@ +document.addEventListener('DOMContentLoaded', function() { + if (typeof window.renderLwsLoginStatus === 'function') { + window.renderLwsLoginStatus('auth-status'); + } + + var video = document.getElementById('video'); + + var urlParams = new URLSearchParams(window.location.search); + var videoSrc = urlParams.get('v'); + var rawSrc = urlParams.get('raw'); + + if (rawSrc) { + // Play directly via HTTP Range requests natively supported by lws + video.src = rawSrc; + video.addEventListener('loadedmetadata', function() { + video.play(); + }); + return; + } + + if (!videoSrc) { + alert("No video source provided."); + return; + } + + var logsContainer = document.getElementById('debug-logs'); + function logMsg(msg) { + if (logsContainer) { + var time = new Date().toLocaleTimeString(); + logsContainer.innerHTML = '[' + time + '] ' + msg + '
' + logsContainer.innerHTML; + } + } + + // Video Element Events + video.addEventListener('play', function() { logMsg('video: play'); }); + video.addEventListener('playing', function() { logMsg('video: playing'); }); + video.addEventListener('pause', function() { logMsg('video: pause'); }); + video.addEventListener('waiting', function() { logMsg('video: waiting (buffering)'); }); + video.addEventListener('stalled', function() { logMsg('video: stalled'); }); + video.addEventListener('seeking', function() { logMsg('video: seeking to ' + video.currentTime.toFixed(3) + 's'); }); + video.addEventListener('seeked', function() { logMsg('video: seeked to ' + video.currentTime.toFixed(3) + 's'); }); + video.addEventListener('error', function() { + var err = video.error; + logMsg('video error: code ' + (err ? err.code : 'unknown') + ', msg: ' + (err ? err.message : 'unknown')); + }); + + if (Hls.isSupported()) { + logMsg('hls.js supported'); + var hls = new Hls({ + debug: false, + maxBufferLength: 60, + maxMaxBufferLength: 120, + maxBufferHole: 0.5, + startPosition: 0, + nudgeMaxRetry: 5, + }); + hls.loadSource(videoSrc); + hls.attachMedia(video); + + hls.on(Hls.Events.MANIFEST_PARSED, function() { + logMsg('hls: manifest parsed, playing'); + video.play(); + }); + + hls.on(Hls.Events.ERROR, function(event, data) { + var msg = 'hls error: type=' + data.type + ', details=' + data.details + ', fatal=' + data.fatal; + logMsg(msg); + if (data.fatal) { + switch(data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + logMsg('hls: fatal network error, trying to recover'); + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + logMsg('hls: fatal media error, trying to recover'); + hls.recoverMediaError(); + break; + default: + logMsg('hls: unrecoverable fatal error'); + hls.destroy(); + break; + } + } + }); + + hls.on(Hls.Events.BUFFER_APPENDED, function() { + if (video.buffered.length > 0) { + var ranges = []; + for (var i = 0; i < video.buffered.length; i++) { + ranges.push('[' + video.buffered.start(i).toFixed(1) + 's - ' + video.buffered.end(i).toFixed(1) + 's]'); + } + logMsg('buffer: ' + ranges.join(', ')); + } + }); + hls.on(Hls.Events.FRAG_LOADING, function(event, data) { + if (data.frag) { + logMsg('loading seg ' + data.frag.sn + ' (' + data.frag.start.toFixed(1) + 's - ' + (data.frag.start + data.frag.duration).toFixed(1) + 's)'); + } + }); + } + // For Safari, which natively supports HLS + else if (video.canPlayType('application/vnd.apple.mpegurl')) { + logMsg('native HLS supported'); + video.src = videoSrc; + video.addEventListener('loadedmetadata', function() { + logMsg('native HLS metadata loaded, playing'); + video.play(); + }); + + // Polling buffer status for native HLS + setInterval(function() { + if (video.buffered.length > 0) { + var ranges = []; + for (var i = 0; i < video.buffered.length; i++) { + ranges.push('[' + video.buffered.start(i).toFixed(1) + 's - ' + video.buffered.end(i).toFixed(1) + 's]'); + } + logMsg('native buffer: ' + ranges.join(', ')); + } + }, 2000); + } + else { + logMsg('error: browser does not support HLS'); + alert("Your browser does not support playing this video."); + } +}); diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/strict-csp.svg b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/strict-csp.svg new file mode 100644 index 0000000000..cd128f1d21 --- /dev/null +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-hls/mount-origin/strict-csp.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/minimal-examples-lowlevel/http-server/minimal-http-server-sse-ring/minimal-http-server-sse-ring.c b/minimal-examples-lowlevel/http-server/minimal-http-server-sse-ring/minimal-http-server-sse-ring.c index d18b7f6c33..6760a0e9a0 100644 --- a/minimal-examples-lowlevel/http-server/minimal-http-server-sse-ring/minimal-http-server-sse-ring.c +++ b/minimal-examples-lowlevel/http-server/minimal-http-server-sse-ring/minimal-http-server-sse-ring.c @@ -180,6 +180,8 @@ callback_sse(struct lws *wsi, enum lws_callback_reasons reason, void *user, case LWS_CALLBACK_PROTOCOL_INIT: vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct vhd)); + if (!vhd) + return 1; vhd->context = lws_get_context(wsi); vhd->protocol = lws_get_protocol(wsi); vhd->vhost = lws_get_vhost(wsi); diff --git a/minimal-examples-lowlevel/raw/minimal-raw-file/minimal-raw-file.c b/minimal-examples-lowlevel/raw/minimal-raw-file/minimal-raw-file.c index e6c3548ddf..88ebde7cf8 100644 --- a/minimal-examples-lowlevel/raw/minimal-raw-file/minimal-raw-file.c +++ b/minimal-examples-lowlevel/raw/minimal-raw-file/minimal-raw-file.c @@ -49,6 +49,8 @@ callback_raw_test(struct lws *wsi, enum lws_callback_reasons reason, case LWS_CALLBACK_PROTOCOL_INIT: vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct raw_vhd)); + if (!vhd) + return 1; vhd->filefd = lws_open(filepath, O_RDWR); if (vhd->filefd == -1) { lwsl_err("Unable to open %s\n", filepath); diff --git a/minimal-examples-lowlevel/raw/minimal-raw-serial/minimal-raw-file.c b/minimal-examples-lowlevel/raw/minimal-raw-serial/minimal-raw-file.c index 06387f8349..da3ef757e1 100644 --- a/minimal-examples-lowlevel/raw/minimal-raw-serial/minimal-raw-file.c +++ b/minimal-examples-lowlevel/raw/minimal-raw-serial/minimal-raw-file.c @@ -72,6 +72,8 @@ callback_raw_test(struct lws *wsi, enum lws_callback_reasons reason, case LWS_CALLBACK_PROTOCOL_INIT: vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct raw_vhd)); + if (!vhd) + return 1; vhd->filefd = lws_open(filepath, O_RDWR); if (vhd->filefd == -1) { lwsl_err("Unable to open %s\n", filepath); diff --git a/minimal-examples-lowlevel/raw/minimal-raw-webrtc-camshow/minimal-raw-webrtc-camshow.c b/minimal-examples-lowlevel/raw/minimal-raw-webrtc-camshow/minimal-raw-webrtc-camshow.c index f7f0aa7ac7..4c0c13752b 100644 --- a/minimal-examples-lowlevel/raw/minimal-raw-webrtc-camshow/minimal-raw-webrtc-camshow.c +++ b/minimal-examples-lowlevel/raw/minimal-raw-webrtc-camshow/minimal-raw-webrtc-camshow.c @@ -43,7 +43,6 @@ static const struct lws_switches switches[] = { static const char *url = "wss://127.0.0.1:7681"; static const char *devs_list = "/dev/video0"; static const char *audio_dev = "default"; -static char *devices_copy = NULL; static const char *client_name; static uint32_t app_width = 1280; static uint32_t app_height = 720; @@ -121,16 +120,33 @@ set_clock(lws_usec_t us) static void start_app_attach(struct lws_vhost *vh, const char *logical_name, const char *access_token) { - char *devices_copy_local = strdup(devs_list); - char *p = devices_copy_local, *token; + struct lws_tokenize ts; + lws_tokenize_elem e; + char token[256]; + + lws_tokenize_init(&ts, devs_list, LWS_TOKENIZE_F_COMMA_SEP_LIST | + LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_SLASH_NONTERM | + LWS_TOKENIZE_F_DOT_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + do { + e = lws_tokenize(&ts); + if (e != LWS_TOKZE_TOKEN) + continue; + + if (lws_tokenize_cstr(&ts, token, sizeof(token))) { + lwsl_err("Token too long\n"); + continue; + } - while ((token = strsep(&p, ","))) { lwsl_notice("Attaching %s (audio %s) to WebRTC mixer\n", token, audio_dev); - if (cam_ops->attach(vh, url, token, audio_dev, client_name, app_width, app_height, access_token)) + if (cam_ops->attach(vh, url, token, audio_dev, client_name, + app_width, app_height, access_token)) lwsl_err("Failed to queue attach for %s\n", token); - } - free(devices_copy_local); + } while (e > 0); } static void pairing_indication(struct lws_vhost *vh, const char *logical_name, int start) @@ -268,7 +284,5 @@ main(int argc, const char **argv) lws_context_destroy(cx); - if (devices_copy) free(devices_copy); - return 0; } diff --git a/minimal-examples-lowlevel/webtransport/minimal-webtransport-test-server/CMakeLists.txt b/minimal-examples-lowlevel/webtransport/minimal-webtransport-qir/CMakeLists.txt similarity index 74% rename from minimal-examples-lowlevel/webtransport/minimal-webtransport-test-server/CMakeLists.txt rename to minimal-examples-lowlevel/webtransport/minimal-webtransport-qir/CMakeLists.txt index e97b2245e0..332b4502fd 100644 --- a/minimal-examples-lowlevel/webtransport/minimal-webtransport-test-server/CMakeLists.txt +++ b/minimal-examples-lowlevel/webtransport/minimal-webtransport-qir/CMakeLists.txt @@ -1,19 +1,20 @@ -project(lws-minimal-webtransport-test-server C) +project(lws-minimal-webtransport-qir C) cmake_minimum_required(VERSION 3.10) find_package(libwebsockets CONFIG REQUIRED) list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR}) include(CheckCSourceCompiles) include(LwsCheckRequirements) -set(SAMP lws-minimal-webtransport-test-server) -set(SRCS minimal-webtransport-test-server.c ../../../../plugins/protocol_webtransport_test/protocol_webtransport_test.c) +set(SAMP lws-minimal-webtransport-qir) +set(SRCS minimal-webtransport-qir.c) set(requirements 1) -require_lws_config(LWS_ROLE_WT 1 requirements) +require_lws_config(LWS_WITH_NETWORK 1 requirements) +require_lws_config(LWS_WITH_CLIENT 1 requirements) require_lws_config(LWS_WITH_SERVER 1 requirements) +require_lws_config(LWS_ROLE_WT 1 requirements) require_lws_config(LWS_WITH_HTTP3 1 requirements) - if (requirements) add_executable(${SAMP} ${SRCS}) if (websockets_shared) diff --git a/minimal-examples-lowlevel/webtransport/minimal-webtransport-qir/minimal-webtransport-qir.c b/minimal-examples-lowlevel/webtransport/minimal-webtransport-qir/minimal-webtransport-qir.c new file mode 100644 index 0000000000..89e00568cd --- /dev/null +++ b/minimal-examples-lowlevel/webtransport/minimal-webtransport-qir/minimal-webtransport-qir.c @@ -0,0 +1,1443 @@ +/* + * lws-minimal-webtransport-qir + * + * Written in 2010-2026 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * WebTransport Interop Runner shim implementation. + */ + +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#include +#define mkdir(path, mode) _mkdir(path) +static void usleep(unsigned long l) { Sleep(l / 1000); } +#else +#include +#endif +#include +#include + + + +static int interrupted; +static int is_server; +static char testcase[64] = ""; +static struct lws_context *context; +static struct lws *first_session_wsi; +static char global_endpoint[128] = ""; + + + +struct request_item { + char url[512]; + char host[128]; + int port; + char endpoint[128]; + char filename[128]; + struct lws *session_wsi; + int started; + int completed; +}; + +static struct request_item client_requests[256]; +static int client_requests_count; + +struct server_request_item { + char endpoint[128]; + char filename[128]; + int started; + int completed; +}; + +static struct server_request_item server_requests[256]; +static int server_requests_count; + +struct pending_datagram { + char filename[256]; + int is_push; /* 1 for PUSH, 0 for GET */ +}; + +static struct pending_datagram dg_queue[512]; +static int dg_queue_head; +static int dg_queue_tail; + +static void enqueue_datagram(const char *filename, int is_push) +{ + if ((dg_queue_tail + 1) % 512 == dg_queue_head) { + lwsl_err("Datagram queue overflow!\n"); + return; + } + struct pending_datagram *dg = &dg_queue[dg_queue_tail]; + lws_strncpy(dg->filename, filename, sizeof(dg->filename)); + dg->is_push = is_push; + dg_queue_tail = (dg_queue_tail + 1) % 512; +} + +static struct pending_datagram *dequeue_datagram(void) +{ + if (dg_queue_head == dg_queue_tail) + return NULL; + struct pending_datagram *dg = &dg_queue[dg_queue_head]; + dg_queue_head = (dg_queue_head + 1) % 512; + return dg; +} + +struct pss_qir { + struct lws *wsi; + int is_session; + char endpoint[128]; + + /* For child streams */ + int is_unidi; + int is_initiator; /* 1 if we sent GET, 0 if we received GET */ + char filename[256]; + int request_index; + + /* Send state */ + int fd_in; + size_t file_len; + size_t sent_len; + int header_sent; + + /* Receive state */ + int fd_out; + char push_hdr[512]; + size_t push_hdr_len; + size_t push_hdr_read; + int push_hdr_done; + int initialized; + int write_completed; +}; + +static void init_pss(struct pss_qir *pss) +{ + if (pss && !pss->initialized) { + pss->fd_in = -1; + pss->fd_out = -1; + pss->request_index = -1; + pss->write_completed = 0; + pss->initialized = 1; + } +} + +static int pss_is_file_sender(struct pss_qir *pss) +{ + int local_is_sender = (strstr(testcase, "-receive") && is_server) || + (strstr(testcase, "-send") && !is_server) || + (strcmp(testcase, "transfer") == 0 && (is_server || !client_requests_count || client_requests[0].filename[0] == '\0')); + if (pss->is_unidi) + return local_is_sender && !pss->is_initiator; + return local_is_sender; +} + +static int pss_is_file_receiver(struct pss_qir *pss) +{ + int local_is_receiver = (strstr(testcase, "-receive") && !is_server) || + (strstr(testcase, "-send") && is_server) || + (strcmp(testcase, "transfer") == 0 && !is_server && client_requests_count && client_requests[0].filename[0] != '\0'); + if (pss->is_unidi) + return local_is_receiver && !pss->is_initiator; + return local_is_receiver; +} + +#if defined(LWS_WITH_CUSTOM_HEADERS) +static void print_custom_header_cb(const char *name, int nlen, void *custom) +{ + struct lws *wsi = (struct lws *)custom; + char val[128]; + int vl = lws_hdr_custom_copy(wsi, val, sizeof(val) - 1, name, nlen); + if (vl >= 0) { + val[vl] = '\0'; + lwsl_user(" Custom Header: %.*s = %s\n", nlen, name, val); + } +} +#endif + + +static void sigint_handler(int sig) +{ + interrupted = 1; +} +static void trim_trailing_whitespace(char *str) +{ + size_t len = strlen(str); + while (len > 0 && (str[len - 1] == ' ' || str[len - 1] == '\r' || str[len - 1] == '\n' || str[len - 1] == '\t')) { + str[len - 1] = '\0'; + len--; + } +} + +static int parse_client_requests(void) +{ + const char *reqs = getenv("REQUESTS_CLIENT"); + struct lws_tokenize ts; + lws_tokenize_elem e; + char token[512]; + + if (!reqs) + reqs = getenv("REQUESTS"); + if (!reqs) + return 0; + + lws_tokenize_init(&ts, reqs, LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_SLASH_NONTERM | + LWS_TOKENIZE_F_DOT_NONTERM | + LWS_TOKENIZE_F_COLON_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + while ((e = lws_tokenize(&ts)) > 0) { + if (e != LWS_TOKZE_TOKEN) + continue; + + if (lws_tokenize_cstr(&ts, token, sizeof(token))) { + lwsl_err("Client request URL too long\n"); + continue; + } + + struct request_item *item = &client_requests[client_requests_count]; + lws_strncpy(item->url, token, sizeof(item->url)); + trim_trailing_whitespace(item->url); + + /* Parse URL: https://:// */ + char *p = item->url; + if (strncmp(p, "https://", 8) == 0) + p += 8; + + char *host_start = p; + char *slash = strchr(p, '/'); + if (!slash) + continue; + + *slash = '\0'; + char *port_colon = strchr(host_start, ':'); + if (port_colon) { + *port_colon = '\0'; + item->port = atoi(port_colon + 1); + } else { + item->port = 443; + } + lws_strncpy(item->host, host_start, sizeof(item->host)); + *slash = '/'; + + p = slash + 1; + char *next_slash = strchr(p, '/'); + if (next_slash) { + *next_slash = '\0'; + lws_snprintf(item->endpoint, sizeof(item->endpoint), "/%s", p); + *next_slash = '/'; + lws_strncpy(item->filename, next_slash + 1, sizeof(item->filename)); + trim_trailing_whitespace(item->filename); + } else { + lws_snprintf(item->endpoint, sizeof(item->endpoint), "/%s", p); + item->filename[0] = '\0'; + } + + client_requests_count++; + if (client_requests_count >= (int)LWS_ARRAY_SIZE(client_requests)) + break; + } + + /* If we are the client and acting as the sender, we also need to parse files from REQUESTS_SERVER */ + const char *sreqs = getenv("REQUESTS_SERVER"); + if (sreqs && client_requests_count == 1 && client_requests[0].filename[0] == '\0') { + struct lws_tokenize sts; + lws_tokenize_elem se; + char stoken[256]; + int first = 1; + + lws_tokenize_init(&sts, sreqs, LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_SLASH_NONTERM | + LWS_TOKENIZE_F_DOT_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + while ((se = lws_tokenize(&sts)) > 0) { + if (se != LWS_TOKZE_TOKEN) + continue; + + if (lws_tokenize_cstr(&sts, stoken, sizeof(stoken))) + continue; + + /* format: / */ + char *slash = strchr(stoken, '/'); + if (!slash) + continue; + + *slash = '\0'; + char filename[256]; + lws_strncpy(filename, slash + 1, sizeof(filename)); + trim_trailing_whitespace(filename); + *slash = '/'; + + if (first) { + lws_strncpy(client_requests[0].filename, filename, sizeof(client_requests[0].filename)); + first = 0; + } else { + struct request_item *item = &client_requests[client_requests_count]; + item->port = client_requests[0].port; + lws_strncpy(item->host, client_requests[0].host, sizeof(item->host)); + lws_strncpy(item->endpoint, client_requests[0].endpoint, sizeof(item->endpoint)); + lws_strncpy(item->filename, filename, sizeof(item->filename)); + client_requests_count++; + if (client_requests_count >= (int)LWS_ARRAY_SIZE(client_requests)) + break; + } + } + } + + return client_requests_count; +} + +static int parse_server_requests(void) +{ + const char *reqs = getenv("REQUESTS_SERVER"); + struct lws_tokenize ts; + lws_tokenize_elem e; + char token[256]; + + if (!reqs) + reqs = getenv("REQUESTS"); + if (!reqs) + return 0; + + lws_tokenize_init(&ts, reqs, LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_SLASH_NONTERM | + LWS_TOKENIZE_F_DOT_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + while ((e = lws_tokenize(&ts)) > 0) { + if (e != LWS_TOKZE_TOKEN) + continue; + + if (lws_tokenize_cstr(&ts, token, sizeof(token))) + continue; + + struct server_request_item *item = &server_requests[server_requests_count]; + + /* format: / */ + char *slash = strchr(token, '/'); + if (!slash) + continue; + + *slash = '\0'; + lws_snprintf(item->endpoint, sizeof(item->endpoint), "/%s", token); + *slash = '/'; + lws_strncpy(item->filename, slash + 1, sizeof(item->filename)); + trim_trailing_whitespace(item->filename); + + server_requests_count++; + if (server_requests_count >= (int)LWS_ARRAY_SIZE(server_requests)) + break; + } + + return server_requests_count; +} + +static void trigger_client_transfers(struct lws *wsi_session, const char *endpoint) +{ + int i; + int local_is_sender = (strstr(testcase, "-receive") && is_server) || + (strstr(testcase, "-send") && !is_server) || + (strcmp(testcase, "transfer") == 0 && (is_server || !client_requests_count || client_requests[0].filename[0] == '\0')); + int local_is_receiver = (strstr(testcase, "-receive") && !is_server) || + (strstr(testcase, "-send") && is_server) || + (strcmp(testcase, "transfer") == 0 && !is_server && client_requests_count && client_requests[0].filename[0] != '\0'); + + lwsl_user("trigger_client_transfers called for endpoint %s, client_requests_count=%d\n", endpoint, client_requests_count); + if (local_is_sender) { + lwsl_user(" Local node is sender, not initiating client transfers.\n"); + return; + } + + for (i = 0; i < client_requests_count; i++) { + struct request_item *item = &client_requests[i]; + lwsl_user(" Client Request %d: url=%s endpoint=%s started=%d filename=%s\n", i, item->url, item->endpoint, item->started, item->filename); + if (strcmp(item->endpoint, endpoint) == 0 && !item->started && item->filename[0]) { + /* Start the transfer according to the testcase */ + item->started = 1; + lwsl_user(" Triggering request %d (%s) on testcase %s\n", i, item->filename, testcase); + if (strstr(testcase, "unidirectional")) { + struct lws *cwsi = lws_wt_create_stream(wsi_session, 1); + lwsl_user(" lws_wt_create_stream(unidi=1) returned wsi %p\n", cwsi); + if (cwsi) { + int err = lws_ensure_user_space(cwsi); + lwsl_user(" lws_ensure_user_space returned %d\n", err); + if (!err) { + struct pss_qir *pss = (struct pss_qir *)lws_wsi_user(cwsi); + lwsl_user(" pss user space: %p\n", pss); + if (pss) { + init_pss(pss); + pss->is_unidi = 1; + pss->is_initiator = 1; + pss->request_index = i; + lws_strncpy(pss->endpoint, item->endpoint, sizeof(pss->endpoint)); + lws_strncpy(pss->filename, item->filename, sizeof(pss->filename)); + lws_callback_on_writable(cwsi); + lwsl_user(" Requested writable callback for client stream wsi %p\n", cwsi); + } + } + } + } else if (strstr(testcase, "bidirectional")) { + struct lws *cwsi = lws_wt_create_stream(wsi_session, 0); + lwsl_user(" lws_wt_create_stream(unidi=0) returned wsi %p\n", cwsi); + if (cwsi) { + int err = lws_ensure_user_space(cwsi); + lwsl_user(" lws_ensure_user_space returned %d\n", err); + if (!err) { + struct pss_qir *pss = (struct pss_qir *)lws_wsi_user(cwsi); + lwsl_user(" pss user space: %p\n", pss); + if (pss) { + init_pss(pss); + pss->is_unidi = 0; + pss->is_initiator = 1; + pss->request_index = i; + lws_strncpy(pss->endpoint, item->endpoint, sizeof(pss->endpoint)); + lws_strncpy(pss->filename, item->filename, sizeof(pss->filename)); + lws_callback_on_writable(cwsi); + lwsl_user(" Requested writable callback for client stream wsi %p\n", cwsi); + } + } + } + } else if (strstr(testcase, "datagram")) { + if (local_is_receiver) { + enqueue_datagram(item->filename, 0); /* 0 for GET */ + lws_callback_on_writable(wsi_session); + } + } + } + } +} + +static void trigger_server_transfers(struct lws *wsi_session, const char *endpoint) +{ + int i; + int local_is_sender = (strstr(testcase, "-receive") && is_server) || + (strstr(testcase, "-send") && !is_server) || + (strcmp(testcase, "transfer") == 0 && (is_server || !client_requests_count || client_requests[0].filename[0] == '\0')); + int local_is_receiver = (strstr(testcase, "-receive") && !is_server) || + (strstr(testcase, "-send") && is_server) || + (strcmp(testcase, "transfer") == 0 && !is_server && client_requests_count && client_requests[0].filename[0] != '\0'); + + lwsl_user("trigger_server_transfers called for endpoint %s, server_requests_count=%d\n", endpoint, server_requests_count); + if (local_is_sender) { + lwsl_user(" Local node is sender, not initiating server transfers.\n"); + return; + } + + for (i = 0; i < server_requests_count; i++) { + struct server_request_item *item = &server_requests[i]; + lwsl_user(" Server Request %d: endpoint=%s started=%d filename=%s\n", i, item->endpoint, item->started, item->filename); + if (strcmp(item->endpoint, endpoint) == 0 && !item->started) { + item->started = 1; + lwsl_user(" Triggering server request %d (%s) on testcase %s\n", i, item->filename, testcase); + if (strstr(testcase, "unidirectional")) { + struct lws *cwsi = lws_wt_create_stream(wsi_session, 1); + lwsl_user(" lws_wt_create_stream(unidi=1) returned wsi %p\n", cwsi); + if (cwsi) { + int err = lws_ensure_user_space(cwsi); + lwsl_user(" lws_ensure_user_space returned %d\n", err); + if (!err) { + struct pss_qir *pss = (struct pss_qir *)lws_wsi_user(cwsi); + lwsl_user(" pss user space: %p\n", pss); + if (pss) { + init_pss(pss); + pss->is_unidi = 1; + pss->is_initiator = 1; + pss->request_index = i; + lws_strncpy(pss->endpoint, item->endpoint, sizeof(pss->endpoint)); + lws_strncpy(pss->filename, item->filename, sizeof(pss->filename)); + lws_callback_on_writable(cwsi); + lwsl_user(" Requested writable callback for server stream wsi %p\n", cwsi); + } + } + } + } else if (strstr(testcase, "bidirectional")) { + struct lws *cwsi = lws_wt_create_stream(wsi_session, 0); + lwsl_user(" lws_wt_create_stream(unidi=0) returned wsi %p\n", cwsi); + if (cwsi) { + int err = lws_ensure_user_space(cwsi); + lwsl_user(" lws_ensure_user_space returned %d\n", err); + if (!err) { + struct pss_qir *pss = (struct pss_qir *)lws_wsi_user(cwsi); + lwsl_user(" pss user space: %p\n", pss); + if (pss) { + init_pss(pss); + pss->is_unidi = 0; + pss->is_initiator = 1; + pss->request_index = i; + lws_strncpy(pss->endpoint, item->endpoint, sizeof(pss->endpoint)); + lws_strncpy(pss->filename, item->filename, sizeof(pss->filename)); + lws_callback_on_writable(cwsi); + lwsl_user(" Requested writable callback for server stream wsi %p\n", cwsi); + } + } + } + } else if (strstr(testcase, "datagram")) { + if (local_is_receiver) { + enqueue_datagram(item->filename, 0); /* 0 for GET */ + lws_callback_on_writable(wsi_session); + } + } + } + } +} + +static int callback_qir(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct pss_qir *pss = (struct pss_qir *)user; + uint8_t buf[LWS_PRE + 4096], *p; + int n, m; + + if (pss) { + init_pss(pss); + } + + if (!pss && reason != LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER) + return 0; + + switch (reason) { + + case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: + { + unsigned char **hp = (unsigned char **)in, *end = (*hp) + len; + + /* Add sec-webtransport-http3-draft header */ + if (lws_add_http_header_by_name(wsi, + (const unsigned char *)"sec-webtransport-http3-draft:", + (const unsigned char *)"draft02", 7, hp, end)) + return -1; + + /* Format and send PROTOCOLS as custom headers */ + const char *env_protocols = getenv("PROTOCOLS_CLIENT"); + if (!env_protocols) + env_protocols = getenv("PROTOCOLS"); + + if (env_protocols) { + char av_buf[512] = ""; + struct lws_tokenize ts; + lws_tokenize_elem e; + char tok[128]; + int first = 1; + + lws_tokenize_init(&ts, env_protocols, LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + while ((e = lws_tokenize(&ts)) > 0) { + if (e != LWS_TOKZE_TOKEN) + continue; + + if (lws_tokenize_cstr(&ts, tok, sizeof(tok))) + continue; + + if (sizeof(av_buf) - strlen(av_buf) > strlen(tok) + 5) { + if (!first) + strcat(av_buf, ", "); + strcat(av_buf, "\""); + strcat(av_buf, tok); + strcat(av_buf, "\""); + first = 0; + } + } + + if (lws_add_http_header_by_name(wsi, + (const unsigned char *)"wt-available-protocols:", + (const unsigned char *)av_buf, (int)strlen(av_buf), hp, end)) + return -1; + } + break; + } + + case LWS_CALLBACK_ESTABLISHED: + case LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED: + pss->wsi = wsi; + pss->request_index = -1; + if (lws_wt_is_session(wsi)) { + if (!is_server) + break; + pss->is_session = 1; + /* Extract endpoint path */ + char path[128]; + int path_len = lws_hdr_copy(wsi, path, sizeof(path) - 1, WSI_TOKEN_HTTP_COLON_PATH); + if (path_len > 0) { + path[path_len] = '\0'; + lws_strncpy(pss->endpoint, path, sizeof(pss->endpoint)); + lws_strncpy(global_endpoint, path, sizeof(global_endpoint)); + } + lwsl_user("Server WebTransport session established on %s\n", pss->endpoint); + + /* Save negotiated protocol to /downloads/negotiated_protocol.txt */ + { + char client_protos[256]; + char negotiated[64] = ""; + int cp_len = lws_hdr_custom_copy(wsi, client_protos, sizeof(client_protos) - 1, + "wt-available-protocols", 22); + if (cp_len > 0) { + const char *env_protocols = getenv("PROTOCOLS_SERVER"); + client_protos[cp_len] = '\0'; + if (!env_protocols) + env_protocols = getenv("PROTOCOLS"); + + if (env_protocols) { + /* client_protos format: '"proto1", "proto2"' */ + struct lws_tokenize ts; + lws_tokenize_elem e; + char token[64]; + + lws_tokenize_init(&ts, client_protos, LWS_TOKENIZE_F_COMMA_SEP_LIST | + LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + while ((e = lws_tokenize(&ts)) > 0) { + if (e != LWS_TOKZE_TOKEN && e != LWS_TOKZE_QUOTED_STRING) + continue; + + if (lws_tokenize_cstr(&ts, token, sizeof(token))) + continue; + + struct lws_tokenize sts; + lws_tokenize_elem se; + char sp_tok[64]; + + lws_tokenize_init(&sts, env_protocols, LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + while ((se = lws_tokenize(&sts)) > 0) { + if (se != LWS_TOKZE_TOKEN) + continue; + + if (lws_tokenize_cstr(&sts, sp_tok, sizeof(sp_tok))) + continue; + + if (strcmp(token, sp_tok) == 0) { + lws_strncpy(negotiated, token, sizeof(negotiated)); + break; + } + } + if (negotiated[0]) + break; + } + } + } + + if (negotiated[0]) { + if (mkdir("/downloads", 0777) < 0 && errno != EEXIST) { // NOSONAR + lwsl_err("Failed to create /downloads: %d\n", errno); + } + int nfd = open("/downloads/negotiated_protocol.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // NOSONAR + if (nfd >= 0) { + if (write(nfd, negotiated, LWS_POSIX_LENGTH_CAST(strlen(negotiated))) < 0) { + lwsl_err("Failed to write negotiated protocol\n"); + } + close(nfd); + } + } + } + + /* If server needs to request files, trigger them now */ + trigger_server_transfers(wsi, pss->endpoint); + } else { + pss->is_session = 0; + pss->is_unidi = lws_wt_is_unidi(wsi); + lws_strncpy(pss->endpoint, global_endpoint, sizeof(pss->endpoint)); + lwsl_user("%s stream established (unidi=%d)\n", is_server ? "Server" : "Client", pss->is_unidi); + + if (is_server && !pss->is_unidi && + lws_get_parent(wsi) && lws_wt_is_session(lws_get_parent(wsi))) { + /* Server receives files over client-initiated bidi streams. + * Find first unstarted server request, assign it to this stream, + * and trigger writable callback to send GET . */ + int i; + for (i = 0; i < server_requests_count; i++) { + if (!server_requests[i].started) { + server_requests[i].started = 1; + pss->request_index = i; + pss->is_initiator = 1; + lws_strncpy(pss->filename, server_requests[i].filename, sizeof(pss->filename)); + lws_callback_on_writable(wsi); + lwsl_user("Server assigned request %d (%s) to bidi stream %p\n", i, pss->filename, wsi); + break; + } + } + } + } + break; + + case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: + case LWS_CALLBACK_CLIENT_ESTABLISHED: + if (is_server) + break; + pss->wsi = wsi; + pss->request_index = -1; + if (lws_wt_is_session(wsi)) { + pss->is_session = 1; + + /* Match the endpoint from client_requests */ + int i; + for (i = 0; i < client_requests_count; i++) { + if (!client_requests[i].session_wsi) { + lws_strncpy(pss->endpoint, client_requests[i].endpoint, sizeof(pss->endpoint)); + client_requests[i].session_wsi = wsi; + lws_strncpy(global_endpoint, pss->endpoint, sizeof(global_endpoint)); + break; + } + } + lwsl_user("Client WebTransport session established on %s. Listing all custom headers:\n", pss->endpoint); +#if defined(LWS_WITH_CUSTOM_HEADERS) + lws_hdr_custom_name_foreach(wsi, print_custom_header_cb, wsi); +#endif + + /* Parse and save negotiated protocol to /downloads/negotiated_protocol.txt */ + char negotiated[64]; + int nl = lws_hdr_custom_copy(wsi, negotiated, sizeof(negotiated) - 1, "wt-protocol", 11); + lwsl_user("wt-protocol header lookup result: %d\n", nl); + if (nl > 0) { + negotiated[nl] = '\0'; + /* Remove quotes */ + char *n_ptr = negotiated; + while (*n_ptr == '"') n_ptr++; + char *ne = n_ptr + strlen(n_ptr); + while (ne > n_ptr && (ne[-1] == '"' || ne[-1] == '\r' || ne[-1] == '\n')) { + ne[-1] = '\0'; + ne--; + } + if (mkdir("/downloads", 0777) < 0 && errno != EEXIST) { // NOSONAR + lwsl_err("Failed to create /downloads: %d\n", errno); + } + int nfd = open("/downloads/negotiated_protocol.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // NOSONAR + if (nfd >= 0) { + if (write(nfd, n_ptr, LWS_POSIX_LENGTH_CAST(strlen(n_ptr))) < 0) { + lwsl_err("Failed to write negotiated protocol\n"); + } + close(nfd); + } + } + + /* If client needs to request files, trigger them now */ + trigger_client_transfers(wsi, pss->endpoint); + } else { + pss->is_session = 0; + pss->is_unidi = lws_wt_is_unidi(wsi); + lws_strncpy(pss->endpoint, global_endpoint, sizeof(pss->endpoint)); + lwsl_user("Client stream established (unidi=%d)\n", pss->is_unidi); + } + break; + + case LWS_CALLBACK_SERVER_WRITEABLE: + case LWS_CALLBACK_CLIENT_WRITEABLE: + if (pss->is_session) { + struct pending_datagram *dg = dequeue_datagram(); + if (dg) { + if (dg->is_push) { + /* Send PUSH \n as a datagram */ + char filepath[512]; + const char *endpoint = pss->endpoint[0] ? pss->endpoint : global_endpoint; + lws_snprintf(filepath, sizeof(filepath), "/www%s/%s", endpoint, dg->filename); + int fd = open(filepath, O_RDONLY); + if (fd >= 0) { + struct stat st; + if (fstat(fd, &st) == 0) { + uint8_t dgbuf[LWS_PRE + 65536]; + int hn = lws_snprintf((char *)&dgbuf[LWS_PRE], sizeof(dgbuf) - LWS_PRE, "PUSH %s\n", dg->filename); + if (hn > 0 && (size_t)hn < sizeof(dgbuf) - LWS_PRE) { + int rn = (int)read(fd, &dgbuf[LWS_PRE + hn], sizeof(dgbuf) - LWS_PRE - (size_t)hn); + if (rn >= 0) { + size_t total_len = (size_t)hn + (size_t)rn; + if (total_len <= sizeof(dgbuf) - LWS_PRE) { + enum lws_write_protocol wp = LWS_WRITE_QUIC_DATAGRAM; + lws_write(wsi, &dgbuf[LWS_PRE], total_len, wp); + lwsl_user("Session WSI sent datagram PUSH: %s (%zu bytes)\n", dg->filename, total_len); + usleep(5000); /* Pace datagram sends to prevent queue overflow */ + + /* Mark completed on client side (if we are the client sending PUSH) */ + if (!is_server) { + int r_idx = -1; + int i; + for (i = 0; i < client_requests_count; i++) { + if (strcmp(client_requests[i].filename, dg->filename) == 0) { + r_idx = i; + break; + } + } + if (r_idx >= 0) { + client_requests[r_idx].completed = 1; + lwsl_user("Client datagram transfer completed: %s\n", dg->filename); + } + } + } + } + } + } + close(fd); + } else { + lwsl_err("Failed to open file %s for datagram PUSH\n", filepath); + } + } else { + /* Send GET as a datagram */ + uint8_t buf[LWS_PRE + 512]; + size_t len = (size_t)lws_snprintf((char *)&buf[LWS_PRE], 512, "GET %s", dg->filename); + enum lws_write_protocol wp = LWS_WRITE_QUIC_DATAGRAM; + lws_write(wsi, &buf[LWS_PRE], len, wp); + lwsl_user("Session WSI sent datagram GET: %s\n", dg->filename); + usleep(5000); /* Pace datagram sends to prevent queue overflow */ + } + /* Request next writable callback to process remaining queue items */ + lws_callback_on_writable(wsi); + } + break; + } + + if (pss_is_file_sender(pss)) { + /* Sender: write file data */ + if (!pss->filename[0] || pss->write_completed) + break; + if (pss->fd_in < 0) { + char filepath[512]; + const char *endpoint = pss->endpoint[0] ? pss->endpoint : global_endpoint; + lws_snprintf(filepath, sizeof(filepath), "/www%s/%s", endpoint, pss->filename); + pss->fd_in = open(filepath, O_RDONLY); + if (pss->fd_in >= 0) { + struct stat st; + if (fstat(pss->fd_in, &st) == 0) { + pss->file_len = (size_t)st.st_size; + } + pss->sent_len = 0; + pss->header_sent = 0; + lwsl_user("Sender opened file %s (%zu bytes) for transmission\n", filepath, pss->file_len); + } else { + lwsl_err("Sender failed to open file %s\n", filepath); + return -1; + } + } + + if (pss->fd_in >= 0) { + if (!pss->is_unidi || pss->header_sent) { + /* Read and send file chunk */ + p = &buf[LWS_PRE]; + n = (int)read(pss->fd_in, p, sizeof(buf) - LWS_PRE); + if (n > 0) { + pss->sent_len += (size_t)n; + int is_final = (pss->sent_len == pss->file_len); + m = lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY | (is_final ? LWS_WRITE_H2_STREAM_END : LWS_WRITE_NO_FIN)); + if (m < 0) + return -1; + if (m < n) { + /* Seek back the unwritten bytes and retry */ + off_t diff = (off_t)(n - m); + if (lseek(pss->fd_in, -diff, SEEK_CUR) == (off_t)-1) { + lwsl_err("lseek failed: %d\n", errno); + return -1; + } + pss->sent_len -= (size_t)diff; + lws_callback_on_writable(wsi); + } else { + if (!is_final) + lws_callback_on_writable(wsi); + else { + close(pss->fd_in); + pss->fd_in = -1; + pss->write_completed = 1; + lwsl_user("Sender completed file write: %zu bytes\n", pss->sent_len); + return 0; + } + } + } else { + /* EOF or error */ + close(pss->fd_in); + pss->fd_in = -1; + pss->write_completed = 1; + lws_write(wsi, NULL, 0, LWS_WRITE_BINARY | LWS_WRITE_H2_STREAM_END); + return 0; + } + } else { + /* Unidirectional stream requires PUSH \n first */ + p = &buf[LWS_PRE]; + n = lws_snprintf((char *)p, sizeof(buf) - LWS_PRE, "PUSH %s\n", pss->filename); + m = lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY | LWS_WRITE_NO_FIN); + if (m < 0) + return -1; + if (m < n) { + /* Throttled or partial, retry header next time */ + lws_callback_on_writable(wsi); + } else { + pss->header_sent = 1; + lws_callback_on_writable(wsi); + lwsl_user("Sender sent PUSH %s\\n\n", pss->filename); + } + } + } + } else { + if (pss->is_initiator) { + /* Initiator: write GET */ + if (!pss->header_sent) { + p = &buf[LWS_PRE]; + n = lws_snprintf((char *)p, sizeof(buf) - LWS_PRE, "GET %s", pss->filename); + m = lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY | LWS_WRITE_H2_STREAM_END); + if (m < 0) + return -1; + pss->header_sent = 1; + lwsl_user("Initiator sent GET %s\n", pss->filename); + } + } + } + break; + + case LWS_CALLBACK_RECEIVE: + if (pss->is_session) { + /* Datagram received! format: GET or PUSH \n */ + char *payload = (char *)in; + size_t payload_len = len; + + if (payload_len > 4 && strncmp(payload, "GET ", 4) == 0) { + /* GET */ + char filename[256]; + size_t fn_len = payload_len - 4; + if (fn_len >= sizeof(filename)) fn_len = sizeof(filename) - 1; + memcpy(filename, payload + 4, fn_len); + filename[fn_len] = '\0'; + + lwsl_user("Session WSI received datagram GET: %s\n", filename); + + /* Respond by sending PUSH \n as a datagram */ + enqueue_datagram(filename, 1); /* 1 for PUSH */ + lws_callback_on_writable(wsi); + } else if (payload_len > 5 && strncmp(payload, "PUSH ", 5) == 0) { + /* PUSH \n */ + char *newline = memchr(payload, '\n', payload_len); + if (newline) { + char filename[256]; + size_t fn_len = (size_t)(newline - (payload + 5)); + if (fn_len >= sizeof(filename)) fn_len = sizeof(filename) - 1; + memcpy(filename, payload + 5, fn_len); + filename[fn_len] = '\0'; + + char *data_start = newline + 1; + size_t data_len = payload_len - (size_t)(data_start - payload); + + lwsl_user("Session WSI received datagram PUSH: %s (%zu bytes)\n", filename, data_len); + + char dirpath[512], filepath[512]; + lws_snprintf(dirpath, sizeof(dirpath), "/downloads%s", pss->endpoint); + if (mkdir(dirpath, 0777) < 0 && errno != EEXIST) { // NOSONAR + lwsl_err("Failed to create directory %s: %d\n", dirpath, errno); + } + lws_snprintf(filepath, sizeof(filepath), "%s/%s", dirpath, filename); + + int fd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0666); // NOSONAR + if (fd >= 0) { + if (write(fd, data_start, LWS_POSIX_LENGTH_CAST(data_len)) < 0) { + lwsl_err("Failed to write datagram content\n"); + } + close(fd); + + /* Mark request completed for datagram-receive */ + if (!is_server) { + int r_idx = -1; + int i; + for (i = 0; i < client_requests_count; i++) { + if (strcmp(client_requests[i].filename, filename) == 0) { + r_idx = i; + break; + } + } + if (r_idx >= 0) { + client_requests[r_idx].completed = 1; + lwsl_user("Client datagram request index %d (%s) completed (datagram received)\n", r_idx, filename); + } + } else { + int r_idx = -1; + int i; + for (i = 0; i < server_requests_count; i++) { + if (strcmp(server_requests[i].filename, filename) == 0) { + r_idx = i; + break; + } + } + if (r_idx >= 0) { + server_requests[r_idx].completed = 1; + lwsl_user("Server datagram request index %d (%s) completed (datagram received)\n", r_idx, filename); + + int all_done = 1; + for (i = 0; i < server_requests_count; i++) { + if (!server_requests[i].completed) { + all_done = 0; + break; + } + } + if (all_done) { + lwsl_user("All server requests completed. Setting interrupted = 1 to exit.\n"); + interrupted = 1; + } + } + } + } + } + } + break; + } + + /* Child Stream Data Received */ + { + char *data = (char *)in; + size_t data_len = len; + int is_file_rec = pss_is_file_receiver(pss); + + lwsl_user("RECEIVE: wsi=%p, len=%zu, is_unidi=%d, is_initiator=%d, push_hdr_done=%d, is_file_rec=%d\n", + wsi, data_len, pss->is_unidi, pss->is_initiator, pss->push_hdr_done, is_file_rec); + + if (is_file_rec) { + /* Receiver of file */ + if (pss->is_unidi) { + /* Unidirectional: parse PUSH \n first */ + if (!pss->push_hdr_done) { + size_t i; + for (i = 0; i < data_len; i++) { + if (pss->push_hdr_len < sizeof(pss->push_hdr) - 1) { + pss->push_hdr[pss->push_hdr_len++] = data[i]; + if (data[i] == '\n') { + pss->push_hdr[pss->push_hdr_len] = '\0'; + pss->push_hdr_done = 1; + /* Parse filename */ + if (strncmp(pss->push_hdr, "PUSH ", 5) == 0) { + char *nl = strchr(pss->push_hdr, '\n'); + if (nl) *nl = '\0'; + lws_strncpy(pss->filename, pss->push_hdr + 5, sizeof(pss->filename)); + lwsl_user("RECEIVE: Parsed filename '%s'\n", pss->filename); + } + /* Open file regardless of rem */ + char dirpath[512], filepath[512]; + const char *endpoint = pss->endpoint[0] ? pss->endpoint : global_endpoint; + lws_snprintf(dirpath, sizeof(dirpath), "/downloads%s", endpoint); + if (mkdir(dirpath, 0777) < 0 && errno != EEXIST) { // NOSONAR + lwsl_err("Failed to create directory %s: %d\n", dirpath, errno); + } + lws_snprintf(filepath, sizeof(filepath), "%s/%s", dirpath, pss->filename); + pss->fd_out = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0666); // NOSONAR + + size_t rem = data_len - i - 1; + lwsl_user("RECEIVE: Opened file '%s' -> fd %d (rem=%zu bytes written)\n", filepath, pss->fd_out, rem); + if (pss->fd_out >= 0 && rem > 0) { + if (write(pss->fd_out, data + i + 1, LWS_POSIX_LENGTH_CAST(rem)) < 0) { + lwsl_err("Failed to write stream chunk\n"); + } + } + break; + } + } + } + } else { + if (pss->fd_out >= 0) { + if (write(pss->fd_out, data, LWS_POSIX_LENGTH_CAST(data_len)) < 0) { + lwsl_err("Failed to write stream data\n"); + } + } else { + lwsl_user("RECEIVE: fd_out is closed/invalid (%d) while trying to write %zu bytes\n", pss->fd_out, data_len); + } + } + } else { + /* Bidirectional: no header, just raw file contents */ + if (pss->fd_out < 0) { + char dirpath[512], filepath[512]; + const char *endpoint = pss->endpoint[0] ? pss->endpoint : global_endpoint; + lws_snprintf(dirpath, sizeof(dirpath), "/downloads%s", endpoint); + if (mkdir(dirpath, 0777) < 0 && errno != EEXIST) { // NOSONAR + lwsl_err("Failed to create directory %s: %d\n", dirpath, errno); + } + lws_snprintf(filepath, sizeof(filepath), "%s/%s", dirpath, pss->filename); + pss->fd_out = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0666); // NOSONAR + lwsl_user("RECEIVE (bidi): Opened file '%s' -> fd %d\n", filepath, pss->fd_out); + } + if (pss->fd_out >= 0) { + if (write(pss->fd_out, data, LWS_POSIX_LENGTH_CAST(data_len)) < 0) { + lwsl_err("Failed to write stream data\n"); + } + } + } + } else { + /* Sender (received GET ) */ + if (data_len > 4 && strncmp(data, "GET ", 4) == 0) { + char filename[256]; + size_t fn_len = data_len - 4; + /* Remove trailing spaces or newlines if any */ + while (fn_len > 0 && (data[4 + fn_len - 1] == ' ' || data[4 + fn_len - 1] == '\r' || data[4 + fn_len - 1] == '\n')) + fn_len--; + if (fn_len >= sizeof(filename)) fn_len = sizeof(filename) - 1; + memcpy(filename, data + 4, fn_len); + filename[fn_len] = '\0'; + + lws_strncpy(pss->filename, filename, sizeof(pss->filename)); + lwsl_user("Sender WSI received GET %s\n", pss->filename); + + /* Open local file from /www// */ + char filepath[512]; + const char *endpoint = pss->endpoint[0] ? pss->endpoint : global_endpoint; + lws_snprintf(filepath, sizeof(filepath), "/www%s/%s", endpoint, pss->filename); + pss->fd_in = open(filepath, O_RDONLY); + if (pss->fd_in >= 0) { + struct stat st; + if (fstat(pss->fd_in, &st) == 0) { + pss->file_len = (size_t)st.st_size; + } + + if (lws_wt_is_unidi(wsi)) { + struct lws *cwsi = lws_wt_create_stream_from_child(wsi, 1); + if (cwsi) { + if (!lws_ensure_user_space(cwsi)) { + struct pss_qir *cpss = (struct pss_qir *)lws_wsi_user(cwsi); + if (cpss) { + cpss->is_unidi = 1; + cpss->is_initiator = 0; + lws_strncpy(cpss->endpoint, endpoint, sizeof(cpss->endpoint)); + lws_strncpy(cpss->filename, filename, sizeof(cpss->filename)); + cpss->fd_in = pss->fd_in; + pss->fd_in = -1; + cpss->file_len = pss->file_len; + lws_callback_on_writable(cwsi); + lwsl_user("Created server-initiated stream %p for unidirectional file response\n", cwsi); + } else { + lwsl_err("Child stream user space is NULL\n"); + close(pss->fd_in); + pss->fd_in = -1; + } + } else { + lwsl_err("Failed to ensure user space for child stream\n"); + close(pss->fd_in); + pss->fd_in = -1; + } + } else { + lwsl_err("Failed to create server-initiated stream for response\n"); + close(pss->fd_in); + pss->fd_in = -1; + } + } else { + lws_callback_on_writable(wsi); + } + } else { + lwsl_err("Sender WSI failed to open file %s\n", filepath); + } + } + } + } + break; + + case LWS_CALLBACK_CLOSED: + case LWS_CALLBACK_CLIENT_CLOSED: + case LWS_CALLBACK_CLOSED_CLIENT_HTTP: + if (pss->fd_in >= 0) { + close(pss->fd_in); + pss->fd_in = -1; + } + if (pss->fd_out >= 0) { + close(pss->fd_out); + pss->fd_out = -1; + } + if (!pss->is_session) { + if (pss->is_unidi && pss->is_initiator) { + lwsl_user("Initiator unidirectional GET stream closed (not marking request completed yet)\n"); + break; + } + if (!is_server) { + int r_idx = -1; + int i; + for (i = 0; i < client_requests_count; i++) { + if (strcmp(client_requests[i].filename, pss->filename) == 0) { + r_idx = i; + break; + } + } + if (r_idx >= 0) { + client_requests[r_idx].completed = 1; + lwsl_user("Client transfer request index %d (%s) completed (stream closed)\n", r_idx, pss->filename); + } + } else { + int r_idx = -1; + int i; + for (i = 0; i < server_requests_count; i++) { + if (strcmp(server_requests[i].filename, pss->filename) == 0) { + r_idx = i; + break; + } + } + if (r_idx >= 0) { + server_requests[r_idx].completed = 1; + lwsl_user("Server transfer request index %d (%s) completed (stream closed)\n", r_idx, pss->filename); + + int all_done = 1; + for (i = 0; i < server_requests_count; i++) { + if (!server_requests[i].completed) { + all_done = 0; + break; + } + } + if (all_done) { + lwsl_user("All server requests completed. Setting interrupted = 1 to exit.\n"); + interrupted = 1; + } + } + } + } else { + lwsl_user("%s session closed. Setting interrupted = 1 to exit.\n", is_server ? "Server" : "Client"); + interrupted = 1; + } + lwsl_user("WSI closed\n"); + break; + + default: + break; + } + + return 0; +} + +static const struct lws_protocols protocols[] = { + { "webtransport", callback_qir, sizeof(struct pss_qir), 65536, 0, NULL, 0 }, + { NULL, NULL, 0, 0, 0, NULL, 0 } +}; + +static struct lws_protocols *dyn_protocols = NULL; + +static void setup_dynamic_protocols(void) +{ + const char *env_protocols = getenv("PROTOCOLS_SERVER"); + if (!env_protocols) + env_protocols = getenv("PROTOCOLS"); + + if (!env_protocols) + return; + + /* Count protocols */ + struct lws_tokenize ts; + lws_tokenize_init(&ts, env_protocols, LWS_TOKENIZE_F_MINUS_NONTERM); + lws_tokenize_elem e; + int count = 0; + + do { + e = lws_tokenize(&ts); + if (e == LWS_TOKZE_TOKEN || e == LWS_TOKZE_QUOTED_STRING) { + count++; + } + } while (e > 0); + + if (count == 0) + return; + + /* Allocate count + 2 protocols: one for each protocol, plus "webtransport", plus NULL terminator */ + dyn_protocols = malloc((size_t)(count + 2) * sizeof(struct lws_protocols)); + if (!dyn_protocols) { + lwsl_err("OOM allocating dynamic protocols\n"); + return; + } + + memset(dyn_protocols, 0, (size_t)(count + 2) * sizeof(struct lws_protocols)); + + /* Re-tokenize and copy */ + lws_tokenize_init(&ts, env_protocols, LWS_TOKENIZE_F_MINUS_NONTERM); + int idx = 0; + do { + e = lws_tokenize(&ts); + if (e == LWS_TOKZE_TOKEN || e == LWS_TOKZE_QUOTED_STRING) { + char name[64]; + if (!lws_tokenize_cstr(&ts, name, sizeof(name))) { + dyn_protocols[idx].name = strdup(name); + dyn_protocols[idx].callback = callback_qir; + dyn_protocols[idx].per_session_data_size = sizeof(struct pss_qir); + dyn_protocols[idx].rx_buffer_size = 65536; + idx++; + } + } + } while (e > 0); + + /* Add generic "webtransport" protocol at the end of the list just in case */ + dyn_protocols[idx].name = "webtransport"; + dyn_protocols[idx].callback = callback_qir; + dyn_protocols[idx].per_session_data_size = sizeof(struct pss_qir); + dyn_protocols[idx].rx_buffer_size = 65536; + idx++; + + /* Terminator is already zeroed out by memset */ +} + +int main(int argc, const char **argv) +{ + struct lws_context_creation_info info; + const char *p; + int n = 0; + int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + + signal(SIGINT, sigint_handler); + + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + + /* Determine role */ + if (argc > 1 && strcmp(argv[1], "server") == 0) + is_server = 1; + else + is_server = 0; + + /* Read testcase env */ + const char *tc = getenv("TESTCASE_NAME"); + if (!tc) + tc = getenv("TESTCASE"); + if (tc) + lws_strncpy(testcase, tc, sizeof(testcase)); + + lwsl_user("LWS WebTransport QIR Tool | Role: %s | Testcase: %s\n", + is_server ? "server" : "client", testcase); + + /* Determine port */ + int port = 443; + const char *port_env = getenv("PORT"); + if (port_env) + port = atoi(port_env); + + setup_dynamic_protocols(); + + memset(&info, 0, sizeof info); + info.port = is_server ? port : CONTEXT_PORT_NO_LISTEN; + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + info.protocols = dyn_protocols ? dyn_protocols : protocols; + info.alpn = "h3"; + + if (is_server) { + static char cert_path[512]; + static char key_path[512]; + int fd_check; + + /* The interop runner mounts host certificates at /certs inside the container */ + fd_check = open("/certs/cert.pem", O_RDONLY); + if (fd_check >= 0) { + close(fd_check); + lws_strncpy(cert_path, "/certs/cert.pem", sizeof(cert_path)); + lws_strncpy(key_path, "/certs/priv.key", sizeof(key_path)); + } else { + /* Fallback to local files if running outside QIR simulation */ + lws_strncpy(cert_path, "localhost-100y.cert", sizeof(cert_path)); + lws_strncpy(key_path, "localhost-100y.key", sizeof(key_path)); + } + + info.ssl_cert_filepath = cert_path; + info.ssl_private_key_filepath = key_path; + parse_server_requests(); + } else { + parse_client_requests(); + } + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + /* Client connection triggering */ + if (!is_server && client_requests_count > 0) { + int i; + for (i = 0; i < client_requests_count; i++) { + struct request_item *item = &client_requests[i]; + + /* Connect only once per unique host/port/endpoint */ + int already_triggered = 0; + int j; + for (j = 0; j < i; j++) { + if (strcmp(client_requests[j].endpoint, item->endpoint) == 0 && + strcmp(client_requests[j].host, item->host) == 0 && + client_requests[j].port == item->port) { + already_triggered = 1; + break; + } + } + if (already_triggered) + continue; + + struct lws_client_connect_info cinfo; + + memset(&cinfo, 0, sizeof(cinfo)); + cinfo.context = context; + cinfo.address = item->host; + cinfo.port = item->port; + cinfo.path = item->endpoint; + cinfo.host = item->host; + cinfo.origin = item->host; + cinfo.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + cinfo.protocol = "webtransport"; + cinfo.alpn = "h3"; + + if (first_session_wsi) { + /* Force multiplexing on the same QUIC network connection */ + cinfo.parent_wsi = first_session_wsi; + } + + struct lws *wsi = lws_client_connect_via_info(&cinfo); + if (!wsi) { + lwsl_err("Failed to initiate WebTransport client connection to %s\n", item->url); + } else { + if (!first_session_wsi) { + first_session_wsi = wsi; + } + } + } + } + + while (n >= 0 && !interrupted) { + n = lws_service(context, 0); + + /* Check if all client requests are done and exit */ + if (!is_server && client_requests_count > 0) { + int all_done = 1; + int i; + for (i = 0; i < client_requests_count; i++) { + if (!client_requests[i].completed) + all_done = 0; + } + if (all_done && strcmp(testcase, "handshake") != 0) { + lwsl_user("All client requests completed. Waiting 500ms before exiting.\n"); + usleep(500000); + break; + } + /* Handshake testcase does not download files, just wait a bit and exit */ + if (strcmp(testcase, "handshake") == 0) { + char client_proto_path[512]; + lws_snprintf(client_proto_path, sizeof(client_proto_path), "/downloads/negotiated_protocol.txt"); + int fd = open(client_proto_path, O_RDONLY); + if (fd >= 0) { + close(fd); + lwsl_user("Handshake verification file found, client exiting successfully.\n"); + break; + } + } + } + } + + lws_context_destroy(context); + + if (dyn_protocols) { + int idx = 0; + while (dyn_protocols[idx].name) { + if (strcmp(dyn_protocols[idx].name, "webtransport") != 0) { + free((void *)dyn_protocols[idx].name); + } + idx++; + } + free(dyn_protocols); + } + + return 0; +} diff --git a/minimal-examples-lowlevel/webtransport/minimal-webtransport-test-server/minimal-webtransport-test-server.c b/minimal-examples-lowlevel/webtransport/minimal-webtransport-test-server/minimal-webtransport-test-server.c deleted file mode 100644 index 0471f5b1f9..0000000000 --- a/minimal-examples-lowlevel/webtransport/minimal-webtransport-test-server/minimal-webtransport-test-server.c +++ /dev/null @@ -1,78 +0,0 @@ -/* - * lws-minimal-webtransport-test-server - * - * Written in 2010-2026 by Andy Green - * - * This file is made available under the Creative Commons CC0 1.0 - * Universal Public Domain Dedication. - * - * This server mounts the assets for the WebTransport test UI and loads the - * `protocol_webtransport_test` plugin. - */ - -#include -#include -#include - -static int interrupted; - -static const struct lws_http_mount mount = { - .mount_next = NULL, - .mountpoint = "/", - .origin = "../../../../plugins/protocol_webtransport_test/assets", - .def = "index.html", - .origin_protocol = LWSMPRO_FILE, - .mountpoint_len = 1, -}; - -extern const lws_plugin_protocol_t webtransport_test; - -static void -sigint_handler(int sig) -{ - interrupted = 1; -} - -int main(int argc, const char **argv) -{ - struct lws_context_creation_info info; - struct lws_context *context; - const char *p; - int n = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; - - signal(SIGINT, sigint_handler); - - if ((p = lws_cmdline_option(argc, argv, "-d"))) - logs = atoi(p); - - lws_set_log_level(logs, NULL); - lwsl_user("LWS minimal webtransport test server | visit https://localhost:7681\n"); - - memset(&info, 0, sizeof info); - info.port = 7681; - info.mounts = &mount; - info.error_document_404 = "/404.html"; - info.options = - LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | - LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE; - - info.pvo = NULL; - info.protocols = webtransport_test.protocols; - - info.ssl_cert_filepath = "localhost-100y.cert"; - info.ssl_private_key_filepath = "localhost-100y.key"; - info.alpn = "h3,h2,http/1.1"; - - context = lws_create_context(&info); - if (!context) { - lwsl_err("lws init failed\n"); - return 1; - } - - while (n >= 0 && !interrupted) - n = lws_service(context, 0); - - lws_context_destroy(context); - - return 0; -} diff --git a/minimal-examples-lowlevel/ws-client/minimal-ws-client-tx/minimal-ws-client.c b/minimal-examples-lowlevel/ws-client/minimal-ws-client-tx/minimal-ws-client.c index 40561af164..6345cd4443 100644 --- a/minimal-examples-lowlevel/ws-client/minimal-ws-client-tx/minimal-ws-client.c +++ b/minimal-examples-lowlevel/ws-client/minimal-ws-client-tx/minimal-ws-client.c @@ -185,6 +185,8 @@ callback_minimal_broker(struct lws *wsi, enum lws_callback_reasons reason, vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct per_vhost_data__minimal)); + if (!vhd) + return 1; vhd->context = lws_get_context(wsi); vhd->protocol = lws_get_protocol(wsi); vhd->vhost = lws_get_vhost(wsi); diff --git a/minimal-examples-lowlevel/ws-server/minimal-ws-server-pmd/protocol_lws_minimal.c b/minimal-examples-lowlevel/ws-server/minimal-ws-server-pmd/protocol_lws_minimal.c index 00287d7235..bcf3c5c042 100644 --- a/minimal-examples-lowlevel/ws-server/minimal-ws-server-pmd/protocol_lws_minimal.c +++ b/minimal-examples-lowlevel/ws-server/minimal-ws-server-pmd/protocol_lws_minimal.c @@ -76,6 +76,8 @@ callback_minimal(struct lws *wsi, enum lws_callback_reasons reason, vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct per_vhost_data__minimal)); + if (!vhd) + return 1; vhd->context = lws_get_context(wsi); vhd->protocol = lws_get_protocol(wsi); vhd->vhost = lws_get_vhost(wsi); diff --git a/plugins/protocol_deaddrop/protocol_lws_deaddrop.c b/plugins/protocol_deaddrop/protocol_lws_deaddrop.c index 4795741363..09b420dc4c 100644 --- a/plugins/protocol_deaddrop/protocol_lws_deaddrop.c +++ b/plugins/protocol_deaddrop/protocol_lws_deaddrop.c @@ -818,7 +818,7 @@ static void deaddrop_handler_server_ws_rx(struct vhd_deaddrop *vhd, struct pss_deaddrop *pss, struct lws *wsi, void *in, size_t len) { -#if defined(__linux__) +#if !defined(WIN32) && !defined(_WIN32) && !defined(LWS_WITH_ESP32) char path[512], resolved_path[PATH_MAX]; #else char path[512]; @@ -862,7 +862,7 @@ deaddrop_handler_server_ws_rx(struct vhd_deaddrop *vhd, struct pss_deaddrop *pss lws_snprintf(path, sizeof(path), "%s/%s", vhd->upload_dir, fname); -#if defined(__linux__) +#if !defined(WIN32) && !defined(_WIN32) && !defined(LWS_WITH_ESP32) if (!realpath(path, resolved_path)) { lwsl_wsi_warn(wsi, "delete: realpath failed %s", path); return; diff --git a/plugins/protocol_lws_acme_client/protocol_lws_acme_client.c b/plugins/protocol_lws_acme_client/protocol_lws_acme_client.c index 5c3c3f6028..6b5931722b 100644 --- a/plugins/protocol_lws_acme_client/protocol_lws_acme_client.c +++ b/plugins/protocol_lws_acme_client/protocol_lws_acme_client.c @@ -986,7 +986,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, ac->state = ACME_STATE_NEW_ACCOUNT; lws_acme_report_status(vhd->vhost, LWS_CUS_REG, NULL); - strcpy(buf, ac->urls[JAD_NEW_ACCOUNT_URL]); + lws_strncpy(buf, ac->urls[JAD_NEW_ACCOUNT_URL], sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); if (!cwsi) { @@ -1271,7 +1271,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, ac->state = ACME_STATE_NEW_NONCE; - strcpy(buf, ac->urls[JAD_NEW_NONCE_URL]); + lws_strncpy(buf, ac->urls[JAD_NEW_NONCE_URL], sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "GET"); @@ -1291,7 +1291,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, */ ac->state = ACME_STATE_NEW_ORDER; - strcpy(buf, ac->urls[JAD_NEW_ORDER_URL]); + lws_strncpy(buf, ac->urls[JAD_NEW_ORDER_URL], sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1322,7 +1322,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, lws_acme_report_status(vhd->vhost, LWS_CUS_AUTH, NULL); - strcpy(buf, ac->authz_url); + lws_strncpy(buf, ac->authz_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1428,7 +1428,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, goto failed; } - strcpy(buf, ac->order_url); + lws_strncpy(buf, ac->order_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1477,7 +1477,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, lws_acme_report_status(vhd->vhost, LWS_CUS_REQ, NULL); ac->goes_around = 0; - strcpy(buf, ac->finalize_url); + lws_strncpy(buf, ac->finalize_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1501,7 +1501,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, goto failed; } - strcpy(buf, ac->order_url); + lws_strncpy(buf, ac->order_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, @@ -1518,7 +1518,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, ac->state = ACME_STATE_DOWNLOAD_CERT; - strcpy(buf, ac->cert_url); + lws_strncpy(buf, ac->cert_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); diff --git a/plugins/protocol_lws_acme_client/protocol_lws_acme_client_core.c b/plugins/protocol_lws_acme_client/protocol_lws_acme_client_core.c index 14de99d32f..81d92cac84 100644 --- a/plugins/protocol_lws_acme_client/protocol_lws_acme_client_core.c +++ b/plugins/protocol_lws_acme_client/protocol_lws_acme_client_core.c @@ -1403,7 +1403,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, ac->state = ACME_STATE_NEW_ACCOUNT; lws_acme_report_status(vhd->vhost, LWS_CUS_REG, NULL); - strcpy(buf, ac->urls[JAD_NEW_ACCOUNT_URL]); + lws_strncpy(buf, ac->urls[JAD_NEW_ACCOUNT_URL], sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); if (!cwsi) { @@ -1707,7 +1707,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, ac->state = ACME_STATE_NEW_NONCE; - strcpy(buf, ac->urls[JAD_NEW_NONCE_URL]); + lws_strncpy(buf, ac->urls[JAD_NEW_NONCE_URL], sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "GET"); @@ -1727,7 +1727,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, */ ac->state = ACME_STATE_NEW_ORDER; - strcpy(buf, ac->urls[JAD_NEW_ORDER_URL]); + lws_strncpy(buf, ac->urls[JAD_NEW_ORDER_URL], sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1758,7 +1758,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, lws_acme_report_status(vhd->vhost, LWS_CUS_AUTH, NULL); - strcpy(buf, ac->authz_url); + lws_strncpy(buf, ac->authz_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1883,7 +1883,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, goto failed; } - strcpy(buf, ac->order_url); + lws_strncpy(buf, ac->order_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1935,7 +1935,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, lws_acme_report_status(vhd->vhost, LWS_CUS_REQ, NULL); ac->goes_around = 0; - strcpy(buf, ac->finalize_url); + lws_strncpy(buf, ac->finalize_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -1959,7 +1959,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, goto failed; } - strcpy(buf, ac->order_url); + lws_strncpy(buf, ac->order_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, @@ -1976,7 +1976,7 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, ac->state = ACME_STATE_DOWNLOAD_CERT; - strcpy(buf, ac->cert_url); + lws_strncpy(buf, ac->cert_url, sizeof(buf)); cwsi = lws_acme_client_connect(vhd->context, vhd->vhost, &ac->cwsi, &ac->i, buf, "POST"); @@ -2028,8 +2028,17 @@ callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason, } time(&t); - tm = localtime(&t); - strftime(timebuf, sizeof(timebuf), "%Y%m%d-%H%M%S", tm); +#if defined(WIN32) || defined(_WIN32) + struct tm tmp; + tm = localtime_s(&tmp, &t) == 0 ? &tmp : NULL; +#else + struct tm tmp; + tm = localtime_r(&t, &tmp); +#endif + if (tm) + strftime(timebuf, sizeof(timebuf), "%Y%m%d-%H%M%S", tm); + else + timebuf[0] = '\0'; lws_strncpy(cert_ts, cert_latest, sizeof(cert_ts)); p = (char *)strstr(cert_ts, "-latest.crt"); diff --git a/plugins/protocol_lws_auth_dns/protocol_lws_auth_dns.c b/plugins/protocol_lws_auth_dns/protocol_lws_auth_dns.c index b475502e52..7fe95e0bba 100644 --- a/plugins/protocol_lws_auth_dns/protocol_lws_auth_dns.c +++ b/plugins/protocol_lws_auth_dns/protocol_lws_auth_dns.c @@ -1033,6 +1033,7 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, uint8_t *dbuf = pss->buf + LWS_PRE; uint8_t *rp = dbuf; + size_t max_buf = is_tcp ? (sizeof(pss->buf) - LWS_PRE) : (udp_payload_size < sizeof(pss->buf) - LWS_PRE ? udp_payload_size : sizeof(pss->buf) - LWS_PRE); if (is_tcp) rp += 2; rp[0] = (uint8_t)(id >> 8); rp[1] = (uint8_t)(id & 0xff); @@ -1219,7 +1220,6 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, } } lws_end_foreach_dll(cd); - size_t max_buf = is_tcp ? (sizeof(pss->buf) - LWS_PRE) : (udp_payload_size < sizeof(pss->buf) - LWS_PRE ? udp_payload_size : sizeof(pss->buf) - LWS_PRE); rp[2] = (uint8_t)(rflags >> 8); rp[3] = (uint8_t)(rflags & 0xff); rp[4] = 0; rp[5] = 1; /* QDCOUNT = 1 */ @@ -1232,6 +1232,10 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, rp += 12; int qlen = (int)(q - (p + 12)); + if ((size_t)(rp - dbuf) + 12 + (size_t)qlen > max_buf) { + lwsl_notice("DNS: response too large for buffer\n"); + goto done; + } memcpy(rp, p + 12, (size_t)qlen); rp += qlen; @@ -1276,8 +1280,9 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, struct auth_dns_rr *rr = lws_container_of(d2, struct auth_dns_rr, list); size_t nlen = strlen(rs->name); if ((size_t)(rp - dbuf) + 12 + nlen + 1 + rr->wire_rdata_len <= max_buf) { - if (name_to_wire(rs->name, matched_ce->zone.origin, rp, &max_buf) == 0) { - size_t written_len = strlen((char *)rp) + 1; /* Name length including root dot */ + size_t rem = max_buf - (size_t)(rp - dbuf); + if (name_to_wire(rs->name, matched_ce->zone.origin, rp, &rem) == 0) { + size_t written_len = rem; /* Name length including root dot */ rp += written_len; *rp++ = (uint8_t)(rs->type >> 8); *rp++ = (uint8_t)(rs->type & 0xff); *rp++ = (uint8_t)(rs->class_ >> 8); *rp++ = (uint8_t)(rs->class_ & 0xff); @@ -1310,6 +1315,10 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, rp[10] = 0; rp[11] = 0; /* ARCOUNT = 0 */ rp += 12; int qlen = (int)(q - (p + 12)); + if ((size_t)(rp - dbuf) + 12 + (size_t)qlen > max_buf) { + lwsl_notice("DNS: response too large for buffer\n"); + goto done; + } memcpy(rp, p + 12, (size_t)qlen); rp += qlen; @@ -1317,7 +1326,6 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, ; } else { int anc = 0; - size_t max_buf = is_tcp ? (sizeof(pss->buf) - LWS_PRE) : (udp_payload_size < sizeof(pss->buf) - LWS_PRE ? udp_payload_size : sizeof(pss->buf) - LWS_PRE); size_t total_size = lws_ptr_diff_size_t(rp, dbuf) + 12 + (size_t)(q - (p + 12)); lws_start_foreach_dll(struct lws_dll2 *, d, lws_dll2_get_head(&found_rs->rr_list)) { struct auth_dns_rr *rr = lws_container_of(d, struct auth_dns_rr, list); @@ -1366,6 +1374,10 @@ callback_auth_dns(struct lws *wsi, enum lws_callback_reasons reason, void *user, rp[10] = (uint8_t)(added_opt >> 8); rp[11] = (uint8_t)(added_opt & 0xff); rp += 12; int qlen = (int)(q - (p + 12)); + if ((size_t)(rp - dbuf) + 12 + (size_t)qlen > max_buf) { + lwsl_notice("DNS: response too large for buffer\n"); + goto done; + } memcpy(rp, p + 12, (size_t)qlen); rp += qlen; diff --git a/plugins/protocol_lws_auth_server/protocol_lws_auth_server.c b/plugins/protocol_lws_auth_server/protocol_lws_auth_server.c index 143556f1e2..6bb37a75c2 100644 --- a/plugins/protocol_lws_auth_server/protocol_lws_auth_server.c +++ b/plugins/protocol_lws_auth_server/protocol_lws_auth_server.c @@ -2290,7 +2290,7 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, lws_get_urlarg_by_name_safe(wsi, "redirect_uri=", redirect_uri, sizeof(redirect_uri)); lws_urldecode(redirect_uri, redirect_uri, sizeof(redirect_uri)); - if (!redirect_uri[0]) + if (!redirect_uri[0] || !auth_verify_redirect_uri(vhd, NULL, redirect_uri)) lws_strncpy(redirect_uri, "/", sizeof(redirect_uri)); lwsl_notice("%s: Extracted redirect_uri: %s\n", __func__, redirect_uri); @@ -2334,8 +2334,17 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, char cookie_hdr3[256], cookie_hdr3_host[256]; char exp[64]; time_t t = 0; - struct tm *tm = gmtime(&t); - strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm); +#if defined(WIN32) || defined(_WIN32) + struct tm tmp; + struct tm *tm = gmtime_s(&tmp, &t) == 0 ? &tmp : NULL; +#else + struct tm tmp; + struct tm *tm = gmtime_r(&t, &tmp); +#endif + if (tm) + strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm); + else + exp[0] = '\0'; if (vhd->cookie_domain[0]) { lws_snprintf(cookie_hdr1, sizeof(cookie_hdr1), "%s=; Path=/; Domain=%s; Expires=%s; Max-Age=0; HttpOnly; SameSite=None; Secure", vhd->cookie_name, vhd->cookie_domain, exp); @@ -2433,10 +2442,11 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, } int logged_in = 0; + int is_admin = 0; int lacks_grant = 0; char sname[128] = {0}; char user_email[128] = {0}; - char grants[256] = {0}; + char grants[2048] = {0}; char *gp = grants, *gend = grants + sizeof(grants); char logs[LWS_SSO_MAX_COOKIE] = {0}; lws_get_urlarg_by_name_safe(wsi, "service_name=", sname, sizeof(sname)); @@ -2510,11 +2520,14 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, if (sqlite3_prepare_v2(vhd->db, "SELECT s.name, g.grant_level FROM grants g JOIN services s ON g.service_id = s.service_id WHERE g.uid = ?", -1, &stmt_u, NULL) == SQLITE_OK) { sqlite3_bind_int(stmt_u, 1, (int)suid); while (sqlite3_step(stmt_u) == SQLITE_ROW) { + const char *sname_db = (const char *)sqlite3_column_text(stmt_u, 0); + int gl = sqlite3_column_int(stmt_u, 1); + if (!strcmp(sname_db, "*") && gl >= 1) + is_admin = 1; + if (!first) gp += lws_snprintf(gp, lws_ptr_diff_size_t(gend, gp), ", "); first = 0; - gp += lws_snprintf(gp, lws_ptr_diff_size_t(gend, gp), "\"%s\": %d", - (const char *)sqlite3_column_text(stmt_u, 0), - sqlite3_column_int(stmt_u, 1)); + gp += lws_snprintf(gp, lws_ptr_diff_size_t(gend, gp), "\"%s\": %d", sname_db, gl); } sqlite3_finalize(stmt_u); } @@ -2559,8 +2572,17 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, char cookie_hdr1[256], cookie_hdr1_host[256], cookie_hdr2[256], cookie_hdr2_host[256], cookie_hdr3[256], cookie_hdr3_host[256]; char exp[64]; time_t t = 0; - struct tm *tm = gmtime(&t); - strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm); +#if defined(WIN32) || defined(_WIN32) + struct tm tmp; + struct tm *tm = gmtime_s(&tmp, &t) == 0 ? &tmp : NULL; +#else + struct tm tmp; + struct tm *tm = gmtime_r(&t, &tmp); +#endif + if (tm) + strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm); + else + exp[0] = '\0'; if (vhd->cookie_domain[0]) { lws_snprintf(cookie_hdr1, sizeof(cookie_hdr1), "%s=; Path=/; Domain=%s; Expires=%s; Max-Age=0; HttpOnly; SameSite=None; Secure", vhd->cookie_name, vhd->cookie_domain, exp); @@ -2624,8 +2646,8 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, pss->http_response_code = HTTP_STATUS_OK; char pl[LWS_SSO_MAX_COOKIE + LWS_PRE]; int len = lws_snprintf(pl + LWS_PRE, sizeof(pl) - LWS_PRE, - "{\"users_empty\":%d, \"csrf_token\":\"%s\", \"logged_in\":%d, \"lacks_grant\":%d, \"email\":\"%s\", \"strikes\":%d, \"grants\":{%s}, \"logs\":[%s]}", - users_empty, csrf, logged_in, lacks_grant, user_email, strikes, grants, logs); + "{\"users_empty\":%d, \"csrf_token\":\"%s\", \"logged_in\":%d, \"is_admin\":%d, \"lacks_grant\":%d, \"email\":\"%s\", \"strikes\":%d, \"grants\":{%s}, \"logs\":[%s]}", + users_empty, csrf, logged_in, is_admin, lacks_grant, user_email, strikes, grants, logs); if (lws_buflist_append_segment(&pss->tx_buflist, (uint8_t *)pl, (size_t)len + LWS_PRE) < 0) return -1; @@ -2848,15 +2870,21 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, /* Always add public grant to newly minted users */ sqlite3_exec(vhd->db, "INSERT OR IGNORE INTO services (name) VALUES ('public')", NULL, NULL, NULL); - char public_grant_query[256]; - lws_snprintf(public_grant_query, sizeof(public_grant_query), "INSERT INTO grants (uid, service_id, grant_level) VALUES ((SELECT uid FROM users WHERE username='%s'), (SELECT service_id FROM services WHERE name='public'), 1)", email); - sqlite3_exec(vhd->db, public_grant_query, NULL, NULL, NULL); + sqlite3_stmt *gstmt = NULL; + if (sqlite3_prepare_v2(vhd->db, "INSERT INTO grants (uid, service_id, grant_level) VALUES ((SELECT uid FROM users WHERE username=?), (SELECT service_id FROM services WHERE name='public'), 1)", -1, &gstmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(gstmt, 1, email, -1, SQLITE_TRANSIENT); + sqlite3_step(gstmt); + sqlite3_finalize(gstmt); + } if (users_count == 1) { sqlite3_exec(vhd->db, "INSERT OR IGNORE INTO services (service_id, name) VALUES (1, '*')", NULL, NULL, NULL); - char grant_query[256]; - lws_snprintf(grant_query, sizeof(grant_query), "INSERT INTO grants (uid, service_id, grant_level) VALUES ((SELECT uid FROM users WHERE username='%s'), 1, 2)", email); - sqlite3_exec(vhd->db, grant_query, NULL, NULL, NULL); + gstmt = NULL; + if (sqlite3_prepare_v2(vhd->db, "INSERT INTO grants (uid, service_id, grant_level) VALUES ((SELECT uid FROM users WHERE username=?), 1, 2)", -1, &gstmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(gstmt, 1, email, -1, SQLITE_TRANSIENT); + sqlite3_step(gstmt); + sqlite3_finalize(gstmt); + } } lws_snprintf(uri, sizeof(uri), "otpauth://totp/%s:%s?secret=%s&issuer=%s", @@ -3184,7 +3212,7 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, { char op[32] = {0}; int req_uid = -1; - char new_grants[512] = {0}; + char new_grants[2048] = {0}; char *gp; if ((gp = (char *)strstr((const char *)in, "\"op\":\""))) { @@ -3200,7 +3228,7 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, if ((gp = (char *)strstr((const char *)in, "\"grants\":\""))) { gp += 10; int i = 0; - while (*gp && *gp != '"' && i < 511) + while (*gp && *gp != '"' && i < (int)sizeof(new_grants) - 1) new_grants[i++] = *gp++; } @@ -3311,7 +3339,17 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, char *p2 = new_grants; while (*p2) { char *comma = (char *)strchr(p2, ','); - if (comma) *comma = '\0'; + char *semi = (char *)strchr(p2, ';'); + char *delim = NULL; + + if (comma && semi) + delim = (comma < semi) ? comma : semi; + else if (comma) + delim = comma; + else + delim = semi; + + if (delim) *delim = '\0'; char *colon = (char *)strchr(p2, ':'); if (colon) { *colon = '\0'; @@ -3334,8 +3372,8 @@ callback_auth_server(struct lws *wsi, enum lws_callback_reasons reason, } } } - if (!comma) break; - p2 = comma + 1; + if (!delim) break; + p2 = delim + 1; } sqlite3_exec(vhd->db, "COMMIT;", NULL, NULL, NULL); } diff --git a/plugins/protocol_lws_cert_dist_server/protocol_lws_cert_dist_server.c b/plugins/protocol_lws_cert_dist_server/protocol_lws_cert_dist_server.c index 99cbb7a58d..63d48a51f7 100644 --- a/plugins/protocol_lws_cert_dist_server/protocol_lws_cert_dist_server.c +++ b/plugins/protocol_lws_cert_dist_server/protocol_lws_cert_dist_server.c @@ -80,12 +80,15 @@ read_newest_file_in_dir(const char *dirpath, const char *suffix) lws_snprintf(path, sizeof(path), "%s/%s", dirpath, best_name); fd = open(path, O_RDONLY); if (fd >= 0) { - if (fstat(fd, &st) == 0 && (buf = malloc((size_t)st.st_size + 1))) { - if (read(fd, buf, (size_t)st.st_size) == (ssize_t)st.st_size) - buf[st.st_size] = '\0'; - else { - free(buf); - buf = NULL; + if (fstat(fd, &st) == 0) { + buf = malloc((size_t)st.st_size + 1); + if (buf) { + if (read(fd, buf, (size_t)st.st_size) == (ssize_t)st.st_size) + buf[st.st_size] = '\0'; + else { + free(buf); + buf = NULL; + } } } close(fd); diff --git a/plugins/protocol_lws_dht_dnssec/protocol_lws_dht_dnssec.c b/plugins/protocol_lws_dht_dnssec/protocol_lws_dht_dnssec.c index 09717c48c4..130dc84ffa 100644 --- a/plugins/protocol_lws_dht_dnssec/protocol_lws_dht_dnssec.c +++ b/plugins/protocol_lws_dht_dnssec/protocol_lws_dht_dnssec.c @@ -3794,9 +3794,18 @@ int lws_dht_dnssec_bump_zone_serial(struct lws_context *context, const char *fil old_serial[serial_len] = '\0'; time_t t = time(NULL); - struct tm *tm = gmtime(&t); +#if defined(WIN32) || defined(_WIN32) + struct tm tmp; + struct tm *tm = gmtime_s(&tmp, &t) == 0 ? &tmp : NULL; +#else + struct tm tmp; + struct tm *tm = gmtime_r(&t, &tmp); +#endif char new_date[16]; - lws_snprintf(new_date, sizeof(new_date), "%04d%02d%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + if (tm) + lws_snprintf(new_date, sizeof(new_date), "%04d%02d%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + else + new_date[0] = '\0'; char new_serial[32]; if (strncmp(old_serial, new_date, 8) == 0 && serial_len >= 10) { diff --git a/plugins/protocol_lws_hls/CMakeLists.txt b/plugins/protocol_lws_hls/CMakeLists.txt new file mode 100644 index 0000000000..368afe9ea0 --- /dev/null +++ b/plugins/protocol_lws_hls/CMakeLists.txt @@ -0,0 +1,53 @@ +# +# libwebsockets - small server side websockets and web server implementation +# +# Copyright (C) 2010 - 2026 Andy Green +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# + + find_package(PkgConfig QUIET) + pkg_check_modules(PC_AVFORMAT libavformat) + pkg_check_modules(PC_AVCODEC libavcodec) + pkg_check_modules(PC_AVUTIL libavutil) + pkg_check_modules(PC_SWSCALE libswscale) + + if (PC_AVFORMAT_FOUND AND PC_AVCODEC_FOUND AND PC_AVUTIL_FOUND AND PC_SWSCALE_FOUND) + set(PLUGIN_NAME "protocol_lws_hls") + set(PLUGIN_SRCS + protocol_lws_hls.c + hls-av.c + hls-dir.c + ) + set(PLUGIN_HDR private-lws-hls.h) + + # Create the plugin target using the macro from plugins/CMakeLists.txt + create_plugin(${PLUGIN_NAME} + "" + "${PLUGIN_SRCS}" + "" + "" + ) + + if (TARGET ${PLUGIN_NAME}) + target_compile_definitions(${PLUGIN_NAME} PRIVATE LWS_BUILDING_SHARED) + target_include_directories(${PLUGIN_NAME} PRIVATE + ${PC_AVFORMAT_INCLUDE_DIRS} + ${PC_AVCODEC_INCLUDE_DIRS} + ${PC_AVUTIL_INCLUDE_DIRS} + ${PC_SWSCALE_INCLUDE_DIRS}) + target_link_libraries(${PLUGIN_NAME} + ${PC_AVFORMAT_LIBRARIES} + ${PC_AVCODEC_LIBRARIES} + ${PC_AVUTIL_LIBRARIES} + ${PC_SWSCALE_LIBRARIES}) + endif() + endif() diff --git a/plugins/protocol_lws_hls/README.md b/plugins/protocol_lws_hls/README.md new file mode 100644 index 0000000000..1bc55bc4d8 --- /dev/null +++ b/plugins/protocol_lws_hls/README.md @@ -0,0 +1,56 @@ +# lws-hls plugin + +This plugin implements an Apple HLS (HTTP Live Streaming) server using `libavformat`. +It dynamically builds an `.m3u8` playlist for media files within a specified directory, +and extracts/remuxes MPEG-TS segments on the fly without performing disk I/O. + +## Minimal Example + +A minimal example server is provided at `minimal-examples-lowlevel/http-server/minimal-http-server-hls`. +This standalone example demonstrates how to: + +- Create an LWS vhost that explicitly loads `protocol_lws_hls`. +- Provide an overarching `media-dir` via the per-vhost options (PVO) mapping to expose local videos. +- Stream media files without transcoding. + +To test the minimal server: + +```bash +cd build/minimal-examples-lowlevel/http-server/minimal-http-server-hls +./lws-minimal-http-server-hls --media-dir /path/to/my/videos +``` +Then visit `http://localhost:7681` to view the directory listing. + +## lwsws configuration + +If you are using the generic `lwsws` (lws web server) framework, you can configure the plugin on a vhost through your JSON configuration file (parsed by `lejp`). You must instantiate the protocol on the vhost, and also create two separate mounts in the URL space: one for static file serving of the player assets, and one using `callback://` to route incoming HTTP requests to the protocol plugin callback. + +Example JSON snippet (e.g. inside `/etc/lwsws/conf.d/myvhost.json`): + +```json +{ + "vhosts": [{ + "name": "localhost", + "port": 7681, + "ws-protocols": [{ + "lws-hls": { + "status": "ok", + "media-dir": "/var/lib/media" + } + }], + "mounts": [ + { + "mountpoint": "/media", + "origin": "file:///usr/local/share/libwebsockets-test-server/hls/mount-origin", + "default": "index.html" + }, + { + "mountpoint": "/media/hls", + "origin": "callback://lws-hls" + } + ] + }] +} +``` + +This attaches the `lws-hls` protocol to the vhost, maps the `/media` URL path to a standard file mount pointing to your player HTML assets, and creates a `/media/hls` callback mount bound to the `lws-hls` protocol callback. This ensures player files (like `player.html` and `dir.css`) are served statically, while dynamic playlist generator, thumbnail generator, and streaming requests are properly routed to the plugin. diff --git a/plugins/protocol_lws_hls/hls-av.c b/plugins/protocol_lws_hls/hls-av.c new file mode 100644 index 0000000000..704ba2b41d --- /dev/null +++ b/plugins/protocol_lws_hls/hls-av.c @@ -0,0 +1,882 @@ +#include "private-lws-hls.h" +#include +#include + +#define HLS_SEGMENT_DUR 10 + + +void * +lws_hls_thumbnail_worker(void *d) +{ + struct per_vhost_data__lws_hls *vhd = (struct per_vhost_data__lws_hls *)d; + + while (1) { + pthread_mutex_lock(&vhd->lock); + + while (!vhd->thread_exit && !vhd->task_head) { + pthread_cond_wait(&vhd->cond, &vhd->lock); + } + + if (vhd->thread_exit) { + pthread_mutex_unlock(&vhd->lock); + break; + } + + struct thumb_task *t = vhd->task_head; + vhd->task_head = t->next; + if (!vhd->task_head) + vhd->task_tail = NULL; + + pthread_mutex_unlock(&vhd->lock); + + char filepath[512]; + snprintf(filepath, sizeof(filepath), "%s/%s", vhd->media_dir, t->filename); + + AVFormatContext *fmt_ctx = NULL; + AVCodecContext *dec_ctx = NULL; + AVCodecContext *enc_ctx = NULL; + struct SwsContext *sws_ctx = NULL; + AVFrame *frame = NULL; + AVFrame *rgb_frame = NULL; + AVPacket *pkt = NULL; + AVPacket *enc_pkt = NULL; + uint8_t *jpeg_data = NULL; + int jpeg_size = 0; + + if (avformat_open_input(&fmt_ctx, filepath, NULL, NULL) == 0) { + if (avformat_find_stream_info(fmt_ctx, NULL) >= 0) { + int video_idx = -1; + for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) { + if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + video_idx = (int)i; + break; + } + } + + if (video_idx >= 0) { + const AVCodec *decoder = avcodec_find_decoder(fmt_ctx->streams[video_idx]->codecpar->codec_id); + if (decoder) { + dec_ctx = avcodec_alloc_context3(decoder); + avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[video_idx]->codecpar); + + if (avcodec_open2(dec_ctx, decoder, NULL) == 0) { + frame = av_frame_alloc(); + pkt = av_packet_alloc(); + + while (av_read_frame(fmt_ctx, pkt) >= 0) { + if (pkt->stream_index == video_idx) { + if (avcodec_send_packet(dec_ctx, pkt) == 0) { + if (avcodec_receive_frame(dec_ctx, frame) == 0) { + const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG); + if (encoder) { + enc_ctx = avcodec_alloc_context3(encoder); + enc_ctx->width = frame->width; + enc_ctx->height = frame->height; + enc_ctx->time_base = (AVRational){1, 25}; + enc_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P; + + if (avcodec_open2(enc_ctx, encoder, NULL) == 0) { + sws_ctx = sws_getContext(frame->width, frame->height, dec_ctx->pix_fmt, + enc_ctx->width, enc_ctx->height, enc_ctx->pix_fmt, + SWS_BILINEAR, NULL, NULL, NULL); + + if (sws_ctx) { + rgb_frame = av_frame_alloc(); + rgb_frame->format = enc_ctx->pix_fmt; + rgb_frame->width = enc_ctx->width; + rgb_frame->height = enc_ctx->height; + av_frame_get_buffer(rgb_frame, 32); + + sws_scale(sws_ctx, (const uint8_t * const*)frame->data, frame->linesize, + 0, frame->height, rgb_frame->data, rgb_frame->linesize); + + enc_pkt = av_packet_alloc(); + if (avcodec_send_frame(enc_ctx, rgb_frame) == 0) { + if (avcodec_receive_packet(enc_ctx, enc_pkt) == 0) { + jpeg_data = malloc((size_t)enc_pkt->size); + memcpy(jpeg_data, enc_pkt->data, (size_t)enc_pkt->size); + jpeg_size = enc_pkt->size; + } + } + av_packet_free(&enc_pkt); + av_frame_free(&rgb_frame); + sws_freeContext(sws_ctx); + } + } + avcodec_free_context(&enc_ctx); + } + av_packet_unref(pkt); + break; + } + } + } + av_packet_unref(pkt); + } + av_packet_free(&pkt); + av_frame_free(&frame); + } + avcodec_free_context(&dec_ctx); + } + } + } + avformat_close_input(&fmt_ctx); + } + + pthread_mutex_lock(&vhd->lock); + + struct thumb_cache *c = malloc(sizeof(*c)); + strncpy(c->filename, t->filename, sizeof(c->filename)); + c->data = jpeg_data; + c->len = (size_t)jpeg_size; + + c->next = vhd->cache_head; + vhd->cache_head = c; + vhd->cache_count++; + + if (vhd->cache_count > 20) { + struct thumb_cache *prev = NULL; + struct thumb_cache *curr = vhd->cache_head; + while (curr && curr->next) { + prev = curr; + curr = curr->next; + } + if (prev) { + prev->next = NULL; + if (curr->data) free(curr->data); + free(curr); + vhd->cache_count--; + } + } + + pthread_mutex_unlock(&vhd->lock); + free(t); + + lws_cancel_service(vhd->context); + } + + return NULL; +} + +int +lws_hls_serve_thumbnail(struct lws *wsi, const char *media_dir, const char *filename) +{ + struct per_vhost_data__lws_hls *vhd = (struct per_vhost_data__lws_hls *) + lws_protocol_vh_priv_get(lws_get_vhost(wsi), lws_get_protocol(wsi)); + struct per_session_data__lws_hls *pss = (struct per_session_data__lws_hls *) + lws_wsi_user(wsi); + + if (!vhd || !pss) return -1; + + pthread_mutex_lock(&vhd->lock); + + struct thumb_cache *c = vhd->cache_head; + while (c) { + if (!strcmp(c->filename, filename)) + break; + c = c->next; + } + + if (c) { + pthread_mutex_unlock(&vhd->lock); + strncpy(pss->thumb_filename, filename, sizeof(pss->thumb_filename)); + pss->waiting_for_thumbnail = 1; + lws_callback_on_writable(wsi); + return 0; + } + + struct thumb_task *t = vhd->task_head; + int already_queued = 0; + while (t) { + if (!strcmp(t->filename, filename)) { + already_queued = 1; + break; + } + t = t->next; + } + + if (!already_queued) { + struct thumb_task *nt = malloc(sizeof(*nt)); + if (!nt) { + pthread_mutex_unlock(&vhd->lock); + return -1; + } + strncpy(nt->filename, filename, sizeof(nt->filename)); + nt->next = NULL; + + if (vhd->task_tail) + vhd->task_tail->next = nt; + else + vhd->task_head = nt; + vhd->task_tail = nt; + + pthread_cond_signal(&vhd->cond); + } + + pthread_mutex_unlock(&vhd->lock); + + strncpy(pss->thumb_filename, filename, sizeof(pss->thumb_filename)); + pss->waiting_for_thumbnail = 1; + lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT, 30); + + return 0; +} + + +/* Custom AVIOContext writer for memory */ +struct hls_buffer { + uint8_t *ptr; + size_t size; + size_t allocated; +}; + +#if LIBAVFORMAT_VERSION_MAJOR >= 61 +static int write_packet(void *opaque, const uint8_t *buf, int buf_size) { +#else +static int write_packet(void *opaque, uint8_t *buf, int buf_size) { +#endif + struct hls_buffer *hb = (struct hls_buffer *)opaque; + if (hb->size + (size_t)buf_size > hb->allocated) { + hb->allocated = (hb->size + (size_t)buf_size) * 2; + hb->ptr = realloc(hb->ptr, hb->allocated); + } + memcpy(hb->ptr + hb->size, buf, (size_t)buf_size); + hb->size += (size_t)buf_size; + return buf_size; +} + +static size_t find_moof_offset(uint8_t *buf, size_t size) { + size_t offset = 0; + while (offset + 8 <= size) { + uint32_t box_size = (buf[offset] << 24) | (buf[offset+1] << 16) | (buf[offset+2] << 8) | buf[offset+3]; + if (box_size == 1 || box_size < 8) break; + if (memcmp(buf + offset + 4, "moof", 4) == 0) { + return offset; + } + offset += box_size; + } + return 0; +} + +int +lws_hls_serve_init(struct lws *wsi, const char *media_dir, const char *filename) +{ + char filepath[1024]; + snprintf(filepath, sizeof(filepath), "%s/%s", media_dir, filename); + + AVFormatContext *in_ctx = NULL; + if (avformat_open_input(&in_ctx, filepath, NULL, NULL) < 0) { + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, "File not found"); + return -1; + } + + if (avformat_find_stream_info(in_ctx, NULL) < 0) { + avformat_close_input(&in_ctx); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Stream info error"); + return -1; + } + + AVFormatContext *out_ctx = NULL; + avformat_alloc_output_context2(&out_ctx, NULL, "mp4", NULL); + if (!out_ctx) { + avformat_close_input(&in_ctx); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Out ctx error"); + return -1; + } + + for (unsigned int i = 0; i < in_ctx->nb_streams; i++) { + AVStream *in_stream = in_ctx->streams[i]; + AVCodecParameters *in_codecpar = in_stream->codecpar; + + if (in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO && + in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { + continue; + } + + AVStream *out_stream = avformat_new_stream(out_ctx, NULL); + avcodec_parameters_copy(out_stream->codecpar, in_codecpar); + out_stream->codecpar->codec_tag = 0; + out_stream->time_base = in_stream->time_base; + } + + struct hls_buffer hb; + hb.size = 0; + hb.allocated = 1024 * 1024; + hb.ptr = malloc(hb.allocated); + + unsigned char *avio_ctx_buffer = av_malloc(32768); + AVIOContext *avio_ctx = avio_alloc_context(avio_ctx_buffer, 32768, 1, &hb, NULL, write_packet, NULL); + out_ctx->pb = avio_ctx; + + AVDictionary *opts = NULL; + av_dict_set(&opts, "movflags", "empty_moov+default_base_moof+delay_moov", 0); + + char timescale_str[32]; + for (unsigned int i = 0; i < out_ctx->nb_streams; i++) { + if (out_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + snprintf(timescale_str, sizeof(timescale_str), "%d", out_ctx->streams[i]->time_base.den); + av_dict_set(&opts, "video_track_timescale", timescale_str, 0); + break; + } + } + + if (avformat_write_header(out_ctx, &opts) < 0) { + av_dict_free(&opts); + if (out_ctx) { + av_free(out_ctx->pb->buffer); + av_free(out_ctx->pb); + avformat_free_context(out_ctx); + } + avformat_close_input(&in_ctx); + free(hb.ptr); + return -1; + } + av_write_trailer(out_ctx); + av_dict_free(&opts); + + if (out_ctx) { + av_free(out_ctx->pb->buffer); + av_free(out_ctx->pb); + avformat_free_context(out_ctx); + } + avformat_close_input(&in_ctx); + + struct per_session_data__lws_hls *pss = (struct per_session_data__lws_hls *)lws_wsi_user(wsi); + if (!pss || hb.size == 0) { + free(hb.ptr); + return -1; + } + + /* + * The init segment (EXT-X-MAP) must contain ftyp + moov only. + * If av_write_trailer produced a trailing moof+mdat, strip it. + * For init: send everything BEFORE the moof. + * (For media segments the opposite is done: send moof onwards.) + */ + size_t moof_off = find_moof_offset(hb.ptr, hb.size); + size_t send_size = moof_off > 0 ? moof_off : hb.size; + + lwsl_user("HLS: Init segment: total=%zu, moof_offset=%zu, " + "sending=%zu (ftyp+moov)\n", hb.size, moof_off, send_size); + + pss->segment_buf = malloc(LWS_PRE + send_size); + if (!pss->segment_buf) { + free(hb.ptr); + return -1; + } + + memcpy(pss->segment_buf + LWS_PRE, hb.ptr, send_size); + pss->segment_len = send_size; + + pss->segment_pos = 0; + free(hb.ptr); + + uint8_t hbuf[LWS_PRE + 2048], *start = hbuf + LWS_PRE, *p = start, *end = p + 2048; + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "video/mp4", + (lws_filepos_t)pss->segment_len, &p, end) || + lws_finalize_write_http_header(wsi, start, &p, end)) { + return -1; + } + + lws_callback_on_writable(wsi); + return 0; +} +int +lws_hls_serve_manifest(struct lws *wsi, const char *media_dir, const char *filename) +{ + char filepath[1024]; + snprintf(filepath, sizeof(filepath), "%s/%s", media_dir, filename); + + AVFormatContext *fmt_ctx = NULL; + if (avformat_open_input(&fmt_ctx, filepath, NULL, NULL) < 0) { + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, "File not found"); + return -1; + } + + if (avformat_find_stream_info(fmt_ctx, NULL) < 0) { + avformat_close_input(&fmt_ctx); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Stream info error"); + return -1; + } + + int video_idx = -1; + unsigned int i_stream; + int total_segments; + + for (i_stream = 0; i_stream < fmt_ctx->nb_streams; i_stream++) { + if (fmt_ctx->streams[i_stream]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + video_idx = (int)i_stream; + break; + } + } + + int64_t duration = fmt_ctx->duration; + if (duration <= 0 && video_idx >= 0 && fmt_ctx->streams[video_idx]->duration > 0) { + duration = av_rescale_q(fmt_ctx->streams[video_idx]->duration, + fmt_ctx->streams[video_idx]->time_base, AV_TIME_BASE_Q); + } + + if (duration <= 0) { + avformat_close_input(&fmt_ctx); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Unknown duration"); + return -1; + } + + total_segments = (int)(duration / ((int64_t)HLS_SEGMENT_DUR * AV_TIME_BASE)); + if (duration % ((int64_t)HLS_SEGMENT_DUR * AV_TIME_BASE) != 0) + total_segments++; + + avformat_close_input(&fmt_ctx); + + int target_duration = HLS_SEGMENT_DUR; + size_t m3u8_max = 1024 + (size_t)(total_segments * 128); + char *m3u8 = malloc(LWS_PRE + m3u8_max); + if (!m3u8) { + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "OOM"); + return -1; + } + + char *p_m3u8 = m3u8 + LWS_PRE; + p_m3u8 += snprintf(p_m3u8, m3u8_max, + "#EXTM3U\n" + "#EXT-X-VERSION:7\n" + "#EXT-X-TARGETDURATION:%d\n" + "#EXT-X-MEDIA-SEQUENCE:0\n" + "#EXT-X-MAP:URI=\"../init/%s\"\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n", target_duration, filename); + + for (int i = 0; i < total_segments; i++) { + double dur = (double)HLS_SEGMENT_DUR; + if (i == total_segments - 1) { + int64_t rem = duration - (int64_t)i * HLS_SEGMENT_DUR * AV_TIME_BASE; + dur = (double)rem / AV_TIME_BASE; + } + if (dur <= 0.0) { + dur = 0.1; + } + size_t rem_buf = m3u8_max - (size_t)(p_m3u8 - (m3u8 + LWS_PRE)); + p_m3u8 += snprintf(p_m3u8, rem_buf, + "#EXTINF:%f,\n" + "../segment/%s/%d\n", + dur, filename, i); + } + + size_t rem = m3u8_max - (size_t)(p_m3u8 - (m3u8 + LWS_PRE)); + snprintf(p_m3u8, rem, "#EXT-X-ENDLIST\n"); + + size_t len = strlen(m3u8 + LWS_PRE); + + /* Write out to LWS */ + uint8_t *buf = malloc(LWS_PRE + 2048); + if (!buf) { + free(m3u8); + return -1; + } + + uint8_t *start = buf + LWS_PRE; + uint8_t *p = start; + uint8_t *end = p + 2048; + + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "application/vnd.apple.mpegurl", + (lws_filepos_t)len, &p, end) || + lws_finalize_write_http_header(wsi, start, &p, end)) { + free(buf); + free(m3u8); + return 1; + } + + lws_write(wsi, (uint8_t *)(m3u8 + LWS_PRE), len, LWS_WRITE_HTTP_FINAL); + + free(buf); + free(m3u8); + return 1; /* Close connection after sending manifest */ +} + + +int +lws_hls_serve_segment(struct lws *wsi, const char *media_dir, const char *filename, int segment_idx) +{ + char filepath[1024]; + snprintf(filepath, sizeof(filepath), "%s/%s", media_dir, filename); + + AVFormatContext *in_ctx = NULL; + if (avformat_open_input(&in_ctx, filepath, NULL, NULL) < 0) { + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, "File not found"); + return -1; + } + + if (avformat_find_stream_info(in_ctx, NULL) < 0) { + avformat_close_input(&in_ctx); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Stream info error"); + return -1; + } + + AVFormatContext *out_ctx = NULL; + avformat_alloc_output_context2(&out_ctx, NULL, "mp4", NULL); + if (!out_ctx) { + avformat_close_input(&in_ctx); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Out ctx error"); + return -1; + } + + int *stream_mapping = malloc((size_t)in_ctx->nb_streams * sizeof(int)); + int stream_index = 0; + int has_video = 0; + int video_idx = -1; + int audio_idx = -1; + + for (unsigned int i = 0; i < in_ctx->nb_streams; i++) { + AVStream *in_stream = in_ctx->streams[i]; + AVCodecParameters *in_codecpar = in_stream->codecpar; + + if (in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO && + in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { + stream_mapping[i] = -1; + continue; + } + + if (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + has_video = 1; + video_idx = (int)i; + } + if (in_codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + audio_idx = (int)i; + } + + stream_mapping[i] = stream_index++; + AVStream *out_stream = avformat_new_stream(out_ctx, NULL); + avcodec_parameters_copy(out_stream->codecpar, in_codecpar); + out_stream->codecpar->codec_tag = 0; + out_stream->time_base = in_stream->time_base; + } + + struct hls_buffer hb; + hb.size = 0; + hb.allocated = 1024 * 1024; /* 1MB init */ + hb.ptr = malloc(hb.allocated); + + unsigned char *avio_ctx_buffer = av_malloc(32768); + AVIOContext *avio_ctx = avio_alloc_context(avio_ctx_buffer, 32768, + 1, &hb, NULL, write_packet, NULL); + out_ctx->pb = avio_ctx; + + + AVDictionary *opts = NULL; + av_dict_set(&opts, "movflags", "empty_moov+default_base_moof+delay_moov", 0); + + /* Set video_track_timescale to match input for accurate TFDT */ + char timescale_str[32]; + for (unsigned int i = 0; i < out_ctx->nb_streams; i++) { + if (out_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + snprintf(timescale_str, sizeof(timescale_str), "%d", out_ctx->streams[i]->time_base.den); + av_dict_set(&opts, "video_track_timescale", timescale_str, 0); + break; + } + } + + if (avformat_write_header(out_ctx, &opts) < 0) { + + /* Error */ + goto done; + } + + /* + * Do NOT set avoid_negative_ts here. Our timestamps are absolute + * positions in the media file (always non-negative). The fMP4 muxer + * with delay_moov will write the correct tfdt (Track Fragment Decode + * Time) reflecting the absolute position, which hls.js needs to + * stitch segments seamlessly without gaps. + */ + + int64_t start_time = (int64_t)segment_idx * HLS_SEGMENT_DUR * AV_TIME_BASE; + int64_t end_time = (int64_t)(segment_idx + 1) * HLS_SEGMENT_DUR * AV_TIME_BASE; + + int64_t duration = in_ctx->duration; + if (duration <= 0 && video_idx >= 0 && in_ctx->streams[video_idx]->duration > 0) { + duration = av_rescale_q(in_ctx->streams[video_idx]->duration, + in_ctx->streams[video_idx]->time_base, AV_TIME_BASE_Q); + } + + if (duration > 0 && start_time >= duration) { + goto done; + } + + lwsl_user("HLS: Segment %d requested. start_time=%lld (%.3fs), end_time=%lld (%.3fs)\n", + segment_idx, (long long)start_time, (double)start_time / AV_TIME_BASE, + (long long)end_time, (double)end_time / AV_TIME_BASE); + + if (video_idx >= 0) { + int64_t seek_time = start_time > 5000 ? start_time - 5000 : 0; + int64_t target_ts = av_rescale_q(seek_time, AV_TIME_BASE_Q, in_ctx->streams[video_idx]->time_base); + av_seek_frame(in_ctx, video_idx, target_ts, AVSEEK_FLAG_BACKWARD); + lwsl_user("HLS: Segment %d video seek requested to %lld (%.3fs)\n", + segment_idx, (long long)target_ts, (double)start_time / AV_TIME_BASE); + } else { + av_seek_frame(in_ctx, -1, start_time, AVSEEK_FLAG_BACKWARD); + lwsl_user("HLS: Segment %d generic seek requested to %.3fs\n", + segment_idx, (double)start_time / AV_TIME_BASE); + } + + int64_t last_dts[32]; + for (int i = 0; i < 32; i++) { + last_dts[i] = AV_NOPTS_VALUE; + } + int started = 0; + int video_finished = 0; + + /* Diagnostics variables */ + int64_t first_video_pts = AV_NOPTS_VALUE, last_video_pts = AV_NOPTS_VALUE; + int64_t first_video_dts = AV_NOPTS_VALUE, last_video_dts = AV_NOPTS_VALUE; + int64_t first_audio_pts = AV_NOPTS_VALUE, last_audio_pts = AV_NOPTS_VALUE; + int64_t first_audio_dts = AV_NOPTS_VALUE, last_audio_dts = AV_NOPTS_VALUE; + int video_packets_written = 0, audio_packets_written = 0; + int video_packets_discarded = 0, audio_packets_discarded = 0; + AVRational video_out_time_base = {0, 0}; + AVRational audio_out_time_base = {0, 0}; + + AVPacket pkt; + while (av_read_frame(in_ctx, &pkt) >= 0) { AVStream *in_stream = in_ctx->streams[pkt.stream_index]; + if (stream_mapping[pkt.stream_index] < 0) { + av_packet_unref(&pkt); + continue; + } + /* + * Synthesize missing DTS/PTS (MKV has no DTS). + * Must happen before boundary checks. + */ + if (pkt.dts == AV_NOPTS_VALUE) pkt.dts = pkt.pts; + if (pkt.pts == AV_NOPTS_VALUE) pkt.pts = pkt.dts; + + /* Use PTS for boundary checks (always valid after synthesis) */ + int64_t pkt_ts = pkt.pts != AV_NOPTS_VALUE ? pkt.pts : pkt.dts; + if (pkt_ts != AV_NOPTS_VALUE) { + int64_t pkt_time = av_rescale_q(pkt_ts, in_stream->time_base, AV_TIME_BASE_Q); + + + + if (!started) { + if (has_video) { + if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + if ((pkt.flags & AV_PKT_FLAG_KEY) && pkt_time >= start_time - 5000) { + started = 1; + lwsl_user("HLS: Segment %d started writing video at pkt_time=%.3fs (pts=%lld, dts=%lld)\n", + segment_idx, (double)pkt_time / AV_TIME_BASE, (long long)pkt.pts, (long long)pkt.dts); + } else { + video_packets_discarded++; + av_packet_unref(&pkt); + continue; + } + } else { + /* Audio: discard until video has started */ + if (!started) { + audio_packets_discarded++; + av_packet_unref(&pkt); + continue; + } + } + } else { + if (pkt_time >= start_time - 5000) { + started = 1; + lwsl_user("HLS: Segment %d started writing audio-only at pkt_time=%.3fs (pts=%lld, dts=%lld)\n", + segment_idx, (double)pkt_time / AV_TIME_BASE, (long long)pkt.pts, (long long)pkt.dts); + } else { + audio_packets_discarded++; + av_packet_unref(&pkt); + continue; + } + } + } + + /* Stop at end of segment on next video keyframe */ + if (pkt_time >= end_time - 5000 && in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + if (pkt.flags & AV_PKT_FLAG_KEY) { + lwsl_user("HLS: Segment %d reached next video keyframe at pkt_time=%.3fs (pts=%lld, dts=%lld). Video finished.\n", + segment_idx, (double)pkt_time / AV_TIME_BASE, (long long)pkt.pts, (long long)pkt.dts); + video_finished = 1; + end_time = pkt_time; /* Extend or shrink end_time to match ACTUAL video end */ + av_packet_unref(&pkt); + continue; + } + } + + /* If there's no audio track, we must break once we're sure no more B-frames exist */ + if (video_finished && audio_idx < 0) { + int64_t dts_time = av_rescale_q(pkt.dts != AV_NOPTS_VALUE ? pkt.dts : pkt.pts, in_stream->time_base, AV_TIME_BASE_Q); + if (dts_time >= end_time) { + av_packet_unref(&pkt); + break; + } + } + + if (video_finished && in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + if (pkt_time >= end_time) { + video_packets_discarded++; + av_packet_unref(&pkt); + continue; + } + /* Otherwise, it's a B-frame belonging to the current segment, keep it! */ + } + + /* Stop audio after video has finished and audio reaches the actual video end boundary */ + if (video_finished && in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + if (pkt_time >= end_time) { + av_packet_unref(&pkt); + lwsl_user("HLS: Segment %d reached audio end at pkt_time=%.3fs. Stopping.\n", + segment_idx, (double)pkt_time / AV_TIME_BASE); + break; + } + } + } + + int out_stream_idx = stream_mapping[pkt.stream_index]; + pkt.stream_index = out_stream_idx; + AVStream *out_stream = out_ctx->streams[out_stream_idx]; + + pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); + pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); + pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base); + pkt.pos = -1; + + if (pkt.dts != AV_NOPTS_VALUE) { + if (last_dts[out_stream_idx] != AV_NOPTS_VALUE && pkt.dts <= last_dts[out_stream_idx]) { + pkt.dts = last_dts[out_stream_idx] + 1; + } + last_dts[out_stream_idx] = pkt.dts; + } + if (pkt.pts != AV_NOPTS_VALUE && pkt.pts < pkt.dts) { + pkt.pts = pkt.dts; + } + + /* Track stats for output packets */ + if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + if (first_video_pts == AV_NOPTS_VALUE) { + first_video_pts = pkt.pts; + first_video_dts = pkt.dts; + } + last_video_pts = pkt.pts; + last_video_dts = pkt.dts; + video_packets_written++; + } else if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + if (first_audio_pts == AV_NOPTS_VALUE) { + first_audio_pts = pkt.pts; + first_audio_dts = pkt.dts; + } + last_audio_pts = pkt.pts; + last_audio_dts = pkt.dts; + audio_packets_written++; + } + + av_interleaved_write_frame(out_ctx, &pkt); + av_packet_unref(&pkt); + } + + av_write_trailer(out_ctx); + av_dict_free(&opts); + + if (video_idx >= 0 && stream_mapping[video_idx] >= 0) { + video_out_time_base = out_ctx->streams[stream_mapping[video_idx]]->time_base; + } + if (audio_idx >= 0 && stream_mapping[audio_idx] >= 0) { + audio_out_time_base = out_ctx->streams[stream_mapping[audio_idx]]->time_base; + } + +done: + if (out_ctx) { + av_free(out_ctx->pb->buffer); + av_free(out_ctx->pb); + avformat_free_context(out_ctx); + } + avformat_close_input(&in_ctx); + free(stream_mapping); + + /* Now we have the segment in hb.ptr! Send it to the client via LWS. + For proper LWS async writing, we should save `hb` to per-session data and trigger WRITEABLE. + We will allocate the buffer with LWS_PRE, attach to wsi, and request WRITEABLE. */ + + struct per_session_data__lws_hls *pss = + (struct per_session_data__lws_hls *)lws_protocol_vh_priv_get( + lws_get_vhost(wsi), lws_get_protocol(wsi)); + + /* Wait, lws_protocol_vh_priv_get gets VHD. We want PSS! */ + pss = (struct per_session_data__lws_hls *)lws_wsi_user(wsi); + + if (!pss || hb.size == 0) { + free(hb.ptr); + return -1; + } + + + size_t offset = find_moof_offset(hb.ptr, hb.size); + size_t send_size = hb.size - offset; + + /* Calculate and log stats */ + double video_duration_sec = 0.0; + double audio_duration_sec = 0.0; + double start_av_delta_sec = 0.0; + double end_av_delta_sec = 0.0; + + if (first_video_pts != AV_NOPTS_VALUE && video_out_time_base.den > 0) { + video_duration_sec = (double)(last_video_pts - first_video_pts) * av_q2d(video_out_time_base); + } + if (first_audio_pts != AV_NOPTS_VALUE && audio_out_time_base.den > 0) { + audio_duration_sec = (double)(last_audio_pts - first_audio_pts) * av_q2d(audio_out_time_base); + } + if (first_video_pts != AV_NOPTS_VALUE && video_out_time_base.den > 0 && + first_audio_pts != AV_NOPTS_VALUE && audio_out_time_base.den > 0) { + double first_v_sec = (double)first_video_pts * av_q2d(video_out_time_base); + double first_a_sec = (double)first_audio_pts * av_q2d(audio_out_time_base); + double last_v_sec = (double)last_video_pts * av_q2d(video_out_time_base); + double last_a_sec = (double)last_audio_pts * av_q2d(audio_out_time_base); + start_av_delta_sec = first_a_sec - first_v_sec; + end_av_delta_sec = last_a_sec - last_v_sec; + } + + lwsl_user("HLS: Segment %d summary:\n" + " Discarded: video=%d, audio=%d\n" + " Written: video=%d, audio=%d\n" + " Video output PTS: [%lld to %lld] (diff = %lld, %.3fs)\n" + " Video output DTS: [%lld to %lld]\n" + " Audio output PTS: [%lld to %lld] (diff = %lld, %.3fs)\n" + " Audio output DTS: [%lld to %lld]\n" + " First Audio-Video PTS delta: %.3fs\n" + " Last Audio-Video PTS delta: %.3fs\n" + " Segment size: %zu bytes (sent %zu bytes)\n", + segment_idx, + video_packets_discarded, audio_packets_discarded, + video_packets_written, audio_packets_written, + (long long)first_video_pts, (long long)last_video_pts, + (long long)(last_video_pts - first_video_pts), + video_duration_sec, + (long long)first_video_dts, (long long)last_video_dts, + (long long)first_audio_pts, (long long)last_audio_pts, + (long long)(last_audio_pts - first_audio_pts), + audio_duration_sec, + (long long)first_audio_dts, (long long)last_audio_dts, + start_av_delta_sec, + end_av_delta_sec, + hb.size, send_size); + + pss->segment_buf = malloc(LWS_PRE + send_size); + if (!pss->segment_buf) { + free(hb.ptr); + return -1; + } + + memcpy(pss->segment_buf + LWS_PRE, hb.ptr + offset, send_size); + pss->segment_len = send_size; + + pss->segment_pos = 0; + free(hb.ptr); + + /* Send HTTP headers */ + uint8_t hbuf[LWS_PRE + 2048], *start = hbuf + LWS_PRE, *p = start, *end = p + 2048; + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "video/mp4", + (lws_filepos_t)pss->segment_len, &p, end) || + lws_finalize_write_http_header(wsi, start, &p, end)) { + return -1; + } + + /* Request writable callback to pump data */ + lws_callback_on_writable(wsi); + + return 0; /* Keep connection alive to write data */ +} diff --git a/plugins/protocol_lws_hls/hls-dir.c b/plugins/protocol_lws_hls/hls-dir.c new file mode 100644 index 0000000000..8e020af20e --- /dev/null +++ b/plugins/protocol_lws_hls/hls-dir.c @@ -0,0 +1,153 @@ +#include "private-lws-hls.h" +#include +#include + +struct file_entry { + char name[256]; + time_t mtime; +}; + +struct dir_state { + struct file_entry *entries; + size_t count; + size_t max; + const char *base_dir; +}; + +static int +hls_dir_cb(const char *dirpath, void *user, struct lws_dir_entry *lde) +{ + struct dir_state *ds = (struct dir_state *)user; + struct stat st; + char path[1024]; + + if (!strcmp(lde->name, ".") || !strcmp(lde->name, "..")) + return 0; + + snprintf(path, sizeof(path), "%s/%s", dirpath, lde->name); + + if (lde->type == LDOT_DIR) { + lws_dir(path, ds, hls_dir_cb); + return 0; + } + + if (lde->type != LDOT_FILE) + return 0; + + /* only list media files */ + if (!strstr(lde->name, ".mp4") && !strstr(lde->name, ".mkv")) + return 0; + + if (ds->count >= ds->max) { + ds->max += 64; + ds->entries = realloc(ds->entries, ds->max * sizeof(struct file_entry)); + if (!ds->entries) + return 1; + } + + if (stat(path, &st) == 0) { + const char *rel_path = path; + size_t base_len = strlen(ds->base_dir); + if (!strncmp(path, ds->base_dir, base_len) && path[base_len] == '/') + rel_path = path + base_len + 1; + + strncpy(ds->entries[ds->count].name, rel_path, sizeof(ds->entries[ds->count].name) - 1); + ds->entries[ds->count].name[sizeof(ds->entries[ds->count].name) - 1] = '\0'; + ds->entries[ds->count].mtime = st.st_mtime; + ds->count++; + } + + return 0; +} + +static int +cmp_mtime(const void *a, const void *b) +{ + const struct file_entry *fa = (const struct file_entry *)a; + const struct file_entry *fb = (const struct file_entry *)b; + if (fb->mtime > fa->mtime) return 1; + if (fb->mtime < fa->mtime) return -1; + return 0; +} + +int +lws_hls_serve_dir(struct lws *wsi, const char *media_dir) +{ + struct dir_state ds; + memset(&ds, 0, sizeof(ds)); + ds.base_dir = media_dir; + + lws_dir(media_dir, &ds, hls_dir_cb); + + if (ds.count > 0 && ds.entries) + qsort(ds.entries, ds.count, sizeof(struct file_entry), cmp_mtime); + + /* Generate HTML */ + size_t html_size = 8192 + (ds.count * 512); + char *html = malloc(LWS_PRE + html_size); + if (!html) { + if (ds.entries) free(ds.entries); + lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); + return -1; + } + + char *p_html = html + LWS_PRE; + p_html += snprintf(p_html, html_size, + "LWS HLS Media" + "" + "" + "" + "" + "
" + "

Media Directory

"); + + for (size_t i = 0; i < ds.count; i++) { + size_t rem = html_size - (size_t)(p_html - (html + LWS_PRE)); + p_html += snprintf(p_html, rem, + "", + ds.entries[i].name, ds.entries[i].name, ds.entries[i].name); + } + + size_t rem = html_size - (size_t)(p_html - (html + LWS_PRE)); + snprintf(p_html, rem, "
"); + + size_t len = strlen(html + LWS_PRE); + + uint8_t *buf = malloc(LWS_PRE + 2048); + if (!buf) { + free(html); + if (ds.entries) free(ds.entries); + return -1; + } + + uint8_t *start = buf + LWS_PRE; + uint8_t *p = start; + uint8_t *end = p + 2048; + + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "text/html", + (lws_filepos_t)len, &p, end)) { + free(buf); + free(html); + if (ds.entries) free(ds.entries); + return 1; + } + + if (lws_finalize_write_http_header(wsi, start, &p, end)) { + free(buf); + free(html); + if (ds.entries) free(ds.entries); + return 1; + } + + /* Write body */ + lws_write(wsi, (uint8_t *)(html + LWS_PRE), len, LWS_WRITE_HTTP_FINAL); + + free(buf); + free(html); + if (ds.entries) free(ds.entries); + + return 1; /* Return 1 to close, we sent FINAL */ +} diff --git a/plugins/protocol_lws_hls/private-lws-hls.h b/plugins/protocol_lws_hls/private-lws-hls.h new file mode 100644 index 0000000000..0661898f12 --- /dev/null +++ b/plugins/protocol_lws_hls/private-lws-hls.h @@ -0,0 +1,111 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2026 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#if !defined (LWS_PLUGIN_STATIC) +#if !defined(LWS_DLL) +#define LWS_DLL +#endif +#if !defined(LWS_INTERNAL) +#define LWS_INTERNAL +#endif +#include +#endif + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +#include +#include +#include +#include + +#include + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +struct thumb_task { + struct thumb_task *next; + char filename[256]; +}; + +struct thumb_cache { + struct thumb_cache *next; + char filename[256]; + uint8_t *data; + size_t len; +}; + +struct per_vhost_data__lws_hls { + struct lws_context *context; + struct lws_vhost *vhost; + const struct lws_protocols *protocol; + + const char *media_dir; /* configured via pvo */ + + /* Thumbnail worker thread */ + pthread_t thumb_thread; + pthread_mutex_t lock; + pthread_cond_t cond; + int thread_exit; + + struct thumb_task *task_head; + struct thumb_task *task_tail; + + struct thumb_cache *cache_head; + int cache_count; + + struct per_session_data__lws_hls *pss_list; /* active sessions */ +}; + +struct per_session_data__lws_hls { + struct per_session_data__lws_hls *pss_list; + struct lws *wsi; + uint8_t *segment_buf; + size_t segment_len; + size_t segment_pos; + + /* Thumbnail async state */ + int waiting_for_thumbnail; + char thumb_filename[256]; +}; + +/* hls-av.c */ +void * +lws_hls_thumbnail_worker(void *d); + +int +lws_hls_serve_thumbnail(struct lws *wsi, const char *media_dir, const char *filename); +int +lws_hls_serve_dir(struct lws *wsi, const char *media_dir); + +/* hls-av.c */ +int +lws_hls_serve_thumbnail(struct lws *wsi, const char *media_dir, const char *filename); + +int +lws_hls_serve_init(struct lws *wsi, const char *media_dir, const char *filename); + +int +lws_hls_serve_manifest(struct lws *wsi, const char *media_dir, const char *filename); + +int +lws_hls_serve_segment(struct lws *wsi, const char *media_dir, const char *filename, int segment_idx); diff --git a/plugins/protocol_lws_hls/protocol_lws_hls.c b/plugins/protocol_lws_hls/protocol_lws_hls.c new file mode 100644 index 0000000000..466ef8d0cc --- /dev/null +++ b/plugins/protocol_lws_hls/protocol_lws_hls.c @@ -0,0 +1,316 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2026 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include "private-lws-hls.h" +#include + +static int +callback_lws_hls(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct per_vhost_data__lws_hls *vhd = + (struct per_vhost_data__lws_hls *) + lws_protocol_vh_priv_get(lws_get_vhost(wsi), + lws_get_protocol(wsi)); + const struct lws_protocol_vhost_options *pvo; + + struct per_session_data__lws_hls *pss = + (struct per_session_data__lws_hls *)user; + + switch (reason) { + case LWS_CALLBACK_PROTOCOL_INIT: + if (!in) + return 0; + + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), sizeof(struct per_vhost_data__lws_hls)); + if (!vhd) + return 1; + + if ((pvo = lws_pvo_search((const struct lws_protocol_vhost_options *)in, "media-dir"))) + vhd->media_dir = pvo->value; + else { + lwsl_err("%s: media-dir pvo required\n", __func__); + return 1; + } + + vhd->context = lws_get_context(wsi); + vhd->protocol = lws_get_protocol(wsi); + vhd->vhost = lws_get_vhost(wsi); + + pthread_mutex_init(&vhd->lock, NULL); + pthread_cond_init(&vhd->cond, NULL); + vhd->thread_exit = 0; + if (pthread_create(&vhd->thumb_thread, NULL, lws_hls_thumbnail_worker, vhd)) { + lwsl_err("Failed to create thumbnail thread\n"); + return 1; + } + + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + if (!vhd) + break; + vhd->thread_exit = 1; + pthread_cond_signal(&vhd->cond); + pthread_join(vhd->thumb_thread, NULL); + pthread_mutex_destroy(&vhd->lock); + pthread_cond_destroy(&vhd->cond); + + /* free cache */ + struct thumb_cache *c = vhd->cache_head; + while (c) { + struct thumb_cache *next = c->next; + free(c->data); + free(c); + c = next; + } + + /* free task queue */ + struct thumb_task *t = vhd->task_head; + while (t) { + struct thumb_task *next = t->next; + free(t); + t = next; + } + break; + + case LWS_CALLBACK_HTTP: + { + const char *url = (const char *)in; + + if (!vhd) + return lws_callback_http_dummy(wsi, reason, user, in, len); + + lwsl_user("HLS plugin received HTTP request for '%s'\n", url ? url : "NULL"); + + if (!strcmp(url, "")) { + /* Redirect to add trailing slash */ + char uri[512]; + int ulen = lws_hdr_copy(wsi, uri, sizeof(uri) - 2, WSI_TOKEN_GET_URI); + if (ulen > 0) { + unsigned char redirect_buf[512 + LWS_PRE]; + unsigned char *p_red = redirect_buf + LWS_PRE; + unsigned char *end_red = redirect_buf + sizeof(redirect_buf) - 1; + + uri[ulen] = '/'; + uri[ulen + 1] = '\0'; + ulen++; + + int m = lws_http_redirect(wsi, HTTP_STATUS_MOVED_PERMANENTLY, + (unsigned char *)uri, ulen, &p_red, end_red); + if (m < 0) + return -1; + return lws_http_transaction_completed(wsi); + } + } + + /* Simple routing based on URL prefix */ + if (!strcmp(url, "/") || !strcmp(url, "/index.html")) { + return lws_hls_serve_dir(wsi, vhd->media_dir); + } + else if (!strncmp(url, "/preview/", 9)) { + return lws_hls_serve_thumbnail(wsi, vhd->media_dir, url + 9); + } + else if (!strncmp(url, "/stream/", 8)) { + return lws_hls_serve_manifest(wsi, vhd->media_dir, url + 8); + } + else if (!strncmp(url, "/init/", 6)) { + return lws_hls_serve_init(wsi, vhd->media_dir, url + 6); + } + else if (!strncmp(url, "/segment/", 9)) { + const char *p = url + 9; + const char *sep = strchr(p, '/'); + if (!sep) + goto err_404; + + char filename[256]; + size_t fn_len = (size_t)(sep - p); + if (fn_len >= sizeof(filename)) + goto err_404; + + strncpy(filename, p, fn_len); + filename[fn_len] = '\0'; + + int segment_idx = atoi(sep + 1); + return lws_hls_serve_segment(wsi, vhd->media_dir, filename, segment_idx); + } else { + /* Let LWS standard file serving handle static files from the mount origin */ + return lws_callback_http_dummy(wsi, reason, user, in, len); + } + + return 0; + +err_404: + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); + return -1; + } + + case LWS_CALLBACK_EVENT_WAIT_CANCELLED: + if (!vhd) + break; + /* Thread finished a thumbnail. Wake up all waiting HTTP sessions */ + lws_start_foreach_llp(struct per_session_data__lws_hls **, + ppss, vhd->pss_list) { + if ((*ppss)->waiting_for_thumbnail) + lws_callback_on_writable((*ppss)->wsi); + } lws_end_foreach_llp(ppss, pss_list); + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: + if (pss && pss->waiting_for_thumbnail) { + pthread_mutex_lock(&vhd->lock); + struct thumb_cache *c = vhd->cache_head; + while (c) { + if (!strcmp(c->filename, pss->thumb_filename)) + break; + c = c->next; + } + + if (c) { + /* Found it in cache! */ + size_t len = c->len; + uint8_t *buf = malloc(LWS_PRE + len); + if (!buf) { + pthread_mutex_unlock(&vhd->lock); + return -1; + } + + uint8_t *start = buf + LWS_PRE; + uint8_t *p = start; + uint8_t *end = p + len; + + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "image/jpeg", + (lws_filepos_t)len, &p, end)) { + free(buf); + pthread_mutex_unlock(&vhd->lock); + return 1; + } + + if (lws_finalize_write_http_header(wsi, start, &p, end)) { + free(buf); + pthread_mutex_unlock(&vhd->lock); + return 1; + } + + memcpy(buf + LWS_PRE, c->data, len); + lws_write(wsi, buf + LWS_PRE, len, LWS_WRITE_HTTP_FINAL); + free(buf); + + pss->waiting_for_thumbnail = 0; + pthread_mutex_unlock(&vhd->lock); + + if (lws_http_transaction_completed(wsi)) + return -1; + return 0; + } + + /* Not in cache. Did it fail? */ + int is_pending = 0; + struct thumb_task *t = vhd->task_head; + while (t) { + if (!strcmp(t->filename, pss->thumb_filename)) { + is_pending = 1; + break; + } + t = t->next; + } + + pthread_mutex_unlock(&vhd->lock); + + if (!is_pending) { + /* Not pending and not in cache -> extraction failed */ + pss->waiting_for_thumbnail = 0; + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); + return -1; + } + + /* Still pending, keep waiting */ + return 0; + } + + if (!pss || !pss->segment_buf || pss->segment_pos >= pss->segment_len) + return 1; /* Done or nothing to write */ + + size_t rem = pss->segment_len - pss->segment_pos; + int flags = (pss->segment_pos + rem == pss->segment_len) ? LWS_WRITE_HTTP_FINAL : LWS_WRITE_HTTP; + + int m = lws_write(wsi, pss->segment_buf + LWS_PRE + pss->segment_pos, rem, (enum lws_write_protocol)flags); + if (m < 0) { + free(pss->segment_buf); + pss->segment_buf = NULL; + return -1; + } + + pss->segment_pos += (size_t)m; + if (pss->segment_pos < pss->segment_len) { + lws_callback_on_writable(wsi); + return 0; + } + + free(pss->segment_buf); + pss->segment_buf = NULL; + return 1; /* Close connection after sending segment */ + + case LWS_CALLBACK_CLOSED_HTTP: + if (pss) { + if (vhd) + lws_ll_fwd_remove(struct per_session_data__lws_hls, pss_list, + pss, vhd->pss_list); + if (pss->segment_buf) { + free(pss->segment_buf); + pss->segment_buf = NULL; + } + } + break; + + default: + break; + } + + return 0; +} + +#define LWS_PLUGIN_PROTOCOL_LWS_HLS \ + { \ + "lws-hls", \ + callback_lws_hls, \ + sizeof(struct per_session_data__lws_hls), \ + 1024, \ + 0, NULL, 0 \ + } + +#if !defined (LWS_PLUGIN_STATIC) + +LWS_VISIBLE const struct lws_protocols lws_hls_protocols[] = { + LWS_PLUGIN_PROTOCOL_LWS_HLS +}; + +LWS_VISIBLE const lws_plugin_protocol_t lws_hls = { + .hdr = { + .name = "lws hls", + ._class = "lws_protocol_plugin", + .lws_build_hash = LWS_BUILD_HASH, + .api_magic = LWS_PLUGIN_API_MAGIC + }, + + .protocols = lws_hls_protocols, + .count_protocols = LWS_ARRAY_SIZE(lws_hls_protocols), + .extensions = NULL, + .count_extensions = 0, +}; + +#endif diff --git a/plugins/protocol_lws_login/protocol_lws_login.c b/plugins/protocol_lws_login/protocol_lws_login.c index 2316852250..3cc16ae77f 100644 --- a/plugins/protocol_lws_login/protocol_lws_login.c +++ b/plugins/protocol_lws_login/protocol_lws_login.c @@ -114,7 +114,10 @@ static const char * const canned_css = ".pie-timer circle{fill:none;stroke:#fff;stroke-width:10;stroke-dasharray:31.4;transition:stroke-dashoffset 1s linear;}" ".lws-login-refgirl{position:absolute;bottom:0px;right:-10px;height:120px;opacity:0.8;pointer-events:none;z-index:1;}" ".lws-preauth-widget{display:none;position:absolute;top:0;left:100%;margin-left:15px;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:8px;max-height:200px;overflow-y:auto;background:#2b2d31;box-shadow:0 8px 16px rgba(0,0,0,0.3);z-index:1000;min-width:280px;}" - ".lws-preauth-widget.active{display:block;}"; + ".lws-preauth-widget.active{display:block;}" + ".lws-login-avatar-admin{color:#007bff;margin-right:6px;display:inline-block;vertical-align:middle;}" + ".lws-login-avatar-user{color:#333;margin-right:6px;display:inline-block;vertical-align:middle;}" + "@media(prefers-color-scheme:dark){.lws-login-avatar-user{color:#888;}}"; static const char * const canned_js = "window.lwsLoginSilentRefresh=async function(){" @@ -152,6 +155,10 @@ static const char * const canned_js = "window.lwsLoginRetry=0;" "var u='.lws-login-logout?redirect_uri='+encodeURIComponent(window.location.href);" "var a=st.is_admin?'':'';" + "var av='';" + "var gl=st.is_admin?255:(st.grant_level||0);" + "var ac=gl>=2?'lws-login-avatar-admin':'lws-login-avatar-user';" + "c+=''+av+'';" "c+='
';" "c+=a+' ';" "if(!st.has_grant&&!st.is_admin)c+='
';" @@ -644,8 +651,8 @@ callback_lws_login(struct lws *wsi, enum lws_callback_reasons reason, } if (!vhd) { - lwsl_info("%s: ALLOWING (vhd is NULL !!! protocol init failed?)\n", __func__); - return 0; + lwsl_err("%s: DENYING (vhd is NULL !!! protocol init failed or unconfigured)\n", __func__); + return 1; } if (vhd->unauth_protocols) { @@ -973,9 +980,24 @@ callback_lws_login(struct lws *wsi, enum lws_callback_reasons reason, } } - lws_snprintf(fq_uri, sizeof(fq_uri), "%s://%s%s", - lws_is_ssl(wsi) ? "https" : "http", - h ? h : "localhost", path); + { + const char *scheme = "http"; +#if defined(LWS_WITH_CUSTOM_HEADERS) + char proto[16] = ""; + + if (lws_hdr_custom_copy(wsi, proto, sizeof(proto), "x-forwarded-proto:", 18) > 0) { + if (!strcasecmp(proto, "https")) + scheme = "https"; + } else +#endif + if (lws_is_ssl(lws_get_network_wsi(wsi))) { + scheme = "https"; + } + + lws_snprintf(fq_uri, sizeof(fq_uri), "%s://%s%s", + scheme, + h ? h : "localhost", path); + } if (lws_add_http_common_headers(wsi, HTTP_STATUS_FOUND, "text/html", 0, (unsigned char **)&p, (unsigned char *)end)) return 1; if (lws_add_http_header_by_name(wsi, (unsigned char *)"set-cookie:", (unsigned char *)cookie, (int)strlen(cookie), (unsigned char **)&p, (unsigned char *)end)) return 1; @@ -1031,7 +1053,7 @@ callback_lws_login(struct lws *wsi, enum lws_callback_reasons reason, if (lws_login_ends_with(path, "/.lws-login-refresh")) { char csrf[64] = {0}; size_t csrf_len = sizeof(csrf); - char refresh_session[16] = {0}; + char refresh_session[128] = {0}; size_t refresh_session_len = sizeof(refresh_session); int ck_len; @@ -1119,10 +1141,25 @@ callback_lws_login(struct lws *wsi, enum lws_callback_reasons reason, } } - lws_snprintf(fq_uri, sizeof(fq_uri), "%s://%s%s", - lws_is_ssl(wsi) ? "https" : "http", - h ? h : "localhost", - path); + { + const char *scheme = "http"; +#if defined(LWS_WITH_CUSTOM_HEADERS) + char proto[16] = ""; + + if (lws_hdr_custom_copy(wsi, proto, sizeof(proto), "x-forwarded-proto:", 18) > 0) { + if (!strcasecmp(proto, "https")) + scheme = "https"; + } else +#endif + if (lws_is_ssl(lws_get_network_wsi(wsi))) { + scheme = "https"; + } + + lws_snprintf(fq_uri, sizeof(fq_uri), "%s://%s%s", + scheme, + h ? h : "localhost", + path); + } lws_urlencode(urlenc_path, fq_uri, sizeof(urlenc_path)); @@ -1156,8 +1193,17 @@ callback_lws_login(struct lws *wsi, enum lws_callback_reasons reason, char cookie_hdr1[256], cookie_hdr1_host[256]; char exp[64]; time_t t = 0; - struct tm *tm = gmtime(&t); - strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm); +#if defined(WIN32) || defined(_WIN32) + struct tm tmp; + struct tm *tm = gmtime_s(&tmp, &t) == 0 ? &tmp : NULL; +#else + struct tm tmp; + struct tm *tm = gmtime_r(&t, &tmp); +#endif + if (tm) + strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm); + else + exp[0] = '\0'; redirect_uri[0] = '\0'; if (lws_get_urlarg_by_name_safe(wsi, "redirect_uri=", redirect_uri, sizeof(redirect_uri)) >= 0) diff --git a/plugins/protocol_lws_mirror/protocol_lws_mirror.c b/plugins/protocol_lws_mirror/protocol_lws_mirror.c index d86f596ceb..3c549ba495 100644 --- a/plugins/protocol_lws_mirror/protocol_lws_mirror.c +++ b/plugins/protocol_lws_mirror/protocol_lws_mirror.c @@ -291,10 +291,6 @@ callback_lws_mirror(struct lws *wsi, enum lws_callback_reasons reason, lws_pthread_mutex_unlock(&v->lock); /* } vhost lock */ break; -bail1: - lws_pthread_mutex_unlock(&v->lock); /* } vhost lock */ - return 1; - case LWS_CALLBACK_CLOSED: /* detach our pss from the mirror instance */ mi = pss->mi; @@ -474,6 +470,10 @@ callback_lws_mirror(struct lws *wsi, enum lws_callback_reasons reason, } return 0; + +bail1: + lws_pthread_mutex_unlock(&v->lock); /* } vhost lock */ + return 1; } #define LWS_PLUGIN_PROTOCOL_MIRROR { \ diff --git a/plugins/protocol_lws_ssh_base/telnet.c b/plugins/protocol_lws_ssh_base/telnet.c index 33bf209152..681ae2b7eb 100644 --- a/plugins/protocol_lws_ssh_base/telnet.c +++ b/plugins/protocol_lws_ssh_base/telnet.c @@ -135,6 +135,8 @@ lws_callback_raw_telnet(struct lws *wsi, enum lws_callback_reasons reason, vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct per_vhost_data__telnet)); + if (!vhd) + return -1; vhd->context = lws_get_context(wsi); vhd->protocol = lws_get_protocol(wsi); vhd->vhost = lws_get_vhost(wsi); diff --git a/plugins/protocol_lws_webrtc/protocol_lws_webrtc.c b/plugins/protocol_lws_webrtc/protocol_lws_webrtc.c index 61967a7dba..2e120a7d73 100644 --- a/plugins/protocol_lws_webrtc/protocol_lws_webrtc.c +++ b/plugins/protocol_lws_webrtc/protocol_lws_webrtc.c @@ -629,7 +629,7 @@ lws_webrtc_create_offer(struct pss_webrtc *pss) /* Default PTs for Offer */ pss->media->pt_audio = 111; pss->media->pt_video_h264 = 102; - pss->media->pt_video_av1 = 104; + pss->media->pt_video_av1 = 0; pss->media->pt_video = pss->media->pt_video_h264; /* Default to H264 */ pss->media->rtp_ctx_video.ts = (uint32_t)(lws_now_usecs() * 9 / 100); @@ -651,7 +651,7 @@ lws_webrtc_create_offer(struct pss_webrtc *pss) /* Video Section */ lws_snprintf(video_m, sizeof(video_m), - "m=video %u UDP/TLS/RTP/SAVPF %u %u\\r\\n" + "m=video %u UDP/TLS/RTP/SAVPF %u\\r\\n" "c=IN IP4 0.0.0.0\\r\\n" "a=rtcp-mux\\r\\n" "a=ice-ufrag:%s\\r\\n" @@ -663,22 +663,16 @@ lws_webrtc_create_offer(struct pss_webrtc *pss) "a=msid:lws-stream lws-track-video\\r\\n" "a=rtpmap:%u H264/90000\\r\\n" "a=fmtp:%u level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e02a\\r\\n" - "a=rtpmap:%u AV1/90000\\r\\n" - "a=fmtp:%u profile=0;level-idx=5;tier=0\\r\\n" - "a=rtcp-fb:%u nack\\r\\n" - "a=rtcp-fb:%u nack pli\\r\\n" "a=rtcp-fb:%u nack\\r\\n" "a=rtcp-fb:%u nack pli\\r\\n" "a=ssrc:%u cname:lws-video\\r\\n" "a=ssrc:%u msid:lws-stream lws-track-video\\r\\n" "%s" "a=end-of-candidates\\r\\n", - vhd->udp_port, pss->media->pt_video_h264, pss->media->pt_video_av1, + vhd->udp_port, pss->media->pt_video_h264, pss->ice_ufrag, pss->ice_pwd, vhd->fingerprint, pss->media->pt_video_h264, pss->media->pt_video_h264, - pss->media->pt_video_av1, pss->media->pt_video_av1, pss->media->pt_video_h264, pss->media->pt_video_h264, - pss->media->pt_video_av1, pss->media->pt_video_av1, pss->media->ssrc_video, pss->media->ssrc_video, candidates); /* Audio Section */ @@ -829,7 +823,7 @@ handle_candidate(struct pss_webrtc *pss, struct vhd_webrtc *vhd, const char *can while (lws_tokenize(&ts) != LWS_TOKZE_ENDED) { lwsl_notice("%s: Token: '%.*s' (len %d, type %d), state %d\n", __func__, (int)ts.token_len, ts.token, (int)ts.token_len, ts.e, state); - if (state == 0 && ts.token_len == 3 && !strncmp(ts.token, "udp", 3)) { + if (state == 0 && ts.token_len == 3 && !strncasecmp(ts.token, "udp", 3)) { state = 1; /* Found Protocol udp */ } else if (state == 1) { /* Priority */ @@ -853,6 +847,11 @@ handle_candidate(struct pss_webrtc *pss, struct vhd_webrtc *vhd, const char *can if (state == 5 && port > 0) { webrtc_pss_log(pss, "Found ICE Candidate: %s:%d\n", ip_str, port); + if (pss->media && pss->media->peer_stun_received) { + webrtc_pss_log(pss, "Skipping candidate parsing as ICE is already resolved via STUN.\n"); + return 0; + } + if (lws_sa46_parse_numeric_address(ip_str, &pss->media->peer_sa46) < 0) return -1; sa46_sockport(&pss->media->peer_sa46, htons((uint16_t)port)); @@ -1003,7 +1002,7 @@ lws_webrtc_parse_sdp_codecs(struct pss_webrtc *pss, const char *sdp_clean) if (lws_tokenize(&ts) == LWS_TOKZE_TOKEN) { if (!strncasecmp(ts.token, "H264/90000", 10)) { /* We found H264. Map already populated in Pass 1. */ - } else if (!strncasecmp(ts.token, "AV1/90000", 9)) { + } else if (0 && !strncasecmp(ts.token, "AV1/90000", 9)) { pss->media->pt_video_av1 = (uint8_t)pt; } else if (!strncasecmp(ts.token, "VP9/90000", 9)) { } else if (!strncasecmp(ts.token, "opus/48000", 10)) { @@ -1340,7 +1339,7 @@ handle_offer(struct lws *wsi, struct pss_webrtc *pss, struct vhd_webrtc *vhd, co lwsl_warn(" SDP Parsing: PT %d -> Token '%.*s'\n", pt, (int)ts.token_len, ts.token); if (!strncasecmp(ts.token, "H264/90000", 10)) { /* We found H264. Map already populated in Pass 1. */ - } else if (!strncasecmp(ts.token, "AV1/90000", 9)) { + } else if (0 && !strncasecmp(ts.token, "AV1/90000", 9)) { pss->media->pt_video_av1 = (uint8_t)pt; lwsl_info(" Found AV1 PT: %d\n", pt); } else if (!strncasecmp(ts.token, "VP9/90000", 9)) { @@ -1733,9 +1732,14 @@ lws_shared_webrtc_callback(struct lws *wsi, enum lws_callback_reasons reason, lwsl_notice("%s: Generating self-signed certificate (this may take a few seconds)...\n", __func__); lws_usec_t t1 = lws_now_usecs(); - if (lws_x509_create_self_signed(vhd->context, &vhd->cert_mem, &vhd->cert_len, - &vhd->key_mem, &vhd->key_len, - vhd->external_ip, 2048)) { + struct lws_x509_cert_gen_info cert_info; + memset(&cert_info, 0, sizeof(cert_info)); + cert_info.san = vhd->external_ip[0] ? vhd->external_ip : "localhost"; + cert_info.curve_name = "P-256"; + cert_info.is_server = 1; + + if (lws_x509_create_cert(vhd->context, &vhd->cert_mem, &vhd->cert_len, + &vhd->key_mem, &vhd->key_len, &cert_info)) { lwsl_err("%s: Cert generation failed\n", __func__); return -1; } @@ -1753,7 +1757,7 @@ lws_shared_webrtc_callback(struct lws *wsi, enum lws_callback_reasons reason, } vhd->wsi_udp = lws_create_adopt_udp(vhd->vhost, - NULL, vhd->udp_port, LWS_CAUDP_BIND, + "0.0.0.0", vhd->udp_port, LWS_CAUDP_BIND, "lws-webrtc-udp", NULL, NULL, NULL, NULL, NULL); if (!vhd->wsi_udp) { lwsl_err("%s: UDP socket creation failed\n", __func__); @@ -2035,6 +2039,7 @@ webrtc_handle_stun(struct lws *wsi, struct vhd_webrtc *vhd, struct pss_webrtc ** if (pss->media) { pss->media->peer_sa46 = udp_desc->sa46; pss->media->has_peer_sa46 = 1; + pss->media->peer_stun_received = 1; } int fd = (int)(lws_intptr_t)lws_get_socket_fd(wsi); @@ -2350,6 +2355,7 @@ lws_shared_webrtc_udp_callback(struct lws *wsi, enum lws_callback_reasons reason case LWS_CALLBACK_RAW_RX: if (!vhd || !udp_desc) return 0; + lwsl_notice("%s: UDP packet received on port %u (len %zu)\n", __func__, vhd->udp_port, len); /* Create pure IPv4 mapping for logic checks */ struct sockaddr_in pure_sin; diff --git a/plugins/protocol_lws_webrtc/protocol_lws_webrtc.h b/plugins/protocol_lws_webrtc/protocol_lws_webrtc.h index 61a391b8e2..c81b59094e 100644 --- a/plugins/protocol_lws_webrtc/protocol_lws_webrtc.h +++ b/plugins/protocol_lws_webrtc/protocol_lws_webrtc.h @@ -43,6 +43,7 @@ struct lws_webrtc_peer_media { struct lws *wsi_udp; lws_sockaddr46 peer_sa46; int has_peer_sa46; + uint8_t peer_stun_received; struct lws_rtp_ctx rtp_ctx_video; struct lws_rtp_ctx rtp_ctx_audio; diff --git a/plugins/protocol_lws_webrtc_mixer/assets/main.js b/plugins/protocol_lws_webrtc_mixer/assets/main.js index 258ead2d49..6b7a9db7a3 100644 --- a/plugins/protocol_lws_webrtc_mixer/assets/main.js +++ b/plugins/protocol_lws_webrtc_mixer/assets/main.js @@ -115,6 +115,12 @@ function updateView() { window.addEventListener('resize', adjustOverlaySize); displayVideo.addEventListener('resize', adjustOverlaySize); displayVideo.addEventListener('loadedmetadata', adjustOverlaySize); + displayVideo.addEventListener('click', () => { + if (displayVideo.paused) { + console.log("Video clicked, attempting manual play..."); + displayVideo.play().catch(e => console.error("Manual play failed:", e)); + } + }); startFPSMonitor(); } diff --git a/plugins/protocol_lws_webrtc_mixer/mixer-media.c b/plugins/protocol_lws_webrtc_mixer/mixer-media.c index dc94026b70..bad57a444b 100644 --- a/plugins/protocol_lws_webrtc_mixer/mixer-media.c +++ b/plugins/protocol_lws_webrtc_mixer/mixer-media.c @@ -1518,24 +1518,46 @@ init_participant_media(struct participant *p, enum lws_video_codec codec) lws_snprintf(n_cfilt, sizeof(n_cfilt), "cfilt_%p", s); s->appsrc = gst_element_factory_make("appsrc", n_appsrc); - s->decodebin = gst_element_factory_make("avdec_h264", n_dec); + + GstElement *decoder = NULL; + GstElement *parser = NULL; + GstCaps *caps = NULL; + + if (codec == LWS_WEBRTC_CODEC_AV1) { + decoder = gst_element_factory_make("dav1ddec", n_dec); + if (!decoder) + decoder = gst_element_factory_make("avdec_av1", n_dec); + parser = gst_element_factory_make("av1parse", n_parse); + caps = gst_caps_new_empty_simple("video/x-av1"); + } else { + decoder = gst_element_factory_make("avdec_h264", n_dec); + parser = gst_element_factory_make("h264parse", n_parse); + caps = gst_caps_new_simple("video/x-h264", + "stream-format", G_TYPE_STRING, "byte-stream", + "alignment", G_TYPE_STRING, "au", + NULL); + } + + s->decodebin = decoder; + if (!s->decodebin) { - lwsl_err("%s: Critical: avdec_h264 not found, cannot build static chain\n", __func__); + lwsl_err("%s: Critical: Decoder not found for codec %d, cannot build static chain\n", __func__, codec); + if (caps) gst_caps_unref(caps); return -1; } + GstElement *que = gst_element_factory_make("queue", n_que); g_object_set(G_OBJECT(que), "max-size-buffers", 0, "max-size-time", (guint64)0, "max-size-bytes", (guint)0, NULL); - GstElement *h264parse = gst_element_factory_make("h264parse", n_parse); - GstElement *deint = gst_element_factory_make("deinterlace", n_deint); GstElement *vconv = gst_element_factory_make("videoconvert", n_vconv); GstElement *vscale = gst_element_factory_make("videoscale", n_vscale); GstElement *vrate = gst_element_factory_make("videorate", n_vrate); GstElement *cfilter = gst_element_factory_make("capsfilter", n_cfilt); - if (!s->appsrc || !s->decodebin || !que || !h264parse || !deint || !vconv || !vscale || !vrate || !cfilter) { + if (!s->appsrc || !s->decodebin || !que || !parser || !deint || !vconv || !vscale || !vrate || !cfilter) { lwsl_err("%s: Failed to create GStreamer elements\n", __func__); + if (caps) gst_caps_unref(caps); return -1; } @@ -1544,10 +1566,6 @@ init_participant_media(struct participant *p, enum lws_video_codec codec) g_object_set(G_OBJECT(cfilter), "caps", icaps, NULL); gst_caps_unref(icaps); - GstCaps *caps = gst_caps_new_simple("video/x-h264", - "stream-format", G_TYPE_STRING, "byte-stream", - "alignment", G_TYPE_STRING, "au", - NULL); g_object_set(G_OBJECT(s->appsrc), "caps", caps, "format", GST_FORMAT_TIME, "is-live", TRUE, "do-timestamp", FALSE, NULL); gst_caps_unref(caps); @@ -1564,10 +1582,10 @@ init_participant_media(struct participant *p, enum lws_video_codec codec) /* Set compositor pad to be as lenient as possible */ g_object_set(G_OBJECT(p->room->compositor), "latency", (GstClockTime)0, NULL); - gst_bin_add_many(GST_BIN(p->room->pipeline), s->appsrc, que, h264parse, s->decodebin, deint, vconv, vscale, vrate, cfilter, NULL); + gst_bin_add_many(GST_BIN(p->room->pipeline), s->appsrc, que, parser, s->decodebin, deint, vconv, vscale, vrate, cfilter, NULL); - /* Direct link: appsrc -> que -> h264parse -> avdec_h264 -> deinterlace -> vconv -> vscale -> videorate -> cfilter */ - if (!gst_element_link_many(s->appsrc, que, h264parse, s->decodebin, deint, vconv, vscale, vrate, cfilter, NULL)) { + /* Direct link: appsrc -> que -> parser -> decoder -> deinterlace -> vconv -> vscale -> videorate -> cfilter */ + if (!gst_element_link_many(s->appsrc, que, parser, s->decodebin, deint, vconv, vscale, vrate, cfilter, NULL)) { lwsl_err("%s: Failed to link static participant chain\n", __func__); } @@ -1591,7 +1609,7 @@ init_participant_media(struct participant *p, enum lws_video_codec codec) gst_element_sync_state_with_parent(s->appsrc); gst_element_sync_state_with_parent(que); - gst_element_sync_state_with_parent(h264parse); + gst_element_sync_state_with_parent(parser); gst_element_sync_state_with_parent(s->decodebin); gst_element_sync_state_with_parent(deint); gst_element_sync_state_with_parent(vconv); diff --git a/plugins/protocol_urlarg/protocol_urlarg.c b/plugins/protocol_urlarg/protocol_urlarg.c index 243398ab2e..20af41d1fd 100644 --- a/plugins/protocol_urlarg/protocol_urlarg.c +++ b/plugins/protocol_urlarg/protocol_urlarg.c @@ -101,12 +101,7 @@ callback_lws_urlarg(struct lws *wsi, enum lws_callback_reasons reason, pss->wlen = pss->alen; - -bail: - if (lws_http_transaction_completed(wsi)) - return -1; - - return 0; + goto bail; } break; @@ -115,6 +110,12 @@ callback_lws_urlarg(struct lws *wsi, enum lws_callback_reasons reason, } return 0; + +bail: + if (lws_http_transaction_completed(wsi)) + return -1; + + return 0; } #define LWS_PLUGIN_PROTOCOL_URLARG { \ diff --git a/plugins/protocol_webtransport_shared_world/assets/app2.js b/plugins/protocol_webtransport_shared_world/assets/app2.js index 9eb5c8b1d5..b15d9b4367 100644 --- a/plugins/protocol_webtransport_shared_world/assets/app2.js +++ b/plugins/protocol_webtransport_shared_world/assets/app2.js @@ -7,7 +7,27 @@ let shadowGenerator = null; let sunLight = null; let skyDome = null; let myAvatar = null; +let camera = null; let currentSeed = 12345; +let myPlayerId = null; + +const otherAvatars = {}; +const keys = {}; + +let localX = 0; +let localZ = 0; +let localAngle = 0; +let localIsMoving = false; + +const stats = { + sentCount: 0, + recvCount: 0, + lastSent: "", + lastRecv: "" +}; + +window.addEventListener("keydown", (e) => { keys[e.code] = true; }); +window.addEventListener("keyup", (e) => { keys[e.code] = false; }); async function initEngine() { try { @@ -30,24 +50,11 @@ function initScene() { const skyColor = new BABYLON.Color3(0.7, 0.8, 0.9); scene.clearColor = new BABYLON.Color4(skyColor.r, skyColor.g, skyColor.b, 1.0); - const camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 100, -40), scene); // Start high to avoid spawning underground - camera.setTarget(BABYLON.Vector3.Zero()); - camera.attachControl(canvas, true); - - // WASD Controls - camera.keysUp.push(87); // W - camera.keysDown.push(83); // S - camera.keysLeft.push(65); // A - camera.keysRight.push(68); // D - camera.speed = 2.0; + camera = new BABYLON.TargetCamera("camera1", new BABYLON.Vector3(0, 100, -40), scene); // Start high to avoid spawning underground camera.maxZ = 4000; - // Enable Collisions and Gravity scene.collisionsEnabled = true; scene.gravity = new BABYLON.Vector3(0, -9.81, 0); - camera.checkCollisions = true; - camera.applyGravity = true; - camera.ellipsoid = new BABYLON.Vector3(2, 5, 2); // Click to capture mouse (Pointer Lock) canvas.addEventListener("click", () => { @@ -81,6 +88,26 @@ function initScene() { skyDome.material = skyMat; } +function updateCamera() { + if (!myAvatar) return; + const avatarPos = myAvatar.pathWrapper.position; + const angle = myAvatar.pathWrapper.rotation.y; + + const distance = 80; + const height = 30; + + const camX = avatarPos.x - Math.sin(angle) * distance; + const camZ = avatarPos.z - Math.cos(angle) * distance; + const camY = getTerrainHeight(camX, camZ, currentSeed) + height; + + // Smooth follow + camera.position.x = BABYLON.Scalar.Lerp(camera.position.x, camX, 0.1); + camera.position.y = BABYLON.Scalar.Lerp(camera.position.y, camY, 0.1); + camera.position.z = BABYLON.Scalar.Lerp(camera.position.z, camZ, 0.1); + + camera.setTarget(avatarPos); +} + function hash2D(x, y, seed) { let n = (Math.imul(x, 137) + Math.imul(y, 149) + seed) | 0; n = (n ^ (n >>> 13)) | 0; @@ -117,14 +144,12 @@ function fbm(x, y, octaves, seed) { for (let i = 0; i < octaves; i++) { total += smoothNoise2D(x * frequency, y * frequency, seed + i) * amplitude; maxVal += amplitude; - amplitude *= 0.5; frequency *= 2.0; } return total / maxVal; } -// Function to calculate exact terrain height at any point function getTerrainHeight(px, pz, seed) { let n = fbm(px * 0.005, pz * 0.005, 4, seed); const distFromCenter = Math.sqrt(px * px + pz * pz); @@ -213,15 +238,11 @@ function buildWorld(seed) { class Avatar { constructor(meshes, animationGroups, skeletons, scene) { - // Create a parent wrapper to handle translation and lookAt along the circle this.pathWrapper = new BABYLON.TransformNode("pathWrapper", scene); - - // Create an inner wrapper (kept for clean hierarchy, but with 0 rotation since the model faces +Z natively) this.offsetWrapper = new BABYLON.TransformNode("offsetWrapper", scene); this.offsetWrapper.parent = this.pathWrapper; this.offsetWrapper.rotation = new BABYLON.Vector3(0, 0, 0); - // Parent ALL imported root meshes to the offsetWrapper (this ensures the skeleton/bones are rotated too!) meshes.forEach(m => { if (!m.parent) { m.parent = this.offsetWrapper; @@ -232,53 +253,70 @@ class Avatar { m.receiveShadows = true; }); - // The overall scale can be applied to the pathWrapper this.pathWrapper.scaling = new BABYLON.Vector3(10, 10, 10); - // Try playing via AnimationGroups (newer glTF/GLB) - if (animationGroups && animationGroups.length > 0) { - const walkAnim = animationGroups.find(ag => ag.name.toLowerCase().includes("walk")) || animationGroups[0]; - walkAnim.play(true); - } - // Fallback to older .babylon skeleton animations (like dummy3.babylon) - else if (skeletons && skeletons.length > 0) { - const skeleton = skeletons[0]; - const ranges = skeleton.getAnimationRanges(); - let walkRange = null; - + this.walkRange = null; + this.skeleton = null; + if (skeletons && skeletons.length > 0) { + this.skeleton = skeletons[0]; + const ranges = this.skeleton.getAnimationRanges(); if (ranges && ranges.length > 0) { - walkRange = ranges.find(r => r.name.toLowerCase().includes("walk")) || ranges[0]; + this.walkRange = ranges.find(r => r.name.toLowerCase().includes("walk")) || ranges[0]; } - - if (walkRange) { - scene.beginAnimation(skeleton, walkRange.from, walkRange.to, true, 1.0); - } else { - scene.beginAnimation(skeleton, 0, 100, true, 1.0); + } + + this.animControl = null; + this.isMoving = false; + this.speed = 100.0; + this.rotationSpeed = 3.0; + } + + setMoving(moving) { + if (this.isMoving === moving) return; + this.isMoving = moving; + if (moving) { + if (this.walkRange && this.skeleton) { + this.animControl = scene.beginAnimation(this.skeleton, this.walkRange.from, this.walkRange.to, true, 1.0); + } + } else { + if (this.animControl) { + this.animControl.pause(); } } + } - this.angle = 0; - this.radius = 20; // Radius of the circle to walk in - this.speed = 0.5; - this.center = new BABYLON.Vector3(0, 0, 0); + dispose() { + if (this.pathWrapper) { + this.pathWrapper.dispose(); + } } +} - update(deltaTime) { - if (!this.pathWrapper) return; - - this.angle -= (this.speed * deltaTime) / 1000.0; - - const px = this.center.x + Math.cos(this.angle) * this.radius; - const pz = this.center.z + Math.sin(this.angle) * this.radius; - const py = getTerrainHeight(px, pz, currentSeed); - - this.pathWrapper.position = new BABYLON.Vector3(px, py, pz); +function colorAvatar(avatar, id) { + try { + if (!avatar || !avatar.pathWrapper) return; + console.log("Coloring avatar for player ID:", id); + const hue = (id * 137.5) % 360; + const color = BABYLON.Color3.FromHSV(hue, 0.8, 0.8); - // Point the pathWrapper in the direction of travel (tangent of the circle) - const tx = this.center.x + Math.cos(this.angle - 0.1) * this.radius; - const tz = this.center.z + Math.sin(this.angle - 0.1) * this.radius; - const ty = getTerrainHeight(tx, tz, currentSeed); - this.pathWrapper.lookAt(new BABYLON.Vector3(tx, ty, tz)); + const meshes = avatar.pathWrapper.getChildMeshes(); + meshes.forEach(m => { + if (m.material) { + try { + m.material = m.material.clone("mat-" + id); + if (m.material.diffuseColor !== undefined) { + m.material.diffuseColor = color; + } + if (m.material.albedoColor !== undefined) { + m.material.albedoColor = color; + } + } catch (e) { + console.warn("Could not clone or color material for mesh", m.name, e); + } + } + }); + } catch (e) { + console.error("Error in colorAvatar:", e); } } @@ -293,27 +331,97 @@ async function loadAvatar() { return; } myAvatar = new Avatar(result.meshes, result.animationGroups, result.skeletons, scene); - console.log("Avatar loaded successfully!"); + console.log("Local Avatar loaded successfully!"); + if (myPlayerId !== null) { + colorAvatar(myAvatar, myPlayerId); + } } catch (e) { alert("Failed to load dummy3.babylon. Did you run 'cmake .. && make install'? Error: " + e.message); console.error("Failed to load avatar model:", e); } } +async function spawnRemoteAvatar(id, x, z, angle, isMoving) { + if (otherAvatars[id]) return; + + console.log("Spawning remote avatar placeholder for player ID:", id, "at", x, z); + const placeholder = BABYLON.MeshBuilder.CreateSphere("placeholder-" + id, {diameter: 5}, scene); + placeholder.position = new BABYLON.Vector3(x, getTerrainHeight(x, z, currentSeed), z); + const mat = new BABYLON.StandardMaterial("placeholder-mat-" + id, scene); + mat.diffuseColor = new BABYLON.Color3(0.8, 0.2, 0.2); + placeholder.material = mat; + + otherAvatars[id] = { placeholder, avatar: null }; + + let path = window.location.pathname; + if (!path.endsWith('/')) path += '/'; + + try { + const result = await BABYLON.SceneLoader.ImportMeshAsync("", path, "dummy3.babylon", scene); + if (otherAvatars[id]) { + const currentPos = placeholder.position.clone(); + placeholder.dispose(); + + const avatar = new Avatar(result.meshes, result.animationGroups, result.skeletons, scene); + avatar.pathWrapper.position = currentPos; + avatar.pathWrapper.rotation.y = angle; + avatar.setMoving(isMoving); + + otherAvatars[id].avatar = avatar; + otherAvatars[id].placeholder = null; + colorAvatar(avatar, id); + console.log("Remote avatar mesh spawned successfully for player ID:", id); + } else { + result.meshes.forEach(m => m.dispose()); + } + } catch (e) { + console.error("Failed to load remote avatar:", e); + } +} + +function updateRemoteAvatar(id, x, z, angle, isMoving) { + const entry = otherAvatars[id]; + if (entry) { + if (entry.avatar) { + entry.avatar.pathWrapper.position = new BABYLON.Vector3(x, getTerrainHeight(x, z, currentSeed), z); + entry.avatar.pathWrapper.rotation.y = angle; + entry.avatar.setMoving(isMoving); + } else if (entry.placeholder) { + entry.placeholder.position = new BABYLON.Vector3(x, getTerrainHeight(x, z, currentSeed), z); + } + } else { + spawnRemoteAvatar(id, x, z, angle, isMoving); + } +} + +function removeRemoteAvatar(id) { + const entry = otherAvatars[id]; + if (entry) { + if (entry.placeholder) entry.placeholder.dispose(); + if (entry.avatar) entry.avatar.dispose(); + delete otherAvatars[id]; + console.log("Remote avatar removed for player ID:", id); + } +} + function tryReload() { + if (window.isReloading) return; + window.isReloading = true; console.log("Server disconnected. Waiting for it to come back..."); + const checkServer = async () => { try { - const u = new URL(window.location.href); - if (u.origin !== window.location.origin) { - return; - } - const resp = await fetch(u.href, { method: "HEAD" }); - if (resp.ok) { - window.location.reload(); - } else { - setTimeout(checkServer, 2000); + const pathname = window.location.pathname; + if (/^[a-zA-Z0-9\/\-_\.]+$/.test(pathname)) { + const testUrl = pathname + "?cb=" + Date.now(); + const resp = await fetch(testUrl, { method: "HEAD", cache: "no-store" }); + if (resp.ok) { + console.log("Server is back! Reloading page..."); + window.location.reload(); + return; + } } + setTimeout(checkServer, 2000); } catch (e) { setTimeout(checkServer, 2000); } @@ -321,20 +429,179 @@ function tryReload() { setTimeout(checkServer, 2000); } +function updateOverlay() { + const overlay = document.getElementById("connection-overlay"); + if (!overlay) return; + + let typeText = "Connecting..."; + let typeClass = "connection-overlay"; + if (wtWriter) { + typeText = "WebTransport (HTTP/3)"; + typeClass = "connection-overlay wt"; + } else if (wsConn && wsConn.readyState === WebSocket.OPEN) { + typeText = "WebSocket (WSS)"; + typeClass = "connection-overlay ws"; + } + + overlay.className = typeClass; + overlay.innerHTML = ` +
${typeText}
+
+
Sent: ${stats.sentCount}
+
Recv: ${stats.recvCount}
+
+
+
Last Sent: ${stats.lastSent || 'None'}
+
Last Recv: ${stats.lastRecv || 'None'}
+
+ `; +} + +function handleServerMessage(msg) { + console.log("handleServerMessage received raw:", msg); + const parts = msg.split(/}\s*{/); + for (let i = 0; i < parts.length; i++) { + let part = parts[i].trim(); + if (!part) continue; + if (i > 0) part = "{" + part; + if (i < parts.length - 1) part = part + "}"; + + try { + const obj = JSON.parse(part); + console.log("Parsed JSON message:", obj); + stats.recvCount++; + stats.lastRecv = part; + updateOverlay(); + + if (obj.seed) { + myPlayerId = obj.player_id; + console.log("Welcome message. Player ID:", myPlayerId, "Seed:", obj.seed, "Players in list:", obj.players); + if (myAvatar) { + colorAvatar(myAvatar, myPlayerId); + } + if (obj.seed !== currentSeed) { + currentSeed = obj.seed; + console.log("Received new seed:", currentSeed); + buildWorld(currentSeed); + } + if (obj.players) { + obj.players.forEach(p => { + console.log("Spawning existing player ID:", p.id, "at pos", p.x, p.z); + spawnRemoteAvatar(p.id, p.x, p.z, p.angle, p.isMoving); + }); + } + } else if (obj.join !== undefined) { + console.log("Player ID joined:", obj.join); + if (obj.join === myPlayerId) { + if (myAvatar) { + myAvatar.pathWrapper.position = new BABYLON.Vector3(0, getTerrainHeight(0, 0, currentSeed), 0); + } + } else { + spawnRemoteAvatar(obj.join, 0, 0, 0, false); + } + } else if (obj.leave !== undefined) { + console.log("Player ID left:", obj.leave); + removeRemoteAvatar(obj.leave); + } else if (obj.player_id !== undefined) { + console.log("Player ID update:", obj.player_id, "Pos:", obj.x, obj.z, "Angle:", obj.angle, "isMoving:", obj.isMoving); + if (obj.player_id === myPlayerId) { + if (myAvatar) { + myAvatar.pathWrapper.position = new BABYLON.Vector3(obj.x, getTerrainHeight(obj.x, obj.z, currentSeed), obj.z); + myAvatar.pathWrapper.rotation.y = obj.angle; + myAvatar.setMoving(obj.isMoving); + } + } else { + updateRemoteAvatar(obj.player_id, obj.x, obj.z, obj.angle, obj.isMoving); + } + } + } catch (e) { + console.error("Error parsing JSON part:", part, e); + } + } +} + +let wtWriter = null; +let wsConn = null; + +function sendUpdate(obj) { + const msg = JSON.stringify(obj); + stats.sentCount++; + stats.lastSent = msg; + updateOverlay(); + + if (wtWriter) { + const encoder = new TextEncoder(); + wtWriter.write(encoder.encode(msg)).catch(e => console.error("WT write error:", e)); + } else if (wsConn && wsConn.readyState === WebSocket.OPEN) { + wsConn.send(msg); + } +} + +let lastSentX = -999; +let lastSentZ = -999; +let lastSentAngle = -999; +let lastSentMoving = false; + +setInterval(() => { + // 1. Calculate intended position change from keys + const deltaTime = 100; // tick interval + const dtSec = deltaTime / 1000.0; + let moving = false; + const speed = 100.0; + const rotationSpeed = 3.0; + + if (keys["KeyW"]) { + localX += Math.sin(localAngle) * speed * dtSec; + localZ += Math.cos(localAngle) * speed * dtSec; + moving = true; + } + if (keys["KeyS"]) { + localX -= Math.sin(localAngle) * speed * dtSec; + localZ -= Math.cos(localAngle) * speed * dtSec; + moving = true; + } + if (keys["KeyA"]) { + localAngle -= rotationSpeed * dtSec; + moving = true; + } + if (keys["KeyD"]) { + localAngle += rotationSpeed * dtSec; + moving = true; + } + localIsMoving = moving; + + // 2. Check if anything changed from last sent + if (myPlayerId !== null && (Math.abs(localX - lastSentX) > 0.05 || + Math.abs(localZ - lastSentZ) > 0.05 || + Math.abs(localAngle - lastSentAngle) > 0.01 || + localIsMoving !== lastSentMoving)) { + + sendUpdate({ + x: Number.parseFloat(localX.toFixed(2)), + z: Number.parseFloat(localZ.toFixed(2)), + angle: Number.parseFloat(localAngle.toFixed(2)), + isMoving: localIsMoving + }); + + lastSentX = localX; + lastSentZ = localZ; + lastSentAngle = localAngle; + lastSentMoving = localIsMoving; + } +}, 100); + async function connectAndInit() { await initEngine(); initScene(); currentSeed = 12345; buildWorld(currentSeed); - - // Kick off avatar loading loadAvatar(); engine.runRenderLoop(function () { scene.render(); if (myAvatar) { - myAvatar.update(engine.getDeltaTime()); + updateCamera(); } }); @@ -349,15 +616,18 @@ async function connectAndInit() { const wtUrl = `https://${host}${path}`; let connected = false; + updateOverlay(); if (typeof WebTransport !== 'undefined') { try { console.log("Attempting WebTransport connection to", wtUrl); - const transport = new WebTransport(wtUrl); + const transport = new WebTransport(wtUrl, { + protocols: ['webtransport-shared-world'] + }); await Promise.race([ transport.ready, - new Promise((_, reject) => setTimeout(() => reject(new Error("WT timeout")), 3000)) + new Promise((_, reject) => setTimeout(() => reject(new Error("WT timeout")), 10000)) ]); console.log("WebTransport connected!"); @@ -365,30 +635,40 @@ async function connectAndInit() { const stream = await transport.createBidirectionalStream(); if (stream) { const streamReader = stream.readable.getReader(); - const { value: data, done } = await streamReader.read(); - if (done) { - console.log("WT stream closed, waiting to reload..."); - tryReload(); - } else if (data) { - const msg = new TextDecoder().decode(data); - const obj = JSON.parse(msg); - if (obj.seed && obj.seed !== currentSeed) { - currentSeed = obj.seed; - console.log("Received WT seed:", currentSeed); - buildWorld(currentSeed); + wtWriter = stream.writable.getWriter(); + connected = true; + updateOverlay(); + + (async () => { + try { + while (true) { + const { value: data, done } = await streamReader.read(); + if (done) { + console.log("WT stream done, reloading..."); + tryReload(); + break; + } + if (data) { + const msg = new TextDecoder().decode(data); + handleServerMessage(msg); + } + } + } catch (e) { + console.error("WT read error:", e); + tryReload(); } - } + })(); } - connected = true; - // Handle WT close - transport.closed.then(() => { - console.log("WT connection closed, waiting to reload..."); - tryReload(); - }).catch(() => { - console.log("WT connection error, waiting to reload..."); + (async () => { + try { + await transport.closed; + console.log("WT session closed cleanly, reloading..."); + } catch (e) { + console.warn("WT session closed with error, reloading...", e); + } tryReload(); - }); + })(); } catch (e) { console.warn("WebTransport connection failed or timed out:", e); @@ -399,27 +679,29 @@ async function connectAndInit() { try { console.log("Attempting WebSocket connection to", wsUrl); await new Promise((resolve, reject) => { - const ws = new WebSocket(wsUrl, "webtransport-shared-world"); - ws.onmessage = (event) => { + wsConn = new WebSocket(wsUrl, "webtransport-shared-world"); + wsConn.onmessage = (event) => { try { - const obj = JSON.parse(event.data); - if (obj.seed && obj.seed !== currentSeed) { - currentSeed = obj.seed; - console.log("Received WS seed:", currentSeed); - buildWorld(currentSeed); - connected = true; - } + handleServerMessage(event.data); } catch (e) { console.error("Invalid WS message", e); } - resolve(); }; - ws.onerror = reject; - ws.onclose = () => { - console.log("WS connection closed, waiting to reload..."); + wsConn.onerror = (err) => { + console.error("WebSocket error:", err); tryReload(); + reject(new Error("WebSocket connection failed")); + }; + wsConn.onclose = () => { + console.log("WS connection closed, reloading..."); + tryReload(); + }; + wsConn.onopen = () => { + console.log("WebSocket connected!"); + connected = true; + updateOverlay(); + resolve(); }; - ws.onopen = () => console.log("WebSocket connected!"); setTimeout(() => { if (!connected) reject(new Error("WS timeout")); }, 3000); }); diff --git a/plugins/protocol_webtransport_shared_world/assets/index.html b/plugins/protocol_webtransport_shared_world/assets/index.html index daa136e978..8d0d37c348 100644 --- a/plugins/protocol_webtransport_shared_world/assets/index.html +++ b/plugins/protocol_webtransport_shared_world/assets/index.html @@ -7,6 +7,7 @@ +
Connecting...
diff --git a/plugins/protocol_webtransport_shared_world/assets/style.css b/plugins/protocol_webtransport_shared_world/assets/style.css index 9405c05cc0..42eb4bb32a 100644 --- a/plugins/protocol_webtransport_shared_world/assets/style.css +++ b/plugins/protocol_webtransport_shared_world/assets/style.css @@ -13,3 +13,76 @@ html, body { touch-action: none; outline: none; } + +.connection-overlay { + position: absolute; + top: 20px; + right: 20px; + width: 320px; + padding: 15px; + background: rgba(15, 15, 25, 0.75); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + color: #f0f0f5; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 13px; + border-radius: 12px; + pointer-events: none; + z-index: 10; + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.connection-overlay.wt { + border-color: rgba(46, 204, 113, 0.4); + box-shadow: 0 8px 32px 0 rgba(46, 204, 113, 0.1); +} + +.connection-overlay.ws { + border-color: rgba(241, 196, 15, 0.4); + box-shadow: 0 8px 32px 0 rgba(241, 196, 15, 0.1); +} + +.overlay-title { + font-size: 15px; + font-weight: 700; + margin-bottom: 10px; + letter-spacing: 0.5px; +} + +.connection-overlay.wt .overlay-title { + color: #2ecc71; +} + +.connection-overlay.ws .overlay-title { + color: #f1c40f; +} + +.overlay-stats { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.overlay-json { + display: flex; + flex-direction: column; + gap: 8px; +} + +.json-text { + display: block; + font-family: monospace; + font-size: 11px; + background: rgba(0, 0, 0, 0.4); + padding: 6px; + border-radius: 6px; + word-break: break-all; + max-height: 80px; + overflow-y: auto; + border: 1px solid rgba(255, 255, 255, 0.05); + color: #a2a2d0; +} diff --git a/plugins/protocol_webtransport_shared_world/protocol_webtransport_shared_world.c b/plugins/protocol_webtransport_shared_world/protocol_webtransport_shared_world.c index 06b72d81ab..7f5f33ab14 100644 --- a/plugins/protocol_webtransport_shared_world/protocol_webtransport_shared_world.c +++ b/plugins/protocol_webtransport_shared_world/protocol_webtransport_shared_world.c @@ -18,15 +18,35 @@ #endif #include #include +#include + +struct msg { + char payload[LWS_PRE + 256]; + size_t len; + uint32_t sender_id; +}; struct vhd__shared_world { + lws_dll2_owner_t sessions; + struct lws_ring *ring; uint32_t seed; + uint32_t next_player_id; }; struct pss__shared_world { + lws_dll2_t list; + struct lws *wsi; + uint32_t player_id; + uint32_t tail; + double x; + double z; + double angle; + double speed; int seed_sent; + int is_moving; }; + static int callback_shared_world(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) @@ -34,45 +54,245 @@ callback_shared_world(struct lws *wsi, enum lws_callback_reasons reason, struct pss__shared_world *pss = (struct pss__shared_world *)user; struct vhd__shared_world *vhd = (struct vhd__shared_world *) lws_protocol_vh_priv_get(lws_get_vhost(wsi), lws_get_protocol(wsi)); - uint8_t buf[LWS_PRE + 128]; - int m; + uint8_t buf[LWS_PRE + 4096]; switch (reason) { case LWS_CALLBACK_PROTOCOL_INIT: + if (!in) + return 0; + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct vhd__shared_world)); if (!vhd) return -1; + + vhd->ring = lws_ring_create(sizeof(struct msg), 32, NULL); + if (!vhd->ring) + return -1; + + lws_dll2_owner_clear(&vhd->sessions); + vhd->next_player_id = 0; { struct lws_xos xos; - lws_xos_init(&xos, 0x12345678); /* Simple predictable seed for PRNG to gen actual world seed */ + lws_xos_init(&xos, 0x12345678); vhd->seed = (uint32_t)lws_xos(&xos); } - lwsl_notice("Shared World Plugin Initialized, Seed: %u\n", vhd->seed); + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + if (vhd) { + if (vhd->ring) { + lws_ring_destroy(vhd->ring); + vhd->ring = NULL; + } + } + break; + + case LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED: +#if defined(LWS_ROLE_WT) + if (!lws_wt_is_session(wsi) && lws_wt_get_session_wsi(wsi) != NULL) { + goto init_session; + } +#endif break; case LWS_CALLBACK_ESTABLISHED: - /* Could be WS or WT stream */ #if defined(LWS_ROLE_WT) if (lws_wt_is_session(wsi)) { lwsl_user("WT Session established\n"); - break; /* We don't send data on the session itself */ + break; + } +#endif +#if defined(LWS_ROLE_WT) + init_session: +#endif + if (pss->wsi) { + lwsl_user("Session already initialized\n"); + break; } + { + const char *tt = "WebSocket"; +#if defined(LWS_ROLE_WT) + if (lws_wt_is_session(wsi) || lws_wt_get_session_wsi(wsi) != NULL) { + tt = "WebTransport"; + } #endif - lwsl_user("Connection/Stream established\n"); + lwsl_user("Connection/Stream established (protocol: %s, transport: %s)\n", + lws_get_protocol(wsi)->name, tt); + } + pss->wsi = wsi; + pss->player_id = ++vhd->next_player_id; + pss->x = 0.0; + pss->z = 0.0; + pss->angle = 0.0; + pss->speed = 0.0; + pss->is_moving = 0; pss->seed_sent = 0; + + lws_dll2_add_tail(&pss->list, &vhd->sessions); + pss->tail = lws_ring_get_oldest_tail(vhd->ring); + + /* Broadcast join message */ + { + struct msg jmsg; + jmsg.sender_id = pss->player_id; + jmsg.len = (size_t)lws_snprintf(jmsg.payload + LWS_PRE, sizeof(jmsg.payload) - LWS_PRE, + "{\"join\":%u}", pss->player_id); + if (lws_ring_insert(vhd->ring, &jmsg, 1) != 1) { + lwsl_wsi_warn(wsi, "Failed to insert join message to ring"); + } + } + lws_callback_on_writable(wsi); + lws_callback_on_writable_all_protocol(lws_get_context(wsi), lws_get_protocol(wsi)); + break; + + case LWS_CALLBACK_CLOSED: +#if defined(LWS_ROLE_WT) + if (lws_wt_is_session(wsi)) { + break; + } +#endif + if (!pss->wsi) { + break; + } + { + const char *tt = "WebSocket"; +#if defined(LWS_ROLE_WT) + if (lws_wt_is_session(wsi) || lws_wt_get_session_wsi(wsi) != NULL) { + tt = "WebTransport"; + } +#endif + lwsl_user("Connection/Stream closed (transport: %s)\n", tt); + } + lws_dll2_remove(&pss->list); + + /* Broadcast leave message */ + { + struct msg lmsg; + lmsg.sender_id = pss->player_id; + lmsg.len = (size_t)lws_snprintf(lmsg.payload + LWS_PRE, sizeof(lmsg.payload) - LWS_PRE, + "{\"leave\":%u}", pss->player_id); + if (lws_ring_insert(vhd->ring, &lmsg, 1) != 1) { + lwsl_wsi_warn(wsi, "Failed to insert leave message to ring"); + } + } + + lws_callback_on_writable_all_protocol(lws_get_context(wsi), lws_get_protocol(wsi)); + break; + + case LWS_CALLBACK_RECEIVE: +#if defined(LWS_ROLE_WT) + if (lws_wt_is_session(wsi)) { + break; + } +#endif + if (!pss->wsi) { + break; + } + { + size_t alen; + const char *val; + + val = lws_json_simple_find(in, len, "\"x\":", &alen); + if (val) pss->x = atof(val); + + val = lws_json_simple_find(in, len, "\"z\":", &alen); + if (val) pss->z = atof(val); + + val = lws_json_simple_find(in, len, "\"angle\":", &alen); + if (val) pss->angle = atof(val); + + val = lws_json_simple_find(in, len, "\"speed\":", &alen); + if (val) pss->speed = atof(val); + + val = lws_json_simple_find(in, len, "\"isMoving\":", &alen); + if (val) pss->is_moving = (!strncmp(val, "true", 4)) ? 1 : 0; + + /* Broadcast update */ + { + struct msg umsg; + umsg.sender_id = pss->player_id; + umsg.len = (size_t)lws_snprintf(umsg.payload + LWS_PRE, sizeof(umsg.payload) - LWS_PRE, + "{\"player_id\":%u,\"x\":%.2f,\"z\":%.2f,\"angle\":%.2f,\"isMoving\":%s}", + pss->player_id, pss->x, pss->z, pss->angle, pss->is_moving ? "true" : "false"); + if (lws_ring_insert(vhd->ring, &umsg, 1) != 1) { + lwsl_wsi_warn(wsi, "Failed to insert update message to ring"); + } + } + + lws_callback_on_writable_all_protocol(lws_get_context(wsi), lws_get_protocol(wsi)); + } break; case LWS_CALLBACK_SERVER_WRITEABLE: +#if defined(LWS_ROLE_WT) + if (lws_wt_is_session(wsi)) { + break; + } +#endif + if (!pss->wsi) { + break; + } if (!pss->seed_sent) { - m = lws_snprintf((char *)buf + LWS_PRE, sizeof(buf) - LWS_PRE, - "{\"seed\": %u}", vhd->seed); - if (lws_write(wsi, buf + LWS_PRE, (unsigned int)m, LWS_WRITE_TEXT) < m) + char *p = (char *)buf + LWS_PRE; + char *end = (char *)buf + sizeof(buf); + + p += lws_snprintf(p, (size_t)(end - p), "{\"seed\":%u,\"player_id\":%u,\"players\":[", + vhd->seed, pss->player_id); + + int first = 1; + lws_start_foreach_dll(struct lws_dll2 *, d, lws_dll2_get_head(&vhd->sessions)) { + struct pss__shared_world *other = lws_container_of(d, struct pss__shared_world, list); + if (other != pss) { + if (!first) + p += lws_snprintf(p, (size_t)(end - p), ","); + first = 0; + p += lws_snprintf(p, (size_t)(end - p), "{\"id\":%u,\"x\":%.2f,\"z\":%.2f,\"angle\":%.2f,\"isMoving\":%s}", + other->player_id, other->x, other->z, other->angle, other->is_moving ? "true" : "false"); + } + } lws_end_foreach_dll(d); + + p += lws_snprintf(p, (size_t)(end - p), "]}"); + + size_t slen = (size_t)(p - ((char *)buf + LWS_PRE)); + lwsl_user("Sending initial welcome JSON: %s\n", (char *)buf + LWS_PRE); + if (lws_write(wsi, buf + LWS_PRE, (unsigned int)slen, LWS_WRITE_TEXT) < (int)slen) return -1; + pss->seed_sent = 1; } + + { + const struct msg *pmsg = lws_ring_get_element(vhd->ring, &pss->tail); + if (pmsg) { + if (lws_write(wsi, (unsigned char *)pmsg->payload + LWS_PRE, pmsg->len, LWS_WRITE_TEXT) < (int)pmsg->len) + return -1; + + int oldest_consumed = (lws_ring_get_oldest_tail(vhd->ring) == pss->tail); + lws_ring_consume(vhd->ring, &pss->tail, NULL, 1); + + if (oldest_consumed) { + uint32_t oldest = pss->tail; + size_t max_waiting = 0; + + lws_start_foreach_dll(struct lws_dll2 *, d, lws_dll2_get_head(&vhd->sessions)) { + struct pss__shared_world *other = lws_container_of(d, struct pss__shared_world, list); + size_t waiting = lws_ring_get_count_waiting_elements(vhd->ring, &other->tail); + if (waiting >= max_waiting) { + max_waiting = waiting; + oldest = other->tail; + } + } lws_end_foreach_dll(d); + + lws_ring_update_oldest_tail(vhd->ring, oldest); + } + + if (lws_ring_get_element(vhd->ring, &pss->tail)) + lws_callback_on_writable(wsi); + } + } break; default: @@ -87,7 +307,7 @@ callback_shared_world(struct lws *wsi, enum lws_callback_reasons reason, "webtransport-shared-world", \ callback_shared_world, \ sizeof(struct pss__shared_world), \ - 1024, \ + 4096, \ 0, NULL, 0 \ } diff --git a/plugins/protocol_webtransport_test/CMakeLists.txt b/plugins/protocol_webtransport_test/CMakeLists.txt index 14827ee9fd..cc6edf3d44 100644 --- a/plugins/protocol_webtransport_test/CMakeLists.txt +++ b/plugins/protocol_webtransport_test/CMakeLists.txt @@ -35,4 +35,8 @@ if (requirements) if (NOT LWS_WITH_PLUGINS_BUILTIN) target_compile_definitions(${PLUGIN_NAME} PRIVATE LWS_BUILDING_SHARED) endif() + + install(DIRECTORY assets/ + DESTINATION "share/libwebsockets-test-server/wt-assets" + COMPONENT examples) endif() diff --git a/plugins/protocol_webtransport_test/assets/app.js b/plugins/protocol_webtransport_test/assets/app.js index 7a18d00148..38de6bdaed 100644 --- a/plugins/protocol_webtransport_test/assets/app.js +++ b/plugins/protocol_webtransport_test/assets/app.js @@ -1,6 +1,4 @@ const DOM = { - urlInput: document.getElementById('server-url'), - hashInput: document.getElementById('cert-hash'), startBtn: document.getElementById('start-btn'), connStatus: document.getElementById('conn-status'), progressText: document.getElementById('progress-text'), @@ -48,19 +46,14 @@ function hexString(buffer) { return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } -function parseHash(hashStr) { - const cleaned = hashStr.replace(/[:\s]/g, ''); - const bytes = new Uint8Array(cleaned.length / 2); - for (let i = 0; i < cleaned.length; i += 2) { - bytes[i / 2] = Number.parseInt(cleaned.substring(i, i + 2), 16); - } - return bytes; -} - async function generateTestPayload() { log('Generating 4MB random payload...'); const buffer = new Uint8Array(BLOB_SIZE); - crypto.getRandomValues(buffer); // Generates 4MB of PRNG + const maxRandomChunk = 65536; + for (let offset = 0; offset < BLOB_SIZE; offset += maxRandomChunk) { + const chunk = buffer.subarray(offset, offset + maxRandomChunk); + crypto.getRandomValues(chunk); + } log('Calculating SHA-256 hash...'); const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); @@ -84,16 +77,34 @@ async function sendStreamData(writer, data) { } } +let leftover = null; + async function receiveAndVerify(reader, expectedBytes) { const receivedData = new Uint8Array(expectedBytes); let bytesRead = 0; + if (leftover && leftover.length > 0) { + const copyLen = Math.min(leftover.length, expectedBytes - bytesRead); + receivedData.set(leftover.subarray(0, copyLen), bytesRead); + bytesRead += copyLen; + if (copyLen < leftover.length) { + leftover = leftover.subarray(copyLen); + } else { + leftover = null; + } + } + while (bytesRead < expectedBytes) { const { value, done } = await reader.read(); if (done) break; - receivedData.set(value, bytesRead); - bytesRead += value.length; + const copyLen = Math.min(value.length, expectedBytes - bytesRead); + receivedData.set(value.subarray(0, copyLen), bytesRead); + bytesRead += copyLen; + + if (copyLen < value.length) { + leftover = value.subarray(copyLen); + } } if (bytesRead < expectedBytes) { @@ -118,32 +129,23 @@ async function receiveAndVerify(reader, expectedBytes) { } async function startTest() { + leftover = null; try { DOM.startBtn.disabled = true; updateStatus('Connecting...', 'connecting'); updateProgress(0); DOM.logContainer.innerHTML = ''; - const url = DOM.urlInput.value; - const certHashStr = DOM.hashInput.value.trim(); - - const options = {}; - if (certHashStr) { - try { - const bytes = parseHash(certHashStr); - options.serverCertificateHashes = [{ - algorithm: "sha-256", - value: bytes.buffer - }]; - log('Using provided certificate hash for validation.'); - } catch (e) { - log('Invalid certificate hash format.', 'error'); - throw e; - } + let url = "https://localhost:7681/"; + if (window.location.protocol.startsWith('http')) { + const base = new URL('../', window.location.href).href; + url = base.replace(/^http:/, 'https:'); } log(`Connecting to WebTransport at ${url}`); - wt = new WebTransport(url, options); + wt = new WebTransport(url, { + protocols: ['webtransport-test'] + }); await wt.ready; updateStatus('Connected', 'connected'); @@ -177,7 +179,12 @@ async function startTest() { } log('Test Completed Successfully!', 'success'); - await writer.close(); + try { + writer.releaseLock(); + } catch (e) {} + try { + reader.releaseLock(); + } catch (e) {} wt.close(); updateStatus('Disconnected', 'disconnected'); diff --git a/plugins/protocol_webtransport_test/assets/index.html b/plugins/protocol_webtransport_test/assets/index.html index 2440c60b93..baede4fbf5 100644 --- a/plugins/protocol_webtransport_test/assets/index.html +++ b/plugins/protocol_webtransport_test/assets/index.html @@ -16,15 +16,6 @@

WebTransport Test

-
- - -
-
- - - Required if using self-signed certs without --ignore-certificate-errors flag. -
diff --git a/plugins/protocol_webtransport_test/assets/style.css b/plugins/protocol_webtransport_test/assets/style.css index 48e9255528..50877db5ac 100644 --- a/plugins/protocol_webtransport_test/assets/style.css +++ b/plugins/protocol_webtransport_test/assets/style.css @@ -1,5 +1,3 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); - :root { --bg-color: #0f172a; --card-bg: rgba(30, 41, 59, 0.7); @@ -20,7 +18,7 @@ } body { - font-family: 'Inter', sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: var(--bg-color); color: var(--text-primary); line-height: 1.6; diff --git a/plugins/protocol_webtransport_test/protocol_webtransport_test.c b/plugins/protocol_webtransport_test/protocol_webtransport_test.c index ed8da869d9..9d56eb2b26 100644 --- a/plugins/protocol_webtransport_test/protocol_webtransport_test.c +++ b/plugins/protocol_webtransport_test/protocol_webtransport_test.c @@ -21,10 +21,10 @@ #include #include -#define BLOB_SIZE (4 * 1024 * 1024) -#define HASH_SIZE 32 -#define TOTAL_SEND_SIZE (BLOB_SIZE + HASH_SIZE) -#define TEST_ITERATIONS 10 +#define BLOB_SIZE (4 * 1024 * 1024) +#define HASH_SIZE 32 +#define TOTAL_SEND_SIZE (BLOB_SIZE + HASH_SIZE) +#define TEST_ITERATIONS 10 struct vhd__wt_test { uint8_t *blob; @@ -58,6 +58,9 @@ callback_wt_test(struct lws *wsi, enum lws_callback_reasons reason, switch (reason) { case LWS_CALLBACK_PROTOCOL_INIT: + if (!in) + return 0; + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), lws_get_protocol(wsi), sizeof(struct vhd__wt_test)); @@ -87,7 +90,6 @@ callback_wt_test(struct lws *wsi, enum lws_callback_reasons reason, } lws_genhash_destroy(&hctx, vhd->hash); } - lwsl_notice("WT Test Plugin Initialized\n"); break; case LWS_CALLBACK_PROTOCOL_DESTROY: @@ -96,7 +98,7 @@ callback_wt_test(struct lws *wsi, enum lws_callback_reasons reason, vhd->blob = NULL; } break; - + case LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED: case LWS_CALLBACK_ESTABLISHED: /* We only care about WebTransport child streams for data transfer */ if (lws_wt_is_session(wsi)) { @@ -121,6 +123,13 @@ callback_wt_test(struct lws *wsi, enum lws_callback_reasons reason, break; to_send = TOTAL_SEND_SIZE - pss->send_pos; + { + int tx_cr = lws_wsi_tx_credit(wsi, LWSTXCR_US_TO_PEER, 0); + if (tx_cr <= 0) + break; + if (to_send > (size_t)tx_cr) + to_send = (size_t)tx_cr; + } if (to_send > 65536) to_send = 65536; /* write in chunks */ @@ -142,6 +151,7 @@ callback_wt_test(struct lws *wsi, enum lws_callback_reasons reason, } m = lws_write(wsi, p, (unsigned int)to_send, LWS_WRITE_BINARY); + lwsl_info("WT WRITEABLE: send_pos %zu, to_send %zu, write_ret %d\n", pss->send_pos, to_send, m); if (m < 0) { lwsl_err("write error\n"); return -1; diff --git a/win32port/win32helpers/getopt.c b/win32port/win32helpers/getopt.c index 7e11b3b983..8fa0b33929 100644 --- a/win32port/win32helpers/getopt.c +++ b/win32port/win32helpers/getopt.c @@ -99,19 +99,27 @@ getopt(nargc, nargv, ostr) if (optreset || !*place) { /* update scanning pointer */ optreset = 0; - if (optind >= nargc || *(place = nargv[optind]) != '-') { + if (optind >= nargc) { place = EMSG; return (-1); } - if (place[1] && *++place == '-' /* found "--" */ - && place[1] == '\0') { - ++optind; + place = nargv[optind]; + if (*place != '-') { place = EMSG; return (-1); } + if (place[1]) { + place++; + if (*place == '-' && place[1] == '\0') { + ++optind; + place = EMSG; + return (-1); + } + } } /* option letter okay? */ - if ((optopt = (int)*place++) == (int)':' || - !(oli = (char *)strchr(ostr, optopt))) { + optopt = (int)*place++; + oli = (char *)strchr(ostr, optopt); + if (optopt == (int)':' || !oli) { /* * if the user didn't specify '-' as an option, * assume it means -1. diff --git a/win32port/win32helpers/getopt_long.c b/win32port/win32helpers/getopt_long.c index 840fa75594..c971c1bd44 100644 --- a/win32port/win32helpers/getopt_long.c +++ b/win32port/win32helpers/getopt_long.c @@ -91,18 +91,27 @@ getopt_internal(nargc, nargv, ostr) if (optreset || !*place) { /* update scanning pointer */ optreset = 0; - if (optind >= nargc || *(place = nargv[optind]) != '-') { + if (optind >= nargc) { place = EMSG; return (-1); } - if (place[1] && *++place == '-') { /* found "--" */ - /* ++optind; */ + place = nargv[optind]; + if (*place != '-') { place = EMSG; - return (-2); + return (-1); + } + if (place[1]) { + place++; + if (*place == '-') { /* found "--" */ + /* ++optind; */ + place = EMSG; + return (-2); + } } } /* option letter okay? */ - if ((optopt = (int)*place++) == (int)':' || - !(oli = (char *)strchr(ostr, optopt))) { + optopt = (int)*place++; + oli = (char *)strchr(ostr, optopt); + if (optopt == (int)':' || !oli) { /* * if the user didn't specify '-' as an option, * assume it means -1.