@@ -22,6 +22,22 @@ use tracing::debug;
2222const MAX_HEADER_BYTES : usize = 16384 ; // 16 KiB for HTTP headers
2323const MAX_REWRITE_BODY_BYTES : usize = 256 * 1024 ;
2424const RELAY_BUF_SIZE : usize = 8192 ;
25+ const HTTP_METHOD_PREFIXES : & [ & [ u8 ] ] = & [
26+ b"GET " ,
27+ b"HEAD " ,
28+ b"POST " ,
29+ b"PUT " ,
30+ b"DELETE " ,
31+ b"PATCH " ,
32+ b"OPTIONS " ,
33+ b"CONNECT " ,
34+ b"TRACE " ,
35+ ] ;
36+ pub ( crate ) const HTTP2_PRIOR_KNOWLEDGE_PREFACE : & [ u8 ] = b"PRI * HTTP/2.0\r \n \r \n SM\r \n \r \n " ;
37+ pub ( crate ) const UNSUPPORTED_H2C_UPGRADE_DETAIL : & str =
38+ "HTTP/2 cleartext upgrade (h2c) is not supported for L7-inspected endpoints" ;
39+ const MIN_HTTP2_PREFACE_DETECTION_BYTES : usize = 8 ;
40+
2541/// Idle timeout for `relay_until_eof`. If no data arrives within this window
2642/// the body is considered complete. Prevents blocking on servers that keep
2743/// the TCP connection alive after the response body (common with CDN keep-alive).
@@ -913,6 +929,36 @@ pub(crate) fn request_is_websocket_upgrade(raw_header: &[u8]) -> bool {
913929 validate_websocket_upgrade_request ( & raw_header[ ..header_end] ) . unwrap_or ( false )
914930}
915931
932+ pub ( crate ) fn request_is_h2c_upgrade ( raw_header : & [ u8 ] ) -> bool {
933+ let header_end = raw_header
934+ . windows ( 4 )
935+ . position ( |w| w == b"\r \n \r \n " )
936+ . map_or ( raw_header. len ( ) , |p| p + 4 ) ;
937+ let Ok ( header_str) = std:: str:: from_utf8 ( & raw_header[ ..header_end] ) else {
938+ return false ;
939+ } ;
940+
941+ let mut upgrade_h2c = false ;
942+ let mut connection_upgrade = false ;
943+
944+ for line in header_str. lines ( ) . skip ( 1 ) {
945+ let Some ( ( name, value) ) = line. split_once ( ':' ) else {
946+ continue ;
947+ } ;
948+ let name = name. trim ( ) ;
949+ let value = value. trim ( ) ;
950+ if name. eq_ignore_ascii_case ( "upgrade" ) && header_value_contains_token ( value, "h2c" ) {
951+ upgrade_h2c = true ;
952+ }
953+ if name. eq_ignore_ascii_case ( "connection" ) && header_value_contains_token ( value, "upgrade" )
954+ {
955+ connection_upgrade = true ;
956+ }
957+ }
958+
959+ upgrade_h2c && connection_upgrade
960+ }
961+
916962fn rewrite_websocket_extensions_for_mode (
917963 raw_header : & [ u8 ] ,
918964 mode : WebSocketExtensionMode ,
@@ -1962,18 +2008,27 @@ where
19622008///
19632009/// Checks for common HTTP methods at the start of the stream.
19642010pub fn looks_like_http ( peek : & [ u8 ] ) -> bool {
1965- const METHODS : & [ & [ u8 ] ] = & [
1966- b"GET " ,
1967- b"HEAD " ,
1968- b"POST " ,
1969- b"PUT " ,
1970- b"DELETE " ,
1971- b"PATCH " ,
1972- b"OPTIONS " ,
1973- b"CONNECT " ,
1974- b"TRACE " ,
1975- ] ;
1976- METHODS . iter ( ) . any ( |m| peek. starts_with ( m) )
2011+ HTTP_METHOD_PREFIXES
2012+ . iter ( )
2013+ . any ( |method| peek. starts_with ( method) )
2014+ }
2015+
2016+ pub ( crate ) fn could_be_http_request_prefix ( peek : & [ u8 ] ) -> bool {
2017+ !peek. is_empty ( )
2018+ && HTTP_METHOD_PREFIXES
2019+ . iter ( )
2020+ . any ( |method| peek. len ( ) < method. len ( ) && method. starts_with ( peek) )
2021+ }
2022+
2023+ pub fn looks_like_http2_prior_knowledge ( peek : & [ u8 ] ) -> bool {
2024+ peek. len ( ) >= MIN_HTTP2_PREFACE_DETECTION_BYTES
2025+ && HTTP2_PRIOR_KNOWLEDGE_PREFACE . starts_with ( peek)
2026+ }
2027+
2028+ pub ( crate ) fn could_be_http2_prior_knowledge_prefix ( peek : & [ u8 ] ) -> bool {
2029+ !peek. is_empty ( )
2030+ && peek. len ( ) < MIN_HTTP2_PREFACE_DETECTION_BYTES
2031+ && HTTP2_PRIOR_KNOWLEDGE_PREFACE . starts_with ( peek)
19772032}
19782033
19792034/// Check if an IO error represents a benign connection close.
@@ -2919,10 +2974,26 @@ mod tests {
29192974 assert ! ( looks_like_http( b"GET / HTTP/1.1\r \n " ) ) ;
29202975 assert ! ( looks_like_http( b"POST /api HTTP/1.1\r \n " ) ) ;
29212976 assert ! ( looks_like_http( b"DELETE /foo HTTP/1.1\r \n " ) ) ;
2977+ assert ! ( could_be_http_request_prefix( b"GE" ) ) ;
2978+ assert ! ( !could_be_http_request_prefix( b"GET " ) ) ;
29222979 assert ! ( !looks_like_http( b"\x00 \x00 \x00 \x08 " ) ) ; // Postgres
2980+ assert ! ( !looks_like_http( HTTP2_PRIOR_KNOWLEDGE_PREFACE ) ) ;
29232981 assert ! ( !looks_like_http( b"HELLO" ) ) ; // Unknown
29242982 }
29252983
2984+ #[ test]
2985+ fn http2_prior_knowledge_detection ( ) {
2986+ assert ! ( looks_like_http2_prior_knowledge(
2987+ HTTP2_PRIOR_KNOWLEDGE_PREFACE
2988+ ) ) ;
2989+ assert ! ( looks_like_http2_prior_knowledge(
2990+ & HTTP2_PRIOR_KNOWLEDGE_PREFACE [ ..8 ]
2991+ ) ) ;
2992+ assert ! ( could_be_http2_prior_knowledge_prefix( b"PRI * H" ) ) ;
2993+ assert ! ( !looks_like_http2_prior_knowledge( b"PRI * H" ) ) ;
2994+ assert ! ( !looks_like_http2_prior_knowledge( b"PRI / HTTP/1.1\r \n " ) ) ;
2995+ }
2996+
29262997 #[ test]
29272998 fn test_parse_status_code ( ) {
29282999 assert_eq ! (
@@ -4160,6 +4231,20 @@ mod tests {
41604231 assert ! ( !validate_websocket_upgrade_request( raw) . expect( "h2c request should parse" ) ) ;
41614232 }
41624233
4234+ #[ test]
4235+ fn h2c_upgrade_detection_requires_upgrade_token_and_connection_upgrade ( ) {
4236+ let raw = b"GET /h2c HTTP/1.1\r \n Host: example.com\r \n Upgrade: h2c\r \n Connection: keep-alive, Upgrade\r \n HTTP2-Settings: AAMAAABkAAQAAP__\r \n \r \n " ;
4237+ assert ! ( request_is_h2c_upgrade( raw) ) ;
4238+
4239+ let missing_connection = b"GET /h2c HTTP/1.1\r \n Host: example.com\r \n Upgrade: h2c\r \n \r \n " ;
4240+ assert ! ( !request_is_h2c_upgrade( missing_connection) ) ;
4241+
4242+ let websocket = format ! (
4243+ "GET /ws HTTP/1.1\r \n Host: example.com\r \n Upgrade: websocket\r \n Connection: Upgrade\r \n Sec-WebSocket-Key: {VALID_WS_KEY}\r \n Sec-WebSocket-Version: 13\r \n \r \n "
4244+ ) ;
4245+ assert ! ( !request_is_h2c_upgrade( websocket. as_bytes( ) ) ) ;
4246+ }
4247+
41634248 #[ test]
41644249 fn strip_websocket_extensions_removes_extension_negotiation ( ) {
41654250 let raw = format ! (
0 commit comments