diff --git a/Cargo.lock b/Cargo.lock index 9e7288990..9be026ce8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -43,43 +43,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "ascii" @@ -99,7 +100,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 1.0.64", + "thiserror 1.0.69", "time", ] @@ -111,7 +112,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] @@ -123,7 +124,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -155,9 +156,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "block-buffer" @@ -176,15 +177,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.1.30" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -209,9 +210,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.5.20" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -219,9 +220,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -231,27 +232,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "cookie-factory" @@ -264,9 +265,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -282,18 +283,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -307,9 +308,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "der-parser" @@ -373,7 +374,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -394,7 +395,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ - "log 0.4.22", + "log 0.4.25", "regex", ] @@ -406,19 +407,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" @@ -428,9 +429,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -498,7 +499,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -647,9 +648,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -677,7 +678,7 @@ dependencies = [ "futures-util", "http", "hyper", - "log 0.4.22", + "log 0.4.25", "rustls 0.21.12", "tokio", "tokio-rustls", @@ -799,26 +800,35 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "idna" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "icu_normalizer", - "icu_properties", + "idna_adapter", "smallvec", "utf8_iter", ] +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -852,9 +862,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jemalloc-sys" @@ -893,9 +903,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libredox" @@ -910,15 +920,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -936,14 +946,14 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" dependencies = [ - "log 0.4.22", + "log 0.4.25", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "memchr" @@ -968,22 +978,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", - "log 0.4.22", + "log 0.4.25", "wasi", "windows-sys 0.52.0", ] @@ -1069,9 +1078,9 @@ checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1153,9 +1162,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1195,12 +1204,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -1218,18 +1227,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", "prost-derive", @@ -1237,14 +1246,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" dependencies = [ - "bytes", "heck", "itertools", - "log 0.4.22", + "log 0.4.25", "multimap", "once_cell", "petgraph", @@ -1252,28 +1260,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.87", + "syn 2.0.96", "tempfile", ] [[package]] name = "prost-derive" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "prost-types" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" dependencies = [ "prost", ] @@ -1285,15 +1293,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger", - "log 0.4.22", + "log 0.4.25", "rand", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1330,9 +1338,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags", ] @@ -1351,14 +1359,14 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1368,9 +1376,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1415,15 +1423,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1432,7 +1440,7 @@ version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "log 0.4.22", + "log 0.4.25", "ring", "rustls-webpki 0.101.7", "sct", @@ -1440,11 +1448,11 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.14" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ - "log 0.4.22", + "log 0.4.25", "once_cell", "ring", "rustls-pki-types", @@ -1464,9 +1472,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" @@ -1491,9 +1499,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rusty_ulid" @@ -1514,9 +1522,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scc" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553f8299af7450cda9a52d3a370199904e7a46b5ffd1bef187c4a6af3bb6db69" +checksum = "28e1c91382686d21b5ac7959341fcb9780fa7c03773646995a87c950fa7be640" dependencies = [ "sdd", ] @@ -1539,35 +1547,35 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" +checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -1586,12 +1594,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ "futures", - "log 0.4.22", + "log 0.4.25", "once_cell", "parking_lot", "scc", @@ -1600,13 +1608,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -1643,9 +1651,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1653,12 +1661,12 @@ dependencies = [ [[package]] name = "sozu" -version = "1.0.6" +version = "1.1.0-rc.2" dependencies = [ "clap", "jemallocator", "libc", - "log 0.4.22", + "log 0.4.25", "mio", "nix", "nom", @@ -1671,16 +1679,16 @@ dependencies = [ "sozu-lib", "tempfile", "termion", - "thiserror 2.0.3", + "thiserror 2.0.11", ] [[package]] name = "sozu-command-lib" -version = "1.0.6" +version = "1.1.0-rc.2" dependencies = [ "hex", "libc", - "log 0.4.22", + "log 0.4.25", "memchr", "mio", "nix", @@ -1695,7 +1703,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.3", + "thiserror 2.0.11", "time", "toml", "trailer", @@ -1720,7 +1728,7 @@ dependencies = [ [[package]] name = "sozu-lib" -version = "1.0.6" +version = "1.1.0-rc.2" dependencies = [ "anyhow", "cookie-factory", @@ -1737,7 +1745,7 @@ dependencies = [ "quickcheck", "rand", "regex", - "rustls 0.23.14", + "rustls 0.23.21", "rustls-pemfile", "rusty_ulid", "serial_test", @@ -1745,7 +1753,7 @@ dependencies = [ "slab", "socket2", "sozu-command-lib", - "thiserror 2.0.3", + "thiserror 2.0.11", "time", "tiny_http", ] @@ -1787,9 +1795,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -1804,17 +1812,18 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "tempfile" -version = "3.13.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1845,49 +1854,49 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.64", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -1906,9 +1915,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -1923,7 +1932,7 @@ dependencies = [ "ascii", "chunked_transfer", "httpdate", - "log 0.4.22", + "log 0.4.25", ] [[package]] @@ -1938,9 +1947,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "libc", @@ -2002,9 +2011,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -2012,9 +2021,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -2039,9 +2048,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" @@ -2206,9 +2215,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -2238,15 +2247,15 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror 1.0.64", + "thiserror 1.0.69", "time", ] [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -2256,13 +2265,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] @@ -2284,27 +2293,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] @@ -2333,5 +2342,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] diff --git a/bin/Cargo.toml b/bin/Cargo.toml index e2591f57a..580349b34 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/sozu-proxy/sozu" readme = "README.md" documentation = "https://docs.rs/sozu" homepage = "https://sozu.io" -version = "1.0.6" +version = "1.1.0-rc.2" license = "AGPL-3.0" authors = [ "Geoffroy Couprie ", @@ -42,8 +42,8 @@ tempfile = "^3.13.0" termion = "^4.0.3" thiserror = "^2.0.3" -sozu-command-lib = { path = "../command", version = "^1.0.6" } -sozu-lib = { path = "../lib", version = "^1.0.6" } +sozu-command-lib = { path = "../command", version = "^1.1.0-rc.2" } +sozu-lib = { path = "../lib", version = "1.1.0-rc.2" } [target.'cfg(target_os="linux")'.dependencies] num_cpus = "^1.16.0" diff --git a/bin/config.toml b/bin/config.toml index a37fbaf76..c144e520f 100644 --- a/bin/config.toml +++ b/bin/config.toml @@ -324,7 +324,8 @@ frontends = [ # - weight: weight used by the load balancing algorithm # - sticky-id: sticky session identifier backends = [ - { address = "127.0.0.1:1026", backend_id = "the-backend-to-my-app" } + { address = "127.0.0.1:1026", backend_id = "back26" }, + { address = "127.0.0.1:1027", backend_id = "back27" } ] # this is an example of a routing configuration for the TCP proxy diff --git a/bin/src/cli.rs b/bin/src/cli.rs index ba8b6692a..74355d9bc 100644 --- a/bin/src/cli.rs +++ b/bin/src/cli.rs @@ -299,6 +299,11 @@ pub enum ClusterCmd { help = "Configures the load balancing policy. Possible values are 'roundrobin', 'random' or 'leastconnections'" )] load_balancing_policy: LoadBalancingAlgorithms, + #[clap( + long = "http2", + help = "the backends of this cluster use http2 prio-knowledge" + )] + h2: bool, }, } diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index eb07929d7..2d8acea14 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -150,6 +150,7 @@ impl CommandManager { send_proxy, expect_proxy, load_balancing_policy, + h2, } => { let proxy_protocol = match (send_proxy, expect_proxy) { (true, true) => Some(ProxyProtocolConfig::RelayHeader), @@ -164,7 +165,9 @@ impl CommandManager { https_redirect, proxy_protocol: proxy_protocol.map(|pp| pp as i32), load_balancing: load_balancing_policy as i32, - ..Default::default() + http2: h2, + load_metric: None, + answer_503: None, }) .into(), ) diff --git a/command/Cargo.toml b/command/Cargo.toml index 564c9db5c..064f46c03 100644 --- a/command/Cargo.toml +++ b/command/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/sozu-proxy/sozu" readme = "README.md" documentation = "https://docs.rs/sozu-command-lib" homepage = "https://sozu.io" -version = "1.0.6" +version = "1.1.0-rc.2" license = "LGPL-3.0" authors = [ "Geoffroy Couprie ", diff --git a/command/src/command.proto b/command/src/command.proto index ad93b9e34..9e49f012e 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -162,6 +162,7 @@ message HttpsListenerConfig { // agains session tracking. Defaults to 4. required uint64 send_tls13_tickets = 20; optional CustomHttpAnswers http_answers = 21; + repeated AlpnProtocol alpn = 22; } // details of an TCP listener @@ -366,6 +367,11 @@ enum TlsVersion { TLS_V1_3 = 5; } +enum AlpnProtocol { + Http11 = 0; + H2 = 1; +} + // A cluster is what binds a frontend to backends with routing rules message Cluster { required string cluster_id = 1; @@ -376,6 +382,7 @@ message Cluster { required LoadBalancingAlgorithms load_balancing = 5 [default = ROUND_ROBIN]; optional string answer_503 = 6; optional LoadMetric load_metric = 7; + required bool http2 = 8; } enum LoadBalancingAlgorithms { diff --git a/command/src/config.rs b/command/src/config.rs index e148cc1d0..16a26a309 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -61,7 +61,7 @@ use crate::{ certificate::split_certificate_chain, logging::AccessLogFormat, proto::command::{ - request::RequestType, ActivateListener, AddBackend, AddCertificate, CertificateAndKey, + request::RequestType, ActivateListener, AddBackend, AddCertificate, AlpnProtocol, CertificateAndKey, Cluster, CustomHttpAnswers, HttpListenerConfig, HttpsListenerConfig, ListenerType, LoadBalancingAlgorithms, LoadBalancingParams, LoadMetric, MetricsConfiguration, PathRule, ProtobufAccessLogFormat, ProxyProtocolConfig, Request, RequestHttpFrontend, @@ -71,8 +71,10 @@ use crate::{ ObjectKind, }; +pub const DEFAULT_ALPN: [AlpnProtocol; 1] = [AlpnProtocol::Http11]; + /// provides all supported cipher suites exported by Rustls TLS -/// provider as it support only strongly secure ones. +/// provider as it supports only strongly secure ones. /// /// See the [documentation](https://docs.rs/rustls/latest/rustls/static.ALL_CIPHER_SUITES.html) pub const DEFAULT_RUSTLS_CIPHER_LIST: [&str; 9] = [ @@ -240,6 +242,7 @@ pub struct ListenerBuilder { pub address: SocketAddr, pub protocol: Option, pub public_address: Option, + pub alpn: Option>, pub answer_301: Option, pub answer_400: Option, pub answer_401: Option, @@ -302,6 +305,7 @@ impl ListenerBuilder { fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder { ListenerBuilder { address: address.into(), + alpn: None, answer_301: None, answer_401: None, answer_400: None, @@ -331,14 +335,14 @@ impl ListenerBuilder { } } - pub fn with_public_address(&mut self, public_address: Option) -> &mut Self { + pub fn with_public_address(mut self, public_address: Option) -> Self { if let Some(address) = public_address { self.public_address = Some(address); } self } - pub fn with_answer_404_path(&mut self, answer_404_path: Option) -> &mut Self + pub fn with_answer_404_path(mut self, answer_404_path: Option) -> Self where S: ToString, { @@ -348,7 +352,7 @@ impl ListenerBuilder { self } - pub fn with_answer_503_path(&mut self, answer_503_path: Option) -> &mut Self + pub fn with_answer_503_path(mut self, answer_503_path: Option) -> Self where S: ToString, { @@ -358,27 +362,27 @@ impl ListenerBuilder { self } - pub fn with_tls_versions(&mut self, tls_versions: Vec) -> &mut Self { + pub fn with_tls_versions(mut self, tls_versions: Vec) -> Self { self.tls_versions = Some(tls_versions); self } - pub fn with_cipher_list(&mut self, cipher_list: Option>) -> &mut Self { + pub fn with_cipher_list(mut self, cipher_list: Option>) -> Self { self.cipher_list = cipher_list; self } - pub fn with_cipher_suites(&mut self, cipher_suites: Option>) -> &mut Self { + pub fn with_cipher_suites(mut self, cipher_suites: Option>) -> Self { self.cipher_suites = cipher_suites; self } - pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self { + pub fn with_expect_proxy(mut self, expect_proxy: bool) -> Self { self.expect_proxy = Some(expect_proxy); self } - pub fn with_sticky_name(&mut self, sticky_name: Option) -> &mut Self + pub fn with_sticky_name(mut self, sticky_name: Option) -> Self where S: ToString, { @@ -388,7 +392,7 @@ impl ListenerBuilder { self } - pub fn with_certificate(&mut self, certificate: S) -> &mut Self + pub fn with_certificate(mut self, certificate: S) -> Self where S: ToString, { @@ -396,12 +400,12 @@ impl ListenerBuilder { self } - pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self { + pub fn with_certificate_chain(mut self, certificate_chain: String) -> Self { self.certificate = Some(certificate_chain); self } - pub fn with_key(&mut self, key: String) -> &mut Self + pub fn with_key(mut self, key: String) -> Self where S: ToString, { @@ -409,22 +413,22 @@ impl ListenerBuilder { self } - pub fn with_front_timeout(&mut self, front_timeout: Option) -> &mut Self { + pub fn with_front_timeout(mut self, front_timeout: Option) -> Self { self.front_timeout = front_timeout; self } - pub fn with_back_timeout(&mut self, back_timeout: Option) -> &mut Self { + pub fn with_back_timeout(mut self, back_timeout: Option) -> Self { self.back_timeout = back_timeout; self } - pub fn with_connect_timeout(&mut self, connect_timeout: Option) -> &mut Self { + pub fn with_connect_timeout(mut self, connect_timeout: Option) -> Self { self.connect_timeout = connect_timeout; self } - pub fn with_request_timeout(&mut self, request_timeout: Option) -> &mut Self { + pub fn with_request_timeout(mut self, request_timeout: Option) -> Self { self.request_timeout = request_timeout; self } @@ -455,11 +459,11 @@ impl ListenerBuilder { } /// build an HTTP listener with config timeouts, using defaults if no config is provided - pub fn to_http(&mut self, config: Option<&Config>) -> Result { + pub fn to_http(mut self, config: Option<&Config>) -> Result { if self.protocol != Some(ListenerProtocol::Http) { return Err(ConfigError::WrongListenerProtocol { expected: ListenerProtocol::Http, - found: self.protocol.to_owned(), + found: self.protocol, }); } @@ -473,7 +477,7 @@ impl ListenerBuilder { address: self.address.into(), public_address: self.public_address.map(|a| a.into()), expect_proxy: self.expect_proxy.unwrap_or(false), - sticky_name: self.sticky_name.clone(), + sticky_name: self.sticky_name, front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT), back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT), connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT), @@ -486,34 +490,47 @@ impl ListenerBuilder { } /// build an HTTPS listener using defaults if no config or values were provided upstream - pub fn to_tls(&mut self, config: Option<&Config>) -> Result { + pub fn to_tls(mut self, config: Option<&Config>) -> Result { if self.protocol != Some(ListenerProtocol::Https) { return Err(ConfigError::WrongListenerProtocol { expected: ListenerProtocol::Https, - found: self.protocol.to_owned(), + found: self.protocol, }); } + if let Some(config) = config { + self.assign_config_timeouts(config); + } + + let http_answers = self.get_http_answers()?; + let default_alpn = DEFAULT_ALPN.into_iter().map(|p| p as i32).collect(); + + let alpn = self + .alpn + .as_ref() + .map(|alpn| alpn.iter().map(|p| *p as i32).collect()) + .unwrap_or(default_alpn); + let default_cipher_list = DEFAULT_RUSTLS_CIPHER_LIST .into_iter() .map(String::from) .collect(); - let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list); + let cipher_list = self.cipher_list.unwrap_or(default_cipher_list); let default_cipher_suites = DEFAULT_CIPHER_SUITES .into_iter() .map(String::from) .collect(); - let cipher_suites = self.cipher_suites.clone().unwrap_or(default_cipher_suites); + let cipher_suites = self.cipher_suites.unwrap_or(default_cipher_suites); - let signature_algorithms: Vec = DEFAULT_SIGNATURE_ALGORITHMS + let signature_algorithms = DEFAULT_SIGNATURE_ALGORITHMS .into_iter() .map(String::from) .collect(); - let groups_list: Vec = DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect(); + let groups_list = DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect(); let versions = match self.tls_versions { None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32], @@ -550,15 +567,10 @@ impl ListenerBuilder { .map(split_certificate_chain) .unwrap_or_default(); - let http_answers = self.get_http_answers()?; - - if let Some(config) = config { - self.assign_config_timeouts(config); - } - let https_listener_config = HttpsListenerConfig { + alpn, address: self.address.into(), - sticky_name: self.sticky_name.clone(), + sticky_name: self.sticky_name, public_address: self.public_address.map(|a| a.into()), cipher_list, versions, @@ -584,11 +596,11 @@ impl ListenerBuilder { } /// build an HTTPS listener using defaults if no config or values were provided upstream - pub fn to_tcp(&mut self, config: Option<&Config>) -> Result { + pub fn to_tcp(mut self, config: Option<&Config>) -> Result { if self.protocol != Some(ListenerProtocol::Tcp) { return Err(ConfigError::WrongListenerProtocol { expected: ListenerProtocol::Tcp, - found: self.protocol.to_owned(), + found: self.protocol, }); } @@ -649,6 +661,11 @@ pub enum PathRuleType { Equals, } +/// Congruent with command.proto +fn default_rule_position() -> RulePosition { + RulePosition::Tree +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FileClusterFrontendConfig { @@ -664,7 +681,7 @@ pub struct FileClusterFrontendConfig { pub certificate_chain: Option, #[serde(default)] pub tls_versions: Vec, - #[serde(default)] + #[serde(default = "default_rule_position")] pub position: RulePosition, pub tags: Option>, } @@ -768,6 +785,7 @@ pub enum ListenerProtocol { #[serde(deny_unknown_fields, rename_all = "lowercase")] pub enum FileClusterProtocolConfig { Http, + Http2, Tcp, } @@ -777,6 +795,7 @@ pub struct FileClusterConfig { pub frontends: Vec, pub backends: Vec, pub protocol: FileClusterProtocolConfig, + pub http_version: Option, pub sticky_session: Option, pub https_redirect: Option, #[serde(default)] @@ -856,7 +875,7 @@ impl FileClusterConfig { load_metric: self.load_metric, })) } - FileClusterProtocolConfig::Http => { + FileClusterProtocolConfig::Http | FileClusterProtocolConfig::Http2 => { let mut frontends = Vec::new(); for frontend in self.frontends { let http_frontend = frontend.to_http_front(cluster_id)?; @@ -874,6 +893,7 @@ impl FileClusterConfig { Ok(ClusterConfig::Http(HttpClusterConfig { cluster_id: cluster_id.to_string(), + http2: self.protocol == FileClusterProtocolConfig::Http2, frontends, backends: self.backends, sticky_session: self.sticky_session.unwrap_or(false), @@ -966,6 +986,7 @@ impl HttpFrontendConfig { #[serde(deny_unknown_fields)] pub struct HttpClusterConfig { pub cluster_id: String, + pub http2: bool, pub frontends: Vec, pub backends: Vec, pub sticky_session: bool, @@ -981,6 +1002,7 @@ impl HttpClusterConfig { cluster_id: self.cluster_id.clone(), sticky_session: self.sticky_session, https_redirect: self.https_redirect, + http2: self.http2, proxy_protocol: None, load_balancing: self.load_balancing as i32, answer_503: self.answer_503.clone(), @@ -1040,6 +1062,7 @@ impl TcpClusterConfig { cluster_id: self.cluster_id.clone(), sticky_session: false, https_redirect: false, + http2: false, proxy_protocol: self.proxy_protocol.map(|s| s as i32), load_balancing: self.load_balancing as i32, load_metric: self.load_metric.map(|s| s as i32), @@ -1281,19 +1304,19 @@ impl ConfigBuilder { } } - fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> { + fn push_tls_listener(&mut self, listener: ListenerBuilder) -> Result<(), ConfigError> { let listener = listener.to_tls(Some(&self.built))?; self.built.https_listeners.push(listener); Ok(()) } - fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> { + fn push_http_listener(&mut self, listener: ListenerBuilder) -> Result<(), ConfigError> { let listener = listener.to_http(Some(&self.built))?; self.built.http_listeners.push(listener); Ok(()) } - fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> { + fn push_tcp_listener(&mut self, listener: ListenerBuilder) -> Result<(), ConfigError> { let listener = listener.to_tcp(Some(&self.built))?; self.built.tcp_listeners.push(listener); Ok(()) diff --git a/command/src/proto/display.rs b/command/src/proto/display.rs index 9331de3a7..b4acd1ee2 100644 --- a/command/src/proto/display.rs +++ b/command/src/proto/display.rs @@ -54,9 +54,9 @@ impl Display for CertificateSummary { impl Display for QueryCertificatesFilters { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(d) = self.domain.clone() { - write!(f, "domain:{}", d) + write!(f, "domain:{d}") } else if let Some(fp) = self.fingerprint.clone() { - write!(f, "domain:{}", fp) + write!(f, "domain:{fp}") } else { write!(f, "all certificates") } @@ -152,7 +152,7 @@ impl Response { } impl ResponseContent { - fn display(&self, json: bool) -> Result<(), DisplayError> { + pub fn display(&self, json: bool) -> Result<(), DisplayError> { let content_type = match &self.content_type { Some(content_type) => content_type, None => return Ok(println!("No content")), diff --git a/command/src/state.rs b/command/src/state.rs index e8371a8bd..1418a002a 100644 --- a/command/src/state.rs +++ b/command/src/state.rs @@ -462,7 +462,7 @@ impl ConfigState { if tcp_frontends.contains(&tcp_frontend) { return Err(StateError::Exists { kind: ObjectKind::TcpFrontend, - id: format!("{:?}", tcp_frontend), + id: format!("{tcp_frontend:?}"), }); } @@ -479,7 +479,7 @@ impl ConfigState { .get_mut(&front_to_remove.cluster_id) .ok_or(StateError::NotFound { kind: ObjectKind::TcpFrontend, - id: format!("{:?}", front_to_remove), + id: format!("{front_to_remove:?}"), })?; let len = tcp_frontends.len(); diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 7e80309b4..758a6b1cd 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -14,5 +14,5 @@ rustls = { version = "^0.21.10", features = ["dangerous_configuration"] } time = "^0.3.36" tokio = { version = "1.40.0", features = ["net", "rt-multi-thread"] } -sozu-command-lib = { path = "../command", version = "^1.0.6" } -sozu-lib = { path = "../lib", version = "^1.0.6" } +sozu-command-lib = { path = "../command", version = "^1.1.0-rc.2" } +sozu-lib = { path = "../lib", version = "^1.1.0-rc.2" } diff --git a/e2e/src/http_utils/mod.rs b/e2e/src/http_utils/mod.rs index 9e6248df2..997e9fce4 100644 --- a/e2e/src/http_utils/mod.rs +++ b/e2e/src/http_utils/mod.rs @@ -26,10 +26,32 @@ pub fn http_request, S2: Into, S3: Into, S4: In pub fn immutable_answer(status: u16) -> String { match status { - 400 => String::from("HTTP/1.1 400 Bad Request\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), - 404 => String::from("HTTP/1.1 404 Not Found\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), - 502 => String::from("HTTP/1.1 502 Bad Gateway\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), - 503 => String::from("HTTP/1.1 503 Service Unavailable\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), + // 400 => String::from("HTTP/1.1 400 Bad Request\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), + 400 => String::from("HTTP/1.1 400 Sozu Default Answer\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"), + // 404 => String::from("HTTP/1.1 404 Not Found\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), + 404 => String::from("HTTP/1.1 404 Sozu Default Answer\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"), + // 502 => String::from("HTTP/1.1 502 Bad Gateway\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), + 502 => String::from("HTTP/1.1 502 Sozu Default Answer\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"), + // 503 => String::from("HTTP/1.1 503 Service Unavailable\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), + 503 => String::from("HTTP/1.1 503 Sozu Default Answer\r\nCache-Control: no-cache\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"), _ => unimplemented!() } } + +// use std::io::Write; +// use kawa; + +// /// the default kawa answer for the error code provided, converted to HTTP/1.1 +// pub fn default_answer(code: u16) -> String { +// let mut kawa_answer = kawa::Kawa::new( +// kawa::Kind::Response, +// kawa::Buffer::new(kawa::SliceBuffer(&mut [])), +// ); +// sozu_lib::protocol::mux::fill_default_answer(&mut kawa_answer, code); +// kawa_answer.prepare(&mut kawa::h1::converter::H1BlockConverter); +// let out = kawa_answer.as_io_slice(); +// let mut writer = std::io::BufWriter::new(Vec::new()); +// writer.write_vectored(&out).expect("WRITE"); +// let result = unsafe { std::str::from_utf8_unchecked(writer.buffer()) }; +// result.to_string() +// } diff --git a/e2e/src/mock/async_backend.rs b/e2e/src/mock/async_backend.rs index 56c452b62..b0ff7d25c 100644 --- a/e2e/src/mock/async_backend.rs +++ b/e2e/src/mock/async_backend.rs @@ -35,7 +35,7 @@ impl BackendHandle { let name = name.into(); let (stop_tx, mut stop_rx) = mpsc::channel::<()>(1); let (mut aggregator_tx, aggregator_rx) = mpsc::channel::(1); - let listener = TcpListener::bind(address).expect("could not bind"); + let listener = TcpListener::bind(address).expect(&format!("could not bind on: {address}")); let mut clients = Vec::new(); let thread_name = name.to_owned(); diff --git a/e2e/src/mock/client.rs b/e2e/src/mock/client.rs index da0e1f242..6b2bc324b 100644 --- a/e2e/src/mock/client.rs +++ b/e2e/src/mock/client.rs @@ -39,7 +39,7 @@ impl Client { /// Establish a TCP connection with its address, /// register the yielded TCP stream, apply timeouts pub fn connect(&mut self) { - let stream = TcpStream::connect(self.address).expect("could not connect"); + let stream = TcpStream::connect(self.address).expect(&format!("could not connect to: {}", self.address)); stream .set_read_timeout(Some(Duration::from_millis(100))) .expect("could not set read timeout"); diff --git a/e2e/src/mock/sync_backend.rs b/e2e/src/mock/sync_backend.rs index b97d58384..65f9f6638 100644 --- a/e2e/src/mock/sync_backend.rs +++ b/e2e/src/mock/sync_backend.rs @@ -44,7 +44,7 @@ impl Backend { /// Binds itself to its address, stores the yielded TCP listener pub fn connect(&mut self) { - let listener = TcpListener::bind(self.address).expect("could not bind"); + let listener = TcpListener::bind(self.address).expect(&format!("could not bind on: {}", self.address)); let timeout = Duration::from_millis(100); let timeout = libc::timeval { tv_sec: 0, diff --git a/e2e/src/tests/tests.rs b/e2e/src/tests/tests.rs index 6bcedfc06..5680b57d5 100644 --- a/e2e/src/tests/tests.rs +++ b/e2e/src/tests/tests.rs @@ -776,7 +776,8 @@ fn try_http_behaviors() -> State { && response.ends_with(&expected_response_end) ); - info!("server closes, expecting 503"); + // FIXME: do we want 502 or 503??? + info!("server closes, expecting 502"); // TODO: what if the client continue to use the closed stream client.connect(); client.send(); @@ -787,7 +788,7 @@ fn try_http_behaviors() -> State { let response = client.receive(); println!("request: {request:?}"); println!("response: {response:?}"); - assert_eq!(response, Some(immutable_answer(503))); + assert_eq!(response, Some(immutable_answer(502))); assert_eq!(client.receive(), None); worker.send_proxy_request_type(RequestType::RemoveBackend(RemoveBackend { @@ -987,7 +988,7 @@ fn try_https_redirect() -> State { client.connect(); client.send(); let answer = client.receive(); - let expected_answer = format!("{answer_301_prefix}https://example.com/redirected?true\r\n\r\n"); + let expected_answer = format!("{answer_301_prefix}https://example.com/redirected?true\r\nContent-Length: 0\r\n\r\n"); assert_eq!(answer, Some(expected_answer)); State::Success @@ -1242,7 +1243,9 @@ pub fn try_stick() -> State { backend1.send(0); let response = client.receive(); println!("response: {response:?}"); - assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:")); + assert!(request.unwrap().starts_with( + "GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\n" + )); assert!(response.unwrap().starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: SOZUBALANCEID=sticky_cluster_0-0; Path=/\r\nSozu-Id:")); // invalid sticky_session @@ -1255,7 +1258,9 @@ pub fn try_stick() -> State { backend2.send(0); let response = client.receive(); println!("response: {response:?}"); - assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:")); + assert!(request.unwrap().starts_with( + "GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\n" + )); assert!(response.unwrap().starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: SOZUBALANCEID=sticky_cluster_0-1; Path=/\r\nSozu-Id:")); // good sticky_session (force use backend2, round-robin would have chosen backend1) @@ -1268,7 +1273,9 @@ pub fn try_stick() -> State { backend2.send(0); let response = client.receive(); println!("response: {response:?}"); - assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:")); + assert!(request.unwrap().starts_with( + "GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\n" + )); assert!(response .unwrap() .starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSozu-Id:")); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d41b20c32..da599be89 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/sozu-proxy/sozu" readme = "README.md" documentation = "https://docs.rs/sozu-lib" homepage = "https://sozu.io" -version = "1.0.6" +version = "1.1.0-rc.2" license = "AGPL-3.0" authors = [ "Clément Delafargue ", @@ -33,7 +33,7 @@ cookie-factory = "^0.3.3" hdrhistogram = "^7.5.4" hex = "^0.4.3" hpack = "^0.3.0" -idna = "^1.0.2" +idna = "^1.0.3" kawa = { version = "^0.6.7", default-features = false } libc = "^0.2.159" memchr = "^2.7.4" @@ -56,7 +56,7 @@ socket2 = { version = "^0.5.7", features = ["all"] } thiserror = "^2.0.3" time = "^0.3.36" -sozu-command-lib = { path = "../command", version = "^1.0.6" } +sozu-command-lib = { path = "../command", version = "1.1.0-rc.2" } [dev-dependencies] quickcheck = "^1.0.3" diff --git a/lib/src/backends.rs b/lib/src/backends.rs index 0de77c434..ac4d3938e 100644 --- a/lib/src/backends.rs +++ b/lib/src/backends.rs @@ -297,7 +297,8 @@ impl BackendMap { })?; self.available = true; - Ok((next_backend.clone(), tcp_stream)) + drop(borrowed_backend); + Ok((next_backend, tcp_stream)) } pub fn backend_from_sticky_session( diff --git a/lib/src/http.rs b/lib/src/http.rs index 2f6e69e39..44cd99704 100644 --- a/lib/src/http.rs +++ b/lib/src/http.rs @@ -34,10 +34,10 @@ use crate::{ http::{ answers::HttpAnswers, parser::{hostname_and_port, Method}, - ResponseStream, }, + mux::{self, Mux, MuxClear}, proxy_protocol::expect::ExpectProxyProtocol, - Http, Pipe, SessionState, + Pipe, SessionState, }, router::{Route, Router}, server::{ListenToken, SessionManager}, @@ -62,7 +62,8 @@ StateMachineBuilder! { /// 3. WebSocket (passthrough) enum HttpStateMachine impl SessionState { Expect(ExpectProxyProtocol), - Http(Http), + // Http(Http), + Mux(MuxClear), WebSocket(Pipe), } } @@ -82,7 +83,6 @@ pub struct HttpSession { pool: Weak>, proxy: Rc>, state: HttpStateMachine, - sticky_name: String, has_been_closed: bool, } @@ -100,7 +100,6 @@ impl HttpSession { proxy: Rc>, public_address: SocketAddr, sock: TcpStream, - sticky_name: String, token: Token, wait_time: Duration, ) -> Result { @@ -121,22 +120,40 @@ impl HttpSession { gauge_add!("protocol.http", 1); let session_address = sock.peer_addr().ok(); - HttpStateMachine::Http(Http::new( - answers.clone(), - configured_backend_timeout, - configured_connect_timeout, - configured_frontend_timeout, - container_frontend_timeout, - sock, - token, - listener.clone(), + let frontend = mux::Connection::new_h1_server(sock, container_frontend_timeout); + let router = mux::Router::new(configured_backend_timeout, configured_connect_timeout); + let mut context = mux::Context::new( pool.clone(), - Protocol::HTTP, - public_address, - request_id, + listener.clone(), session_address, - sticky_name.clone(), - )?) + public_address, + ); + context + .create_stream(request_id, 1 << 16) + .ok_or(AcceptError::BufferCapacityReached)?; + HttpStateMachine::Mux(Mux { + configured_frontend_timeout, + frontend_token: token, + frontend, + router, + context, + }) + // HttpStateMachine::Http(Http::new( + // answers.clone(), + // configured_backend_timeout, + // configured_connect_timeout, + // configured_frontend_timeout, + // container_frontend_timeout, + // sock, + // token, + // listener.clone(), + // pool.clone(), + // Protocol::HTTP, + // public_address, + // request_id, + // session_address, + // sticky_name.clone(), + // )?) }; let metrics = SessionMetrics::new(Some(wait_time)); @@ -153,14 +170,14 @@ impl HttpSession { pool, proxy, state, - sticky_name, }) } pub fn upgrade(&mut self) -> SessionIsToBeClosed { debug!("HTTP::upgrade"); let new_state = match self.state.take() { - HttpStateMachine::Http(http) => self.upgrade_http(http), + // HttpStateMachine::Http(http) => self.upgrade_http(http), + HttpStateMachine::Mux(mux) => self.upgrade_mux(mux), HttpStateMachine::Expect(expect) => self.upgrade_expect(expect), HttpStateMachine::WebSocket(ws) => self.upgrade_websocket(ws), HttpStateMachine::FailedUpgrade(_) => unreachable!(), @@ -187,84 +204,120 @@ impl HttpSession { .map(|add| (add.destination(), add.source())) { Some((Some(public_address), Some(session_address))) => { - let mut http = Http::new( - self.answers.clone(), + let frontend = mux::Connection::new_h1_server( + expect.frontend, + expect.container_frontend_timeout, + ); + let router = mux::Router::new( self.configured_backend_timeout, self.configured_connect_timeout, - self.configured_frontend_timeout, - expect.container_frontend_timeout, - expect.frontend, - expect.frontend_token, - self.listener.clone(), + ); + let mut context = mux::Context::new( self.pool.clone(), - Protocol::HTTP, - public_address, - expect.request_id, + self.listener.clone(), Some(session_address), - self.sticky_name.clone(), - ) - .ok()?; - http.frontend_readiness.event = expect.frontend_readiness.event; + public_address, + ); + if context.create_stream(expect.request_id, 1 << 16).is_none() { + error!("HTTP expect upgrade failed: could not create stream"); + return None; + } + let mut mux = Mux { + configured_frontend_timeout: self.configured_frontend_timeout, + frontend_token: self.frontend_token, + frontend, + router, + context, + }; + mux.frontend.readiness_mut().event = expect.frontend_readiness.event; gauge_add!("protocol.proxy.expect", -1); gauge_add!("protocol.http", 1); - Some(HttpStateMachine::Http(http)) + Some(HttpStateMachine::Mux(mux)) } - _ => None, - } - } - - fn upgrade_http(&mut self, http: Http) -> Option { - debug!("http switching to ws"); - let front_token = self.frontend_token; - let back_token = match http.backend_token { - Some(back_token) => back_token, - None => { + _ => { warn!( - "Could not upgrade http request on cluster '{:?}' ({:?}) using backend '{:?}' into websocket for request '{}'", - http.context.cluster_id, self.frontend_token, http.context.backend_id, http.context.id + "HTTP expect upgrade failed: bad header {:?}", + expect.addresses ); - return None; + None } - }; + } + } - let ws_context = http.websocket_context(); - let mut container_frontend_timeout = http.container_frontend_timeout; - let mut container_backend_timeout = http.container_backend_timeout; - container_frontend_timeout.reset(); - container_backend_timeout.reset(); + fn upgrade_mux(&mut self, mut mux: MuxClear) -> Option { + debug!("mux switching to ws"); + let stream = mux.context.streams.pop().unwrap(); + + let (frontend_readiness, frontend_socket, mut container_frontend_timeout) = + match mux.frontend { + mux::Connection::H1(mux::ConnectionH1 { + readiness, + socket, + timeout_container, + .. + }) => (readiness, socket, timeout_container), + mux::Connection::H2(_) => { + error!("Only h1<->h1 connections can upgrade to websocket"); + return None; + } + }; - let backend_buffer = if let ResponseStream::BackendAnswer(kawa) = http.response_stream { - kawa.storage.buffer - } else { + let mux::StreamState::Linked(back_token) = stream.state else { + error!("Upgrading stream should be linked to a backend"); return None; }; + let backend = mux.router.backends.remove(&back_token).unwrap(); + let (cluster_id, backend, backend_readiness, backend_socket, mut container_backend_timeout) = + match backend { + mux::Connection::H1(mux::ConnectionH1 { + position: + mux::Position::Client(cluster_id, backend, mux::BackendStatus::Connected), + readiness, + socket, + timeout_container, + .. + }) => (cluster_id, backend, readiness, socket, timeout_container), + mux::Connection::H1(_) => { + error!("The backend disconnected just after upgrade, abort"); + return None; + } + mux::Connection::H2(_) => { + error!("Only h1<->h1 connections can upgrade to websocket"); + return None; + } + }; + + let ws_context = stream.context.websocket_context(); + + container_frontend_timeout.reset(); + container_backend_timeout.reset(); + let backend_id = backend.borrow().backend_id.clone(); let mut pipe = Pipe::new( - backend_buffer, - http.context.backend_id, - http.backend_socket, - http.backend, + stream.back.storage.buffer, + Some(backend_id), + Some(backend_socket), + Some(backend), Some(container_backend_timeout), Some(container_frontend_timeout), - http.context.cluster_id, - http.request_stream.storage.buffer, - front_token, - http.frontend_socket, + Some(cluster_id), + stream.front.storage.buffer, + self.frontend_token, + frontend_socket, self.listener.clone(), Protocol::HTTP, - http.context.id, - http.context.session_address, + stream.context.id, + stream.context.session_address, ws_context, ); - pipe.frontend_readiness.event = http.frontend_readiness.event; - pipe.backend_readiness.event = http.backend_readiness.event; + pipe.frontend_readiness.event = frontend_readiness.event; + pipe.backend_readiness.event = backend_readiness.event; pipe.set_back_token(back_token); gauge_add!("protocol.http", -1); gauge_add!("protocol.ws", 1); - gauge_add!("http.active_requests", -1); gauge_add!("websocket.active_requests", 1); Some(HttpStateMachine::WebSocket(pipe)) } @@ -288,7 +341,7 @@ impl ProxySession for HttpSession { // Restore gauges match self.state.marker() { StateMarker::Expect => gauge_add!("protocol.proxy.expect", -1), - StateMarker::Http => gauge_add!("protocol.http", -1), + StateMarker::Mux => gauge_add!("protocol.http", -1), StateMarker::WebSocket => { gauge_add!("protocol.ws", -1); gauge_add!("websocket.active_requests", -1); @@ -298,7 +351,7 @@ impl ProxySession for HttpSession { if self.state.failed() { match self.state.marker() { StateMarker::Expect => incr!("http.upgrade.expect.failed"), - StateMarker::Http => incr!("http.upgrade.http.failed"), + StateMarker::Mux => incr!("http.upgrade.http.failed"), StateMarker::WebSocket => incr!("http.upgrade.ws.failed"), } return; @@ -405,10 +458,22 @@ pub struct HttpListener { } impl ListenerHandler for HttpListener { - fn get_addr(&self) -> &SocketAddr { + fn protocol(&self) -> Protocol { + Protocol::HTTP + } + + fn address(&self) -> &SocketAddr { &self.address } + fn public_address(&self) -> SocketAddr { + self.config + .public_address + .as_ref() + .map(|addr| addr.clone().into()) + .unwrap_or(self.address.clone()) + } + fn get_tags(&self, key: &str) -> Option<&CachedTags> { self.tags.get(key) } @@ -422,11 +487,11 @@ impl ListenerHandler for HttpListener { } impl L7ListenerHandler for HttpListener { - fn get_sticky_name(&self) -> &str { + fn sticky_name(&self) -> &str { &self.config.sticky_name } - fn get_connect_timeout(&self) -> u32 { + fn connect_timeout(&self) -> u32 { self.config.connect_timeout } @@ -472,7 +537,7 @@ impl L7ListenerHandler for HttpListener { let now = Instant::now(); - if let Route::ClusterId(cluster) = &route { + if let Route::Cluster(cluster) = &route { time!("frontend_matching_time", cluster, (now - start).as_millis()); } @@ -688,7 +753,7 @@ impl HttpProxy { if !socket_errors.is_empty() { return Err(ProxyError::SoftStop { proxy_protocol: "HTTP".to_string(), - error: format!("Error deregistering listen sockets: {:?}", socket_errors), + error: format!("Error deregistering listen sockets: {socket_errors:?}"), }); } @@ -711,7 +776,7 @@ impl HttpProxy { if !socket_errors.is_empty() { return Err(ProxyError::HardStop { proxy_protocol: "HTTP".to_string(), - error: format!("Error deregistering listen sockets: {:?}", socket_errors), + error: format!("Error deregistering listen sockets: {socket_errors:?}"), }); } @@ -927,7 +992,6 @@ impl ProxyConfiguration for HttpProxy { proxy, public_address, frontend_sock, - owned.config.sticky_name.clone(), session_token, wait_time, )?; @@ -1388,19 +1452,19 @@ mod tests { let frontend5 = listener.frontend_from_request("domain", "/", &Method::Get); assert_eq!( frontend1.expect("should find frontend"), - Route::ClusterId("cluster_1".to_string()) + Route::Cluster("cluster_1".to_string()) ); assert_eq!( frontend2.expect("should find frontend"), - Route::ClusterId("cluster_1".to_string()) + Route::Cluster("cluster_1".to_string()) ); assert_eq!( frontend3.expect("should find frontend"), - Route::ClusterId("cluster_2".to_string()) + Route::Cluster("cluster_2".to_string()) ); assert_eq!( frontend4.expect("should find frontend"), - Route::ClusterId("cluster_3".to_string()) + Route::Cluster("cluster_3".to_string()) ); assert!(frontend5.is_err()); } diff --git a/lib/src/https.rs b/lib/src/https.rs index ce03d33bd..92f5d6c2f 100644 --- a/lib/src/https.rs +++ b/lib/src/https.rs @@ -38,10 +38,11 @@ use sozu_command::{ certificate::Fingerprint, config::DEFAULT_CIPHER_SUITES, proto::command::{ - request::RequestType, response_content::ContentType, AddCertificate, CertificateSummary, - CertificatesByAddress, Cluster, HttpsListenerConfig, ListOfCertificatesByAddress, - ListenerType, RemoveCertificate, RemoveListener, ReplaceCertificate, RequestHttpFrontend, - ResponseContent, TlsVersion, WorkerRequest, WorkerResponse, + request::RequestType, response_content::ContentType, AddCertificate, AlpnProtocol, + CertificateSummary, CertificatesByAddress, Cluster, HttpsListenerConfig, + ListOfCertificatesByAddress, ListenerType, RemoveCertificate, RemoveListener, + ReplaceCertificate, RequestHttpFrontend, ResponseContent, TlsVersion, WorkerRequest, + WorkerResponse, }, ready::Ready, response::HttpFrontend, @@ -52,15 +53,14 @@ use crate::{ backends::BackendMap, pool::Pool, protocol::{ - h2::Http2, http::{ answers::HttpAnswers, parser::{hostname_and_port, Method}, - ResponseStream, }, + mux::{self, Mux, MuxTls}, proxy_protocol::expect::ExpectProxyProtocol, rustls::TlsHandshake, - Http, Pipe, SessionState, + Pipe, SessionState, }, router::{Route, Router}, server::{ListenToken, SessionManager}, @@ -73,8 +73,12 @@ use crate::{ SessionMetrics, SessionResult, StateMachineBuilder, StateResult, }; -// const SERVER_PROTOS: &[&str] = &["http/1.1", "h2"]; -const SERVER_PROTOS: &[&str] = &["http/1.1"]; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TlsCluster { + cluster_id: String, + hostname: String, + path_begin: String, +} StateMachineBuilder! { /// The various Stages of an HTTPS connection: @@ -86,17 +90,13 @@ StateMachineBuilder! { enum HttpsStateMachine impl SessionState { Expect(ExpectProxyProtocol, ServerConnection), Handshake(TlsHandshake), - Http(Http), + Mux(MuxTls), + // Http(Http), WebSocket(Pipe), - Http2(Http2) -> todo!("H2"), + // Http2(Http2) -> todo!("H2"), } } -pub enum AlpnProtocols { - H2, - Http11, -} - pub struct HttpsSession { answers: Rc>, configured_backend_timeout: Duration, @@ -188,8 +188,9 @@ impl HttpsSession { let new_state = match self.state.take() { HttpsStateMachine::Expect(expect, ssl) => self.upgrade_expect(expect, ssl), HttpsStateMachine::Handshake(handshake) => self.upgrade_handshake(handshake), - HttpsStateMachine::Http(http) => self.upgrade_http(http), - HttpsStateMachine::Http2(_) => self.upgrade_http2(), + // HttpsStateMachine::Http(http) => self.upgrade_http(http), + HttpsStateMachine::Mux(mux) => self.upgrade_mux(mux), + // HttpsStateMachine::Http2(_) => self.upgrade_http2(), HttpsStateMachine::WebSocket(wss) => self.upgrade_websocket(wss), HttpsStateMachine::FailedUpgrade(_) => unreachable!(), }; @@ -247,17 +248,14 @@ impl HttpsSession { if !expect.container_frontend_timeout.cancel() { error!("failed to cancel request timeout on expect upgrade phase for 'expect proxy protocol with AF_UNSPEC address'"); } - + warn!( + "HTTP expect upgrade failed: bad header {:?}", + expect.addresses + ); None } fn upgrade_handshake(&mut self, handshake: TlsHandshake) -> Option { - // Add 1st routing phase - // - get SNI - // - get ALPN - // - find corresponding listener - // - determine next protocol (tcps, https ,http2) - let sni = handshake.session.server_name(); let alpn = handshake.session.alpn_protocol(); let alpn = alpn.and_then(|alpn| from_utf8(alpn).ok()); @@ -267,15 +265,16 @@ impl HttpsSession { ); let alpn = match alpn { - Some("http/1.1") => AlpnProtocols::Http11, - Some("h2") => AlpnProtocols::H2, + Some("http/1.1") => AlpnProtocol::Http11, + Some("h2") => AlpnProtocol::H2, Some(other) => { error!("Unsupported ALPN protocol: {}", other); return None; } // Some client don't fill in the ALPN protocol, in this case we default to Http/1.1 - None => AlpnProtocols::Http11, + None => AlpnProtocol::Http11, }; + // println!("ALPN: {alpn:?}"); if let Some(version) = handshake.session.protocol_version() { incr!(rustls_version_str(version)); @@ -290,108 +289,121 @@ impl HttpsSession { }; gauge_add!("protocol.tls.handshake", -1); - match alpn { - AlpnProtocols::Http11 => { - let mut http = Http::new( - self.answers.clone(), - self.configured_backend_timeout, - self.configured_connect_timeout, - self.configured_frontend_timeout, - handshake.container_frontend_timeout, - front_stream, - self.frontend_token, - self.listener.clone(), - self.pool.clone(), - Protocol::HTTPS, - self.public_address, - handshake.request_id, - self.peer_address, - self.sticky_name.clone(), - ) - .ok()?; - - http.frontend_readiness.event = handshake.frontend_readiness.event; - gauge_add!("protocol.https", 1); - Some(HttpsStateMachine::Http(http)) + let router = mux::Router::new( + self.configured_backend_timeout, + self.configured_connect_timeout, + ); + let mut context = mux::Context::new( + self.pool.clone(), + self.listener.clone(), + self.peer_address, + self.public_address, + ); + let mut frontend = match alpn { + AlpnProtocol::Http11 => { + incr!("http.alpn.http11"); + context.create_stream(handshake.request_id, 1 << 16)?; + mux::Connection::new_h1_server(front_stream, handshake.container_frontend_timeout) } - AlpnProtocols::H2 => { - let mut http = Http2::new( + AlpnProtocol::H2 => { + incr!("http.alpn.h2"); + mux::Connection::new_h2_server( front_stream, - self.frontend_token, self.pool.clone(), - Some(self.public_address), - None, - self.sticky_name.clone(), - ); - - http.frontend.readiness.event = handshake.frontend_readiness.event; - - gauge_add!("protocol.http2", 1); - Some(HttpsStateMachine::Http2(http)) + handshake.container_frontend_timeout, + )? } - } + }; + frontend.readiness_mut().event = handshake.frontend_readiness.event; + + gauge_add!("protocol.https", 1); + Some(HttpsStateMachine::Mux(Mux { + configured_frontend_timeout: self.configured_frontend_timeout, + frontend_token: self.frontend_token, + frontend, + context, + router, + })) } - fn upgrade_http(&self, http: Http) -> Option { - debug!("https switching to wss"); - let front_token = self.frontend_token; - let back_token = match http.backend_token { - Some(back_token) => back_token, - None => { - warn!( - "Could not upgrade https request on cluster '{:?}' ({:?}) using backend '{:?}' into secure websocket for request '{}'", - http.context.cluster_id, self.frontend_token, http.context.backend_id, http.context.id - ); - return None; - } - }; + fn upgrade_mux(&self, mut mux: MuxTls) -> Option { + debug!("mux switching to wss"); + let stream = mux.context.streams.pop().unwrap(); - let ws_context = http.websocket_context(); - let mut container_frontend_timeout = http.container_frontend_timeout; - let mut container_backend_timeout = http.container_backend_timeout; - container_frontend_timeout.reset(); - container_backend_timeout.reset(); + let (frontend_readiness, frontend_socket, mut container_frontend_timeout) = + match mux.frontend { + mux::Connection::H1(mux::ConnectionH1 { + readiness, + socket, + timeout_container, + .. + }) => (readiness, socket, timeout_container), + mux::Connection::H2(_) => { + error!("Only h1<->h1 connections can upgrade to websocket"); + return None; + } + }; - let backend_buffer = if let ResponseStream::BackendAnswer(kawa) = http.response_stream { - kawa.storage.buffer - } else { + let mux::StreamState::Linked(back_token) = stream.state else { + error!("Upgrading stream should be linked to a backend"); return None; }; + let backend = mux.router.backends.remove(&back_token).unwrap(); + let (cluster_id, backend, backend_readiness, backend_socket, mut container_backend_timeout) = + match backend { + mux::Connection::H1(mux::ConnectionH1 { + position: + mux::Position::Client(cluster_id, backend, mux::BackendStatus::Connected), + readiness, + socket, + timeout_container, + .. + }) => (cluster_id, backend, readiness, socket, timeout_container), + mux::Connection::H1(_) => { + error!("The backend disconnected just after upgrade, abort"); + return None; + } + mux::Connection::H2(_) => { + error!("Only h1<->h1 connections can upgrade to websocket"); + return None; + } + }; + + let ws_context = stream.context.websocket_context(); + + container_frontend_timeout.reset(); + container_backend_timeout.reset(); + let backend_id = backend.borrow().backend_id.clone(); let mut pipe = Pipe::new( - backend_buffer, - http.context.backend_id, - http.backend_socket, - http.backend, + stream.back.storage.buffer, + Some(backend_id), + Some(backend_socket), + Some(backend), Some(container_backend_timeout), Some(container_frontend_timeout), - http.context.cluster_id, - http.request_stream.storage.buffer, - front_token, - http.frontend_socket, + Some(cluster_id), + stream.front.storage.buffer, + self.frontend_token, + frontend_socket, self.listener.clone(), - Protocol::HTTP, - http.context.id, - http.context.session_address, + Protocol::HTTPS, + stream.context.id, + stream.context.session_address, ws_context, ); - pipe.frontend_readiness.event = http.frontend_readiness.event; - pipe.backend_readiness.event = http.backend_readiness.event; + pipe.frontend_readiness.event = frontend_readiness.event; + pipe.backend_readiness.event = backend_readiness.event; pipe.set_back_token(back_token); gauge_add!("protocol.https", -1); gauge_add!("protocol.wss", 1); - gauge_add!("http.active_requests", -1); gauge_add!("websocket.active_requests", 1); Some(HttpsStateMachine::WebSocket(pipe)) } - fn upgrade_http2(&self) -> Option { - todo!() - } - fn upgrade_websocket( &self, wss: Pipe, @@ -415,21 +427,19 @@ impl ProxySession for HttpsSession { match self.state.marker() { StateMarker::Expect => gauge_add!("protocol.proxy.expect", -1), StateMarker::Handshake => gauge_add!("protocol.tls.handshake", -1), - StateMarker::Http => gauge_add!("protocol.https", -1), + StateMarker::Mux => gauge_add!("protocol.https", -1), StateMarker::WebSocket => { gauge_add!("protocol.wss", -1); gauge_add!("websocket.active_requests", -1); } - StateMarker::Http2 => gauge_add!("protocol.http2", -1), } if self.state.failed() { match self.state.marker() { StateMarker::Expect => incr!("https.upgrade.expect.failed"), StateMarker::Handshake => incr!("https.upgrade.handshake.failed"), - StateMarker::Http => incr!("https.upgrade.http.failed"), + StateMarker::Mux => incr!("https.upgrade.mux.failed"), StateMarker::WebSocket => incr!("https.upgrade.wss.failed"), - StateMarker::Http2 => incr!("https.upgrade.http2.failed"), } return; } @@ -479,18 +489,24 @@ impl ProxySession for HttpsSession { token, super::ready_to_string(events) ); + // println!("EVENT: {token:?}->{events:?}"); self.last_event = Instant::now(); self.metrics.wait_start(); self.state.update_readiness(token, events); } fn ready(&mut self, session: Rc>) -> SessionIsToBeClosed { + // let start = std::time::Instant::now(); + // println!("READY {start:?}"); self.metrics.service_start(); let session_result = self.state .ready(session.clone(), self.proxy.clone(), &mut self.metrics); + // let end = std::time::Instant::now(); + // println!("READY END {end:?} -> {:?}", end.duration_since(start)); + let to_be_closed = match session_result { SessionResult::Close => true, SessionResult::Continue => false, @@ -539,10 +555,22 @@ pub struct HttpsListener { } impl ListenerHandler for HttpsListener { - fn get_addr(&self) -> &StdSocketAddr { + fn protocol(&self) -> Protocol { + Protocol::HTTPS + } + + fn address(&self) -> &StdSocketAddr { &self.address } + fn public_address(&self) -> StdSocketAddr { + self.config + .public_address + .as_ref() + .map(|addr| addr.clone().into()) + .unwrap_or(self.address.clone()) + } + fn get_tags(&self, key: &str) -> Option<&CachedTags> { self.tags.get(key) } @@ -556,11 +584,11 @@ impl ListenerHandler for HttpsListener { } impl L7ListenerHandler for HttpsListener { - fn get_sticky_name(&self) -> &str { + fn sticky_name(&self) -> &str { &self.config.sticky_name } - fn get_connect_timeout(&self) -> u32 { + fn connect_timeout(&self) -> u32 { self.config.connect_timeout } @@ -574,7 +602,6 @@ impl L7ListenerHandler for HttpsListener { let (remaining_input, (hostname, _)) = match hostname_and_port(host.as_bytes()) { Ok(tuple) => tuple, Err(parse_error) => { - // parse_error contains a slice of given_host, which should NOT escape this scope return Err(FrontendFromRequestError::HostParse { host: host.to_owned(), error: parse_error.to_string(), @@ -600,7 +627,7 @@ impl L7ListenerHandler for HttpsListener { let now = Instant::now(); - if let Route::ClusterId(cluster) = &route { + if let Route::Cluster(cluster) = &route { time!("frontend_matching_time", cluster, (now - start).as_millis()); } @@ -726,11 +753,20 @@ impl HttpsListener { .with_cert_resolver(resolver); server_config.send_tls13_tickets = config.send_tls13_tickets as usize; - let mut protocols = SERVER_PROTOS + let protocols = config + .alpn .iter() - .map(|proto| proto.as_bytes().to_vec()) + .filter_map(|protocol| match AlpnProtocol::try_from(*protocol) { + Ok(AlpnProtocol::Http11) => Some("http/1.1"), + Ok(AlpnProtocol::H2) => Some("h2"), + other_protocol => { + error!("unsupported ALPN protocol: {:?}", other_protocol); + None + } + }) + .map(|protocol| protocol.as_bytes().to_vec()) .collect::>(); - server_config.alpn_protocols.append(&mut protocols); + server_config.alpn_protocols = protocols; Ok(server_config) } @@ -840,7 +876,7 @@ impl HttpsProxy { if !socket_errors.is_empty() { return Err(ProxyError::SoftStop { proxy_protocol: "HTTPS".to_string(), - error: format!("Error deregistering listen sockets: {:?}", socket_errors), + error: format!("Error deregistering listen sockets: {socket_errors:?}"), }); } @@ -863,7 +899,7 @@ impl HttpsProxy { if !socket_errors.is_empty() { return Err(ProxyError::HardStop { proxy_protocol: "HTTPS".to_string(), - error: format!("Error deregistering listen sockets: {:?}", socket_errors), + error: format!("Error deregistering listen sockets: {socket_errors:?}"), }); } @@ -1542,25 +1578,25 @@ mod tests { "lolcatho.st".as_bytes(), &PathRule::Prefix(uri1), &MethodRule::new(None), - &Route::ClusterId(cluster_id1.clone()) + &Route::Cluster(cluster_id1.clone()) )); assert!(fronts.add_tree_rule( "lolcatho.st".as_bytes(), &PathRule::Prefix(uri2), &MethodRule::new(None), - &Route::ClusterId(cluster_id2) + &Route::Cluster(cluster_id2) )); assert!(fronts.add_tree_rule( "lolcatho.st".as_bytes(), &PathRule::Prefix(uri3), &MethodRule::new(None), - &Route::ClusterId(cluster_id3) + &Route::Cluster(cluster_id3) )); assert!(fronts.add_tree_rule( "other.domain".as_bytes(), &PathRule::Prefix("test".to_string()), &MethodRule::new(None), - &Route::ClusterId(cluster_id1) + &Route::Cluster(cluster_id1) )); let address = SocketAddress::new_v4(127, 0, 0, 1, 1032); @@ -1601,25 +1637,25 @@ mod tests { let frontend1 = listener.frontend_from_request("lolcatho.st", "/", &Method::Get); assert_eq!( frontend1.expect("should find a frontend"), - Route::ClusterId("cluster_1".to_string()) + Route::Cluster("cluster_1".to_string()) ); println!("TEST {}", line!()); let frontend2 = listener.frontend_from_request("lolcatho.st", "/test", &Method::Get); assert_eq!( frontend2.expect("should find a frontend"), - Route::ClusterId("cluster_1".to_string()) + Route::Cluster("cluster_1".to_string()) ); println!("TEST {}", line!()); let frontend3 = listener.frontend_from_request("lolcatho.st", "/yolo/test", &Method::Get); assert_eq!( frontend3.expect("should find a frontend"), - Route::ClusterId("cluster_2".to_string()) + Route::Cluster("cluster_2".to_string()) ); println!("TEST {}", line!()); let frontend4 = listener.frontend_from_request("lolcatho.st", "/yolo/swag", &Method::Get); assert_eq!( frontend4.expect("should find a frontend"), - Route::ClusterId("cluster_3".to_string()) + Route::Cluster("cluster_3".to_string()) ); println!("TEST {}", line!()); let frontend5 = listener.frontend_from_request("domain", "/", &Method::Get); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0c2bca139..7d565afdd 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -466,8 +466,8 @@ macro_rules! StateMachineBuilder { } macro_rules! _fn_impl { - ($function:ident(&$d($mut:ident)?, self $d(,$arg_name:ident: $arg_type:ty)*) $d(-> $ret:ty)? $d(| $marker:tt => $fail:expr)?) => { - fn $function(&$d($mut)? self $d(,$arg_name: $arg_type)*) $d(-> $ret)? { + ($function:ident$d([$d($bounds:tt)*])?(&$d($mut:ident)?, self $d(,$arg_name:ident: $arg_type:ty)*) $d(-> $ret:ty)? $d(| $marker:tt => $fail:expr)?) => { + fn $function$d(<$d($bounds)*>)?(&$d($mut)? self $d(,$arg_name: $arg_type)*) $d(-> $ret)? { match self { $($state_name::$variant_name(_state, ..) => $crate::fallback!({$($override)?} _state.$function($d($arg_name),*)),)+ $state_name::FailedUpgrade($crate::fallback!({$d($marker)?} _)) => $crate::fallback!({$d($fail)?} unreachable!()) @@ -495,9 +495,7 @@ macro_rules! StateMachineBuilder { /// leaving a FailedUpgrade in its place. /// The FailedUpgrade retains the marker of the previous State. fn take(&mut self) -> $state_name { - let mut owned_state = $state_name::FailedUpgrade(self.marker()); - std::mem::swap(&mut owned_state, self); - owned_state + std::mem::replace(self, $state_name::FailedUpgrade(self.marker())) } _fn_impl!{front_socket(&, self) -> &mio::net::TcpStream} } @@ -505,28 +503,32 @@ macro_rules! StateMachineBuilder { $crate::branch!{ if $($trait)? == SessionState { impl SessionState for $state_name { - _fn_impl!{ready(&mut, self, session: Rc>, proxy: Rc>, metrics: &mut SessionMetrics) -> SessionResult} + _fn_impl!{ready[P: L7Proxy](&mut, self, session: Rc>, proxy: Rc>, metrics: &mut SessionMetrics) -> SessionResult} _fn_impl!{update_readiness(&mut, self, token: Token, events: Ready)} _fn_impl!{timeout(&mut, self, token: Token, metrics: &mut SessionMetrics) -> StateResult} _fn_impl!{cancel_timeouts(&mut, self)} _fn_impl!{print_state(&, self, context: &str) | marker => error!("{} Session(FailedUpgrade({:?}))", context, marker)} - _fn_impl!{close(&mut, self, proxy: Rc>, metrics: &mut SessionMetrics) | _ => {}} + _fn_impl!{close[P: L7Proxy](&mut, self, proxy: Rc>, metrics: &mut SessionMetrics) | _ => {}} _fn_impl!{shutting_down(&mut, self) -> SessionIsToBeClosed | _ => true} } } else {} } }; - ($($tt:tt)+) => { - StateMachineBuilder!{($) $($tt)+} + ($(#[$($state_macros:tt)*])* enum $($tt:tt)+) => { + StateMachineBuilder!{($) $(#[$($state_macros)*])* enum $($tt)+} } } pub trait ListenerHandler { - fn get_addr(&self) -> &SocketAddr; + fn protocol(&self) -> Protocol; + + fn address(&self) -> &SocketAddr; + + fn public_address(&self) -> SocketAddr; fn get_tags(&self, key: &str) -> Option<&CachedTags>; - fn get_concatenated_tags(&self, key: &str) -> Option<&str> { + fn concatenated_tags(&self, key: &str) -> Option<&str> { self.get_tags(key).map(|tags| tags.concatenated.as_str()) } @@ -544,9 +546,9 @@ pub enum FrontendFromRequestError { } pub trait L7ListenerHandler { - fn get_sticky_name(&self) -> &str; + fn sticky_name(&self) -> &str; - fn get_connect_timeout(&self) -> u32; + fn connect_timeout(&self) -> u32; /// retrieve a frontend by parsing a request's hostname, uri and method fn frontend_from_request( @@ -581,10 +583,12 @@ pub enum BackendConnectAction { pub enum BackendConnectionError { #[error("Not found: {0:?}")] NotFound(ObjectKind), - #[error("Too many connections on cluster {0:?}")] + #[error("Too many failed attemps on cluster {0:?}")] MaxConnectionRetries(Option), #[error("the sessions slab has reached maximum capacity")] MaxSessionsMemory, + #[error("the checkout pool has reached maximum capacity")] + MaxBuffers, #[error("error from the backend: {0}")] Backend(BackendError), #[error("failed to retrieve the cluster: {0}")] @@ -604,6 +608,8 @@ pub enum RetrieveClusterError { UnauthorizedRoute, #[error("{0}")] RetrieveFrontend(FrontendFromRequestError), + #[error("https redirect")] + HttpsRedirect, } /// Used in sessions diff --git a/lib/src/protocol/kawa_h1/editor.rs b/lib/src/protocol/kawa_h1/editor.rs index 26a8d3658..f0ecb9f05 100644 --- a/lib/src/protocol/kawa_h1/editor.rs +++ b/lib/src/protocol/kawa_h1/editor.rs @@ -7,8 +7,11 @@ use rusty_ulid::Ulid; use crate::{ pool::Checkout, - protocol::http::{parser::compare_no_case, GenericHttpStream, Method}, - Protocol, + protocol::{ + http::{parser::compare_no_case, GenericHttpStream, Method}, + pipe::WebSocketContext, + }, + Protocol, RetrieveClusterError, }; use sozu_command_lib::logging::LogContext; @@ -38,12 +41,13 @@ pub struct HttpContext { pub user_agent: Option, // ========== Read only - /// signals wether Kawa should write a "Connection" header with a "close" value (request and response) + /// signals whether Kawa should write a "Connection" header with a "close" value (request and response) pub closing: bool, /// the value of the custom header, named "Sozu-Id", that Kawa should write (request and response) pub id: Ulid, - pub backend_id: Option, pub cluster_id: Option, + pub backend_id: Option, + pub backend_address: Option, /// the value of the protocol Kawa should write in the Forwarded headers of the request pub protocol: Protocol, /// the value of the public address Kawa should write in the Forwarded headers of the request @@ -67,6 +71,36 @@ impl kawa::h1::ParserCallbacks for HttpContext { } impl HttpContext { + pub fn new( + id: Ulid, + protocol: Protocol, + sticky_name: String, + public_address: SocketAddr, + session_address: Option, + ) -> Self { + Self { + id, + protocol, + sticky_name, + public_address, + session_address, + + cluster_id: None, + backend_id: None, + backend_address: None, + keep_alive_backend: true, + keep_alive_frontend: true, + sticky_session_found: None, + method: None, + authority: None, + path: None, + status: None, + reason: None, + user_agent: None, + closing: false, + sticky_session: None, + } + } /// Callback for request: /// /// - edit headers (connection, forwarded, sticky cookie, sozu-id) @@ -78,6 +112,14 @@ impl HttpContext { /// - sticky cookie /// - user-agent fn on_request_headers(&mut self, request: &mut GenericHttpStream) { + if request.body_size == kawa::BodySize::Empty { + request.parsing_phase = kawa::ParsingPhase::Terminated; + // request.push_block(kawa::Block::Header(kawa::Pair { + // key: kawa::Store::Static(b"Content-Length"), + // val: kawa::Store::Static(b"0"), + // })); + }; + let buf = &mut request.storage.mut_buffer(); // Captures the request line @@ -139,7 +181,9 @@ impl HttpContext { match block { kawa::Block::Header(header) if !header.is_elided() => { let key = header.key.data(buf); - if compare_no_case(key, b"connection") { + if compare_no_case(key, b"te") { + header.elide(); + } else if compare_no_case(key, b"connection") { has_connection = true; if self.closing { header.val = kawa::Store::Static(b"close"); @@ -356,6 +400,7 @@ impl HttpContext { self.status = None; self.reason = None; self.user_agent = None; + self.id = Ulid::generate(); } pub fn log_context(&self) -> LogContext { @@ -365,4 +410,39 @@ impl HttpContext { backend_id: self.backend_id.as_deref(), } } + + // -> host, path, method + pub fn extract_route(&self) -> Result<(&str, &str, &Method), RetrieveClusterError> { + let given_method = self.method.as_ref().ok_or(RetrieveClusterError::NoMethod)?; + let given_authority = self + .authority + .as_deref() + .ok_or(RetrieveClusterError::NoHost)?; + let given_path = self.path.as_deref().ok_or(RetrieveClusterError::NoPath)?; + + Ok((given_authority, given_path, given_method)) + } + + pub fn get_route(&self) -> String { + if let Some(method) = &self.method { + if let Some(authority) = &self.authority { + if let Some(path) = &self.path { + return format!("{method} {authority}{path}"); + } + return format!("{method} {authority}"); + } + return format!("{method}"); + } + String::new() + } + + pub fn websocket_context(&self) -> WebSocketContext { + WebSocketContext::Http { + method: self.method.clone(), + authority: self.authority.clone(), + path: self.path.clone(), + reason: self.reason.clone(), + status: self.status, + } + } } diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index 270ca7e7e..db8e06d25 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -30,7 +30,6 @@ use crate::{ editor::HttpContext, parser::Method, }, - pipe::WebSocketContext, SessionState, }, retry::RetryPolicy, @@ -243,8 +242,9 @@ impl Http Http return, }; - self.context.id = Ulid::generate(); self.context.reset(); self.request_stream.clear(); @@ -917,17 +916,6 @@ impl Http WebSocketContext { - WebSocketContext::Http { - method: self.context.method.clone(), - authority: self.context.authority.clone(), - path: self.context.path.clone(), - reason: self.context.reason.clone(), - status: self.context.status, - } - } - pub fn log_request(&self, metrics: &SessionMetrics, error: bool, message: Option<&str>) { let listener = self.listener.borrow(); let tags = self.context.authority.as_ref().and_then(|host| { @@ -1040,7 +1028,7 @@ impl Http Http>, metrics: &mut SessionMetrics) { + fn close_backend(&mut self, proxy: Rc>, metrics: &mut SessionMetrics) { self.container_backend_timeout.cancel(); debug!( "{}\tPROXY [{}->{}] CLOSED BACKEND", @@ -1214,45 +1202,11 @@ impl Http host, path, method - pub fn extract_route(&self) -> Result<(&str, &str, &Method), RetrieveClusterError> { - let given_method = self - .context - .method - .as_ref() - .ok_or(RetrieveClusterError::NoMethod)?; - let given_authority = self - .context - .authority - .as_deref() - .ok_or(RetrieveClusterError::NoHost)?; - let given_path = self - .context - .path - .as_deref() - .ok_or(RetrieveClusterError::NoPath)?; - - Ok((given_authority, given_path, given_method)) - } - - pub fn get_route(&self) -> String { - if let Some(method) = &self.context.method { - if let Some(authority) = &self.context.authority { - if let Some(path) = &self.context.path { - return format!("{method} {authority}{path}"); - } - return format!("{method} {authority}"); - } - return format!("{method}"); - } - String::new() - } - - fn cluster_id_from_request( + fn cluster_id_from_request( &mut self, - proxy: Rc>, + proxy: Rc>, ) -> Result { - let (host, uri, method) = match self.extract_route() { + let (host, uri, method) = match self.context.extract_route() { Ok(tuple) => tuple, Err(cluster_error) => { self.set_answer(DefaultAnswer::Answer400 { @@ -1280,7 +1234,7 @@ impl Http cluster_id, + Route::Cluster(id) => id, Route::Deny => { self.set_answer(DefaultAnswer::Answer401 {}); return Err(RetrieveClusterError::UnauthorizedRoute); @@ -1305,11 +1259,11 @@ impl Http( &mut self, cluster_id: &str, frontend_should_stick: bool, - proxy: Rc>, + proxy: Rc>, metrics: &mut SessionMetrics, ) -> Result { let (backend, conn) = self @@ -1330,7 +1284,7 @@ impl Http Http( &self, frontend_should_stick: bool, sticky_session: Option<&str>, cluster_id: &str, - proxy: Rc>, + proxy: Rc>, ) -> Result<(Rc>, TcpStream), BackendError> { match (frontend_should_stick, sticky_session) { (true, Some(sticky_session)) => proxy @@ -1370,10 +1324,10 @@ impl Http( &mut self, session_rc: Rc>, - proxy: Rc>, + proxy: Rc>, metrics: &mut SessionMetrics, ) -> Result { let old_cluster_id = self.context.cluster_id.clone(); @@ -1665,10 +1619,10 @@ impl Http( &mut self, session: Rc>, - proxy: Rc>, + proxy: Rc>, metrics: &mut SessionMetrics, ) -> SessionResult { let mut counter = 0; @@ -1872,10 +1826,10 @@ impl Http SessionState for Http { - fn ready( + fn ready( &mut self, session: Rc>, - proxy: Rc>, + proxy: Rc>, metrics: &mut SessionMetrics, ) -> SessionResult { let session_result = self.ready_inner(session, proxy, metrics); @@ -1906,7 +1860,7 @@ impl SessionState } } - fn close(&mut self, proxy: Rc>, metrics: &mut SessionMetrics) { + fn close(&mut self, proxy: Rc>, metrics: &mut SessionMetrics) { self.close_backend(proxy, metrics); self.frontend_socket.socket_close(); let _ = self.frontend_socket.socket_write_vectored(&[]); diff --git a/lib/src/protocol/mod.rs b/lib/src/protocol/mod.rs index 1342173a8..618939c9f 100644 --- a/lib/src/protocol/mod.rs +++ b/lib/src/protocol/mod.rs @@ -1,5 +1,6 @@ pub mod h2; pub mod kawa_h1; +pub mod mux; pub mod pipe; pub mod proxy_protocol; pub mod rustls; @@ -23,17 +24,17 @@ pub trait SessionState { /// if a session received an event or can still execute, the event loop will /// call this method. Its result indicates if it can still execute or if the /// session can be closed - fn ready( + fn ready( &mut self, session: Rc>, - proxy: Rc>, + proxy: Rc>, metrics: &mut SessionMetrics, ) -> SessionResult; /// if the event loop got an event for a token associated with the session, /// it will call this method fn update_readiness(&mut self, token: Token, events: Ready); /// close the state - fn close(&mut self, _proxy: Rc>, _metrics: &mut SessionMetrics) {} + fn close(&mut self, _proxy: Rc>, _metrics: &mut SessionMetrics) {} /// if a timeout associated with the session triggers, the event loop will /// call this method with the timeout's token fn timeout(&mut self, token: Token, metrics: &mut SessionMetrics) -> StateResult; diff --git a/lib/src/protocol/mux/converter.rs b/lib/src/protocol/mux/converter.rs new file mode 100644 index 000000000..df283a3c7 --- /dev/null +++ b/lib/src/protocol/mux/converter.rs @@ -0,0 +1,226 @@ +use std::cmp::min; + +use kawa::{ + AsBuffer, Block, BlockConverter, Chunk, Flags, Kawa, Pair, ParsingErrorKind, ParsingPhase, + StatusLine, Store, +}; + +use crate::protocol::{ + http::parser::compare_no_case, + mux::{ + parser::{str_to_error_code, FrameHeader, FrameType, H2Error}, + serializer::{gen_frame_header, gen_rst_stream}, + StreamId, + }, +}; + +pub struct H2BlockConverter<'a> { + pub max_frame_size: usize, + pub window: i32, + pub stream_id: StreamId, + pub encoder: &'a mut hpack::Encoder<'static>, + pub out: Vec, +} + +impl<'a, T: AsBuffer> BlockConverter for H2BlockConverter<'a> { + fn initialize(&mut self, kawa: &mut Kawa) { + // This is very ugly... we may add a h2 variant in kawa::ParsingErrorKind + match kawa.parsing_phase { + ParsingPhase::Error { + kind: ParsingErrorKind::Processing { message }, + .. + } => { + let error = str_to_error_code(message); + let mut frame = [0; 13]; + gen_rst_stream(&mut frame, self.stream_id, error).unwrap(); + kawa.push_out(Store::from_slice(&frame)); + } + ParsingPhase::Error { .. } => { + let mut frame = [0; 13]; + gen_rst_stream(&mut frame, self.stream_id, H2Error::InternalError).unwrap(); + kawa.push_out(Store::from_slice(&frame)); + } + _ => {} + } + } + fn call(&mut self, block: Block, kawa: &mut Kawa) -> bool { + let buffer = kawa.storage.buffer(); + match block { + Block::StatusLine => match kawa.detached.status_line.pop() { + StatusLine::Request { + method, + authority, + path, + .. + } => { + self.encoder + .encode_header_into((b":method", method.data(buffer)), &mut self.out) + .unwrap(); + self.encoder + .encode_header_into((b":authority", authority.data(buffer)), &mut self.out) + .unwrap(); + self.encoder + .encode_header_into((b":path", path.data(buffer)), &mut self.out) + .unwrap(); + self.encoder + .encode_header_into((b":scheme", b"https"), &mut self.out) + .unwrap(); + } + StatusLine::Response { status, .. } => { + self.encoder + .encode_header_into((b":status", status.data(buffer)), &mut self.out) + .unwrap(); + } + StatusLine::Unknown => unreachable!(), + }, + Block::Cookies => { + if kawa.detached.jar.is_empty() { + return true; + } + for cookie in kawa + .detached + .jar + .drain(..) + .filter(|cookie| !cookie.is_elided()) + { + let cookie = [cookie.key.data(buffer), b"=", cookie.val.data(buffer)].concat(); + self.encoder + .encode_header_into((b"cookie", &cookie), &mut self.out) + .unwrap(); + } + } + Block::Header(Pair { + key: Store::Empty, .. + }) => { + // elided header + } + Block::Header(Pair { key, val }) => { + { + let key = key.data(buffer); + let val = val.data(buffer); + if compare_no_case(key, b"connection") + || compare_no_case(key, b"host") + || compare_no_case(key, b"http2-settings") + || compare_no_case(key, b"keep-alive") + || compare_no_case(key, b"proxy-connection") + || compare_no_case(key, b"te") && !compare_no_case(val, b"trailers") + || compare_no_case(key, b"trailer") + || compare_no_case(key, b"transfer-encoding") + || compare_no_case(key, b"upgrade") + { + return true; + } + } + self.encoder + .encode_header_into( + (&key.data(buffer).to_ascii_lowercase(), val.data(buffer)), + &mut self.out, + ) + .unwrap(); + } + Block::ChunkHeader(_) => { + // this converter doesn't align H1 chunks on H2 data frames + } + Block::Chunk(Chunk { data }) => { + let mut header = [0; 9]; + let payload_len = data.len(); + let (data, payload_len, can_continue) = + if self.window >= payload_len as i32 && self.max_frame_size >= payload_len { + // the window is wide enought to send the entire chunk + (data, payload_len as u32, true) + } else if self.window > 0 { + // we split the chunk to fit in the window + let payload_len = min(self.max_frame_size, self.window as usize); + let (before, after) = data.split(payload_len); + kawa.blocks.push_front(Block::Chunk(Chunk { data: after })); + ( + before, + payload_len as u32, + self.max_frame_size < self.window as usize, + ) + } else { + // the window can't take any more bytes, return the chunk to the blocks + kawa.blocks.push_front(Block::Chunk(Chunk { data })); + return false; + }; + self.window -= payload_len as i32; + gen_frame_header( + &mut header, + &FrameHeader { + payload_len, + frame_type: FrameType::Data, + flags: 0, + stream_id: self.stream_id, + }, + ) + .unwrap(); + kawa.push_out(Store::from_slice(&header)); + kawa.push_out(data); + // kawa.push_delimiter(); + return can_continue; + } + Block::Flags(Flags { + end_header, + end_stream, + .. + }) => { + let sent_end_stream = if end_header { + let payload = std::mem::take(&mut self.out); + let mut header = [0; 9]; + let chunks = payload.chunks(self.max_frame_size); + let n_chunks = chunks.len(); + for (i, chunk) in chunks.enumerate() { + let flags = if i == 0 && end_stream { 1 } else { 0 } + | if i + 1 == n_chunks { 4 } else { 0 }; + if i == 0 { + gen_frame_header( + &mut header, + &FrameHeader { + payload_len: chunk.len() as u32, + frame_type: FrameType::Headers, + flags, + stream_id: self.stream_id, + }, + ) + .unwrap(); + } else { + gen_frame_header( + &mut header, + &FrameHeader { + payload_len: chunk.len() as u32, + frame_type: FrameType::Continuation, + flags, + stream_id: self.stream_id, + }, + ) + .unwrap(); + } + kawa.push_out(Store::from_slice(&header)); + kawa.push_out(Store::from_slice(chunk)); + } + n_chunks > 0 + } else { + false + }; + if end_stream && !sent_end_stream { + let mut header = [0; 9]; + gen_frame_header( + &mut header, + &FrameHeader { + payload_len: 0, + frame_type: FrameType::Data, + flags: 1, + stream_id: self.stream_id, + }, + ) + .unwrap(); + kawa.push_out(Store::from_slice(&header)); + } + } + } + true + } + fn finalize(&mut self, _kawa: &mut Kawa) { + assert!(self.out.is_empty()); + } +} diff --git a/lib/src/protocol/mux/h1.rs b/lib/src/protocol/mux/h1.rs new file mode 100644 index 000000000..f6b656583 --- /dev/null +++ b/lib/src/protocol/mux/h1.rs @@ -0,0 +1,374 @@ +use std::time::Instant; + +use sozu_command::ready::Ready; + +use crate::{ + println_, + protocol::mux::{ + debug_kawa, forcefully_terminate_answer, parser::H2Error, set_default_answer, + update_readiness_after_read, update_readiness_after_write, BackendStatus, Context, + DebugEvent, Endpoint, GlobalStreamId, MuxResult, Position, StreamState, + }, + socket::SocketHandler, + timer::TimeoutContainer, + L7ListenerHandler, ListenerHandler, Readiness, +}; + +pub struct ConnectionH1 { + pub position: Position, + pub readiness: Readiness, + pub requests: usize, + pub socket: Front, + /// note: a Server H1 will always reference stream 0, but a client can reference any stream + pub stream: GlobalStreamId, + pub timeout_container: TimeoutContainer, +} + +impl std::fmt::Debug for ConnectionH1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConnectionH1") + .field("position", &self.position) + .field("readiness", &self.readiness) + .field("socket", &self.socket.socket_ref()) + .field("stream", &self.stream) + .finish() + } +} + +impl ConnectionH1 { + pub fn readable(&mut self, context: &mut Context, mut endpoint: E) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + println_!("======= MUX H1 READABLE {:?}", self.position); + self.timeout_container.reset(); + let stream = &mut context.streams[self.stream]; + if stream.metrics.start.is_none() { + stream.metrics.start = Some(Instant::now()); + } + let parts = stream.split(&self.position); + let kawa = parts.rbuffer; + let (size, status) = self.socket.socket_read(kawa.storage.space()); + context.debug.push(DebugEvent::I2(0, size)); + kawa.storage.fill(size); + match self.position { + Position::Client(..) => { + count!("back_bytes_in", size as i64); + parts.metrics.backend_bin += size; + } + Position::Server => { + count!("bytes_in", size as i64); + parts.metrics.bin += size; + } + } + if update_readiness_after_read(size, status, &mut self.readiness) { + return MuxResult::Continue; + } + + let was_main_phase = kawa.is_main_phase(); + kawa::h1::parse(kawa, parts.context); + debug_kawa(kawa); + if kawa.is_error() { + match self.position { + Position::Client(..) => { + let StreamState::Linked(token) = stream.state else { + unreachable!() + }; + let global_stream_id = self.stream; + self.end_stream(global_stream_id, context); + endpoint.end_stream(token, global_stream_id, context); + } + Position::Server => { + set_default_answer(stream, &mut self.readiness, 400); + } + } + return MuxResult::Continue; + } + if kawa.is_terminated() { + self.timeout_container.cancel(); + self.readiness.interest.remove(Ready::READABLE); + } + if kawa.is_main_phase() { + if !was_main_phase && self.position.is_server() { + if parts.context.method.is_none() + || parts.context.authority.is_none() + || parts.context.path.is_none() + { + if let kawa::StatusLine::Request { + version: kawa::Version::V10, + .. + } = kawa.detached.status_line + { + error!( + "Unexpected malformed request: HTTP/1.0 from {:?} with {:?} {:?} {:?}", + parts.context.session_address, + parts.context.method, + parts.context.authority, + parts.context.path + ); + } else { + error!("Unexpected malformed request"); + kawa::debug_kawa(kawa); + } + set_default_answer(stream, &mut self.readiness, 400); + return MuxResult::Continue; + } + self.requests += 1; + println_!("REQUESTS: {}", self.requests); + gauge_add!("http.active_requests", 1); + stream.state = StreamState::Link + } + if let StreamState::Linked(token) = stream.state { + endpoint + .readiness_mut(token) + .interest + .insert(Ready::WRITABLE) + } + }; + MuxResult::Continue + } + + pub fn writable(&mut self, context: &mut Context, mut endpoint: E) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + println_!("======= MUX H1 WRITABLE {:?}", self.position); + self.timeout_container.reset(); + let stream = &mut context.streams[self.stream]; + let parts = stream.split(&self.position); + let kawa = parts.wbuffer; + kawa.prepare(&mut kawa::h1::BlockConverter); + debug_kawa(kawa); + let bufs = kawa.as_io_slice(); + if bufs.is_empty() && !self.socket.socket_wants_write() { + self.readiness.interest.remove(Ready::WRITABLE); + return MuxResult::Continue; + } + let (size, status) = self.socket.socket_write_vectored(&bufs); + context.debug.push(DebugEvent::I2(1, size)); + kawa.consume(size); + match self.position { + Position::Client(..) => { + count!("back_bytes_out", size as i64); + parts.metrics.backend_bout += size; + } + Position::Server => { + count!("bytes_out", size as i64); + parts.metrics.bout += size; + } + } + if update_readiness_after_write(size, status, &mut self.readiness) + || self.socket.socket_wants_write() + { + return MuxResult::Continue; + } + + if kawa.is_terminated() && kawa.is_completed() { + match self.position { + Position::Client(..) => self.readiness.interest.insert(Ready::READABLE), + Position::Server => { + if stream.context.closing { + return MuxResult::CloseSession; + } + let kawa = &mut stream.back; + match kawa.detached.status_line { + kawa::StatusLine::Response { code: 101, .. } => { + debug!("============== HANDLE UPGRADE!"); + stream.generate_access_log( + false, + Some(String::from("H1::Upgrade")), + context.listener.clone(), + ); + return MuxResult::Upgrade; + } + kawa::StatusLine::Response { code: 100, .. } => { + debug!("============== HANDLE CONTINUE!"); + // after a 100 continue, we expect the client to continue with its request + self.timeout_container.reset(); + self.readiness.interest.insert(Ready::READABLE); + kawa.clear(); + stream.generate_access_log( + false, + Some(String::from("H1::Continue")), + context.listener.clone(), + ); + return MuxResult::Continue; + } + kawa::StatusLine::Response { code: 103, .. } => { + debug!("============== HANDLE EARLY HINT!"); + if let StreamState::Linked(token) = stream.state { + // after a 103 early hints, we expect the backend to send its response + endpoint + .readiness_mut(token) + .interest + .insert(Ready::READABLE); + kawa.clear(); + stream.generate_access_log( + false, + Some(String::from("H1::EarlyHint+Error")), + context.listener.clone(), + ); + return MuxResult::Continue; + } else { + stream.generate_access_log( + false, + Some(String::from("H1::EarlyHint")), + context.listener.clone(), + ); + return MuxResult::CloseSession; + } + } + _ => {} + } + incr!("http.e2e.http11"); + stream.generate_access_log( + false, + Some(String::from("H1::Continue")), + context.listener.clone(), + ); + stream.metrics.reset(); + let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked); + if stream.context.keep_alive_frontend { + self.timeout_container.reset(); + if let StreamState::Linked(token) = old_state { + endpoint.end_stream(token, self.stream, context); + } + self.readiness.interest.insert(Ready::READABLE); + let stream = &mut context.streams[self.stream]; + stream.context.reset(); + stream.back.clear(); + stream.back.storage.clear(); + stream.front.clear(); + // do not stream.front.storage.clear() because of H1 pipelining + stream.attempts = 0; + } else { + return MuxResult::CloseSession; + } + } + } + } + MuxResult::Continue + } + + pub fn force_disconnect(&mut self) -> MuxResult { + match &mut self.position { + Position::Client(_, _, status) => { + *status = BackendStatus::Disconnecting; + self.readiness.event = Ready::HUP; + MuxResult::Continue + } + Position::Server => MuxResult::CloseSession, + } + } + + pub fn close(&mut self, context: &mut Context, mut endpoint: E) + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + match self.position { + Position::Client(_, _, BackendStatus::KeepAlive) + | Position::Client(_, _, BackendStatus::Disconnecting) => { + println_!("close detached client ConnectionH1"); + return; + } + Position::Client(_, _, BackendStatus::Connecting(_)) + | Position::Client(_, _, BackendStatus::Connected) => { + warn!("BACKEND CLOSING FOR: {:?} {}", self.position, self.stream); + } + Position::Server => { + println_!("H1 SENDING CLOSE NOTIFY"); + self.socket.socket_close(); + let _ = self.socket.socket_write_vectored(&[]); + return; + } + } + // reconnection is handled by the server + let StreamState::Linked(token) = context.streams[self.stream].state else { + unreachable!() + }; + endpoint.end_stream(token, self.stream, context) + } + + pub fn end_stream(&mut self, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + assert_eq!(stream, self.stream); + let stream = &mut context.streams[stream]; + let stream_context = &mut stream.context; + println_!("end H1 stream {}: {stream_context:#?}", self.stream); + match &mut self.position { + Position::Client(_, _, BackendStatus::Connecting(_)) => { + self.stream = usize::MAX; + self.readiness.interest.remove(Ready::ALL); + self.force_disconnect(); + } + Position::Client(_, _, status @ BackendStatus::Connected) => { + self.stream = usize::MAX; + self.readiness.interest.remove(Ready::ALL); + // keep alive should probably be used only if the http context is fully reset + // in case end_stream occurs due to an error the connection state is probably + // unrecoverable and should be terminated + if stream_context.keep_alive_backend && stream.back.is_terminated() { + *status = BackendStatus::KeepAlive; + } else { + self.force_disconnect(); + } + } + Position::Client(_, _, BackendStatus::KeepAlive) + | Position::Client(_, _, BackendStatus::Disconnecting) => unreachable!(), + Position::Server => match (stream.front.consumed, stream.back.is_main_phase()) { + (true, true) => { + // we have a "forwardable" answer from the back + // if the answer is not terminated we send an RstStream to properly clean the stream + // if it is terminated, we finish the transfer, the backend is not necessary anymore + if !stream.context.keep_alive_backend { + warn!("CLOSE DELIMITED"); + // TODO why not force_disconnect? + self.readiness.event = Ready::HUP; + } else if !stream.back.is_terminated() { + forcefully_terminate_answer( + stream, + &mut self.readiness, + H2Error::InternalError, + ); + } else { + stream.state = StreamState::Unlinked; + self.readiness.interest.insert(Ready::WRITABLE); + } + } + (true, false) => { + // we do not have an answer, but the request has already been partially consumed + // so we can't retry, send a 502 bad gateway instead + set_default_answer(stream, &mut self.readiness, 502); + } + (false, false) => { + // we do not have an answer, but the request is untouched so we can retry + debug!("H1 RECONNECT"); + stream.state = StreamState::Link; + } + (false, true) => unreachable!(), + }, + } + } + + pub fn start_stream(&mut self, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + println_!("start H1 stream {stream} {:?}", self.readiness); + self.readiness.interest.insert(Ready::ALL); + self.stream = stream; + match &mut self.position { + Position::Client(_, _, status @ BackendStatus::KeepAlive) => { + *status = BackendStatus::Connected; + } + Position::Client(_, _, BackendStatus::Disconnecting) => unreachable!(), + Position::Client(_, _, _) => {} + Position::Server => unreachable!(), + } + } +} diff --git a/lib/src/protocol/mux/h2.rs b/lib/src/protocol/mux/h2.rs new file mode 100644 index 000000000..c2d1d6312 --- /dev/null +++ b/lib/src/protocol/mux/h2.rs @@ -0,0 +1,1198 @@ +use std::{cmp::min, collections::HashMap}; + +use rusty_ulid::Ulid; +use sozu_command::ready::Ready; + +use crate::{ + println_, + protocol::mux::{ + converter, debug_kawa, forcefully_terminate_answer, + parser::{ + self, error_code_to_str, Frame, FrameHeader, FrameType, H2Error, Headers, ParserError, + ParserErrorKind, WindowUpdate, + }, + pkawa, serializer, set_default_answer, update_readiness_after_read, + update_readiness_after_write, BackendStatus, Context, DebugEvent, Endpoint, + GenericHttpStream, GlobalStreamId, MuxResult, Position, StreamId, StreamState, + }, + socket::SocketHandler, + timer::TimeoutContainer, + L7ListenerHandler, ListenerHandler, Readiness, +}; + +#[inline(always)] +fn error_nom_to_h2(error: nom::Err) -> H2Error { + match error { + nom::Err::Error(parser::ParserError { + kind: parser::ParserErrorKind::H2(e), + .. + }) => e, + nom::Err::Failure(parser::ParserError { + kind: parser::ParserErrorKind::H2(e), + .. + }) => e, + _ => H2Error::ProtocolError, + } +} + +#[derive(Debug)] +pub enum H2State { + ClientPreface, + ClientSettings, + ServerSettings, + Header, + Frame(FrameHeader), + ContinuationHeader(Headers), + ContinuationFrame(Headers), + GoAway, + Error, + Discard, +} + +#[derive(Debug)] +pub struct H2Settings { + pub settings_header_table_size: u32, + pub settings_enable_push: bool, + pub settings_max_concurrent_streams: u32, + pub settings_initial_window_size: u32, + pub settings_max_frame_size: u32, + pub settings_max_header_list_size: u32, + /// RFC 8441 + pub settings_enable_connect_protocol: bool, + /// RFC 9218 + pub settings_no_rfc7540_priorities: bool, +} + +impl Default for H2Settings { + fn default() -> Self { + Self { + settings_header_table_size: 4096, + settings_enable_push: false, + settings_max_concurrent_streams: 100, + settings_initial_window_size: (1 << 16) - 1, + settings_max_frame_size: 1 << 14, + settings_max_header_list_size: u32::MAX, + settings_enable_connect_protocol: false, + settings_no_rfc7540_priorities: true, + } + } +} + +#[derive(Default)] +pub struct Prioriser {} + +impl Prioriser { + pub fn push_priority(&mut self, stream_id: StreamId, priority: parser::PriorityPart) -> bool { + println_!("PRIORITY REQUEST FOR {stream_id}: {priority:?}"); + match priority { + parser::PriorityPart::Rfc7540 { + stream_dependency, + weight, + } => { + if stream_dependency.stream_id == stream_id { + error!("STREAM CAN'T DEPEND ON ITSELF"); + true + } else { + false + } + } + parser::PriorityPart::Rfc9218 { + urgency, + incremental, + } => false, + } + } +} + +pub struct ConnectionH2 { + pub decoder: hpack::Decoder<'static>, + pub encoder: hpack::Encoder<'static>, + pub expect_read: Option<(H2StreamId, usize)>, + pub expect_write: Option, + pub last_stream_id: StreamId, + pub local_settings: H2Settings, + pub peer_settings: H2Settings, + pub position: Position, + pub prioriser: Prioriser, + pub readiness: Readiness, + pub socket: Front, + pub state: H2State, + pub streams: HashMap, + pub timeout_container: TimeoutContainer, + pub window: i32, + pub zero: GenericHttpStream, +} +impl std::fmt::Debug for ConnectionH2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConnectionH2") + .field("position", &self.position) + .field("state", &self.state) + .field("expect", &self.expect_read) + .field("readiness", &self.readiness) + .field("local_settings", &self.local_settings) + .field("peer_settings", &self.peer_settings) + .field("socket", &self.socket.socket_ref()) + .field("streams", &self.streams) + .field("zero", &self.zero.storage.meter(20)) + .field("window", &self.window) + .finish() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum H2StreamId { + Zero, + Other(StreamId, GlobalStreamId), +} + +impl ConnectionH2 { + fn expect_header(&mut self) { + self.state = H2State::Header; + self.expect_read = Some((H2StreamId::Zero, 9)); + } + pub fn readable(&mut self, context: &mut Context, endpoint: E) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + println_!("======= MUX H2 READABLE {:?}", self.position); + self.timeout_container.reset(); + let (stream_id, kawa) = if let Some((stream_id, amount)) = self.expect_read { + let (kawa, did) = match stream_id { + H2StreamId::Zero => (&mut self.zero, usize::MAX), + H2StreamId::Other(_, global_stream_id) => ( + context.streams[global_stream_id] + .split(&self.position) + .rbuffer, + global_stream_id, + ), + }; + println_!("{:?}({stream_id:?}, {amount})", self.state); + if amount > 0 { + if amount > kawa.storage.available_space() { + self.readiness.interest.remove(Ready::READABLE); + return MuxResult::Continue; + } + let (size, status) = self.socket.socket_read(&mut kawa.storage.space()[..amount]); + context.debug.push(DebugEvent::I3(0, did, size)); + kawa.storage.fill(size); + match self.position { + Position::Client(..) => { + count!("back_bytes_in", size as i64); + } + Position::Server => { + count!("bytes_in", size as i64); + } + } + if update_readiness_after_read(size, status, &mut self.readiness) { + return MuxResult::Continue; + } else if size == amount { + self.expect_read = None; + } else { + self.expect_read = Some((stream_id, amount - size)); + match (&self.state, &self.position) { + (H2State::ClientPreface, Position::Server) => { + let i = kawa.storage.data(); + if !b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".starts_with(i) { + println_!("EARLY INVALID PREFACE: {i:?}"); + return self.force_disconnect(); + } + } + _ => {} + } + return MuxResult::Continue; + } + } else { + self.expect_read = None; + } + (stream_id, kawa) + } else { + self.readiness.event.remove(Ready::READABLE); + return MuxResult::Continue; + }; + match (&self.state, &self.position) { + (H2State::Error, _) + | (H2State::GoAway, _) + | (H2State::ServerSettings, Position::Server) + | (H2State::ClientPreface, Position::Client(..)) + | (H2State::ClientSettings, Position::Client(..)) => unreachable!( + "Unexpected combination: (Readable, {:?}, {:?})", + self.state, self.position + ), + (H2State::Discard, _) => { + let i = kawa.storage.data(); + println_!("DISCARDING: {i:?}"); + kawa.storage.clear(); + self.expect_header(); + } + (H2State::ClientPreface, Position::Server) => { + let i = kawa.storage.data(); + let i = match parser::preface(i) { + Ok((i, _)) => i, + Err(_) => return self.force_disconnect(), + }; + match parser::frame_header(i, self.local_settings.settings_max_frame_size) { + Ok(( + _, + FrameHeader { + payload_len, + frame_type: FrameType::Settings, + flags: 0, + stream_id: 0, + }, + )) => { + kawa.storage.clear(); + self.state = H2State::ClientSettings; + self.expect_read = Some((H2StreamId::Zero, payload_len as usize)); + } + _ => return self.force_disconnect(), + }; + } + (H2State::ClientSettings, Position::Server) => { + let i = kawa.storage.data(); + let settings = match parser::settings_frame( + i, + &FrameHeader { + payload_len: i.len() as u32, + frame_type: FrameType::Settings, + flags: 0, + stream_id: 0, + }, + ) { + Ok((_, settings)) => { + kawa.storage.clear(); + settings + } + Err(_) => return self.force_disconnect(), + }; + let kawa = &mut self.zero; + match serializer::gen_settings(kawa.storage.space(), &self.local_settings) { + Ok((_, size)) => kawa.storage.fill(size), + Err(error) => { + error!("Could not serialize SettingsFrame: {:?}", error); + return self.force_disconnect(); + } + }; + + self.state = H2State::ServerSettings; + self.expect_write = Some(H2StreamId::Zero); + return self.handle_frame(settings, context, endpoint); + } + (H2State::ServerSettings, Position::Client(..)) => { + let i = kawa.storage.data(); + match parser::frame_header(i, self.local_settings.settings_max_frame_size) { + Ok(( + _, + header @ FrameHeader { + payload_len, + frame_type: FrameType::Settings, + flags: 0, + stream_id: 0, + }, + )) => { + kawa.storage.clear(); + self.expect_read = Some((H2StreamId::Zero, payload_len as usize)); + self.state = H2State::Frame(header) + } + _ => return self.force_disconnect(), + }; + } + (H2State::Header, _) => { + let i = kawa.storage.data(); + println_!(" header: {i:?}"); + match parser::frame_header(i, self.local_settings.settings_max_frame_size) { + Ok((_, header)) => { + println_!("{header:#?}"); + kawa.storage.clear(); + let stream_id = header.stream_id; + let read_stream = if stream_id == 0 { + H2StreamId::Zero + } else if let Some(global_stream_id) = self.streams.get(&stream_id) { + let allowed_on_half_closed = header.frame_type + == FrameType::WindowUpdate + || header.frame_type == FrameType::Priority + || header.frame_type == FrameType::RstStream; + let stream = &context.streams[*global_stream_id]; + println_!( + "REQUESTING EXISTING STREAM {stream_id}: {}/{:?}", + stream.received_end_of_stream, + stream.state + ); + if !allowed_on_half_closed + && (stream.received_end_of_stream || !stream.state.is_open()) + { + error!( + "CANNOT RECEIVE {:?} ON THIS STREAM {:?}", + header.frame_type, stream.state + ); + return self.goaway(H2Error::StreamClosed); + } + if header.frame_type == FrameType::Data { + H2StreamId::Other(stream_id, *global_stream_id) + } else { + H2StreamId::Zero + } + } else { + if header.frame_type == FrameType::Headers + && self.position.is_server() + && stream_id % 2 == 1 + && stream_id >= self.last_stream_id + { + if context.active_len() + >= self.local_settings.settings_max_concurrent_streams as usize + { + error!( + "MAX CONCURRENT STREAMS: {} {} {}", + self.local_settings.settings_max_concurrent_streams, + context.active_len(), + context.streams.len() + ); + return self.goaway(H2Error::RefusedStream); + } + match self.create_stream(stream_id, context) { + Some(_) => {} + None => { + error!("COULD NOT CREATE NEW STREAM"); + return self.goaway(H2Error::InternalError); + } + } + } else if header.frame_type != FrameType::Priority { + if header.frame_type == FrameType::Data + && header.payload_len == 0 + && header.flags == 1 + { + // error!( + // "SKIPPED DATA: {} {} {} {}", + // stream_id, + // self.last_stream_id, + // header.flags, + // header.payload_len + // ); + self.expect_header(); + return MuxResult::Continue; + } + error!( + "CANNOT RECEIVE {:?} FRAME ON IDLE/CLOSED STREAMS", + header.frame_type + ); + return self.goaway(H2Error::ProtocolError); + } + H2StreamId::Zero + }; + println_!("{} {stream_id:?} {:#?}", header.stream_id, self.streams); + self.expect_read = Some((read_stream, header.payload_len as usize)); + self.state = H2State::Frame(header); + } + Err(nom::Err::Failure(ParserError { + kind: ParserErrorKind::UnknownFrame(skip), + .. + })) => { + self.expect_read = Some((H2StreamId::Zero, skip as usize)); + self.state = H2State::Discard; + } + Err(error) => { + let error = error_nom_to_h2(error); + error!("COULD NOT PARSE FRAME HEADER"); + return self.goaway(error); + } + }; + } + (H2State::ContinuationHeader(headers), _) => { + let i = kawa.storage.unparsed_data(); + println_!(" continuation header: {i:?}"); + match parser::frame_header(i, self.local_settings.settings_max_frame_size) { + Ok(( + _, + FrameHeader { + payload_len, + frame_type: FrameType::Continuation, + flags, + stream_id, + }, + )) => { + // println_!("{header:#?}"); + kawa.storage.end -= 9; + assert_eq!(stream_id, headers.stream_id); + self.expect_read = Some((H2StreamId::Zero, payload_len as usize)); + let mut headers = headers.clone(); + headers.end_headers = flags & 0x4 != 0; + headers.header_block_fragment.len += payload_len; + self.state = H2State::ContinuationFrame(headers); + } + Err(error) => { + let error = error_nom_to_h2(error); + error!("COULD NOT PARSE CONTINUATION HEADER"); + return self.goaway(error); + } + other => { + error!("UNEXPECTED {:?} WHILE PARSING CONTINUATION HEADER", other); + return self.goaway(H2Error::ProtocolError); + } + }; + } + (H2State::Frame(header), _) => { + let i = kawa.storage.unparsed_data(); + println_!(" data: {i:?}"); + let frame = match parser::frame_body(i, header) { + Ok((_, frame)) => frame, + Err(error) => { + let error = error_nom_to_h2(error); + error!("COULD NOT PARSE FRAME BODY"); + return self.goaway(error); + } + }; + if let H2StreamId::Zero = stream_id { + if header.frame_type == FrameType::Headers { + kawa.storage.head = kawa.storage.end; + } else { + kawa.storage.end = kawa.storage.head; + } + } + self.expect_header(); + return self.handle_frame(frame, context, endpoint); + } + (H2State::ContinuationFrame(headers), _) => { + kawa.storage.head = kawa.storage.end; + let i = kawa.storage.data(); + println_!(" data: {:?}", i); + let headers = headers.clone(); + self.expect_header(); + return self.handle_frame(Frame::Headers(headers), context, endpoint); + } + } + MuxResult::Continue + } + + pub fn writable(&mut self, context: &mut Context, mut endpoint: E) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + println_!("======= MUX H2 WRITABLE {:?}", self.position); + self.timeout_container.reset(); + if let Some(H2StreamId::Zero) = self.expect_write { + let kawa = &mut self.zero; + println_!("{:?}", kawa.storage.data()); + while !kawa.storage.is_empty() { + let (size, status) = self.socket.socket_write(kawa.storage.data()); + context.debug.push(DebugEvent::I2(1, size)); + kawa.storage.consume(size); + match self.position { + Position::Client(..) => { + count!("back_bytes_out", size as i64); + } + Position::Server => { + count!("bytes_out", size as i64); + } + } + if update_readiness_after_write(size, status, &mut self.readiness) { + return MuxResult::Continue; + } + } + // when H2StreamId::Zero is used to write READABLE is disabled + // so when we finish the write we enable READABLE again + self.readiness.interest.insert(Ready::READABLE); + self.expect_write = None; + } + match (&self.state, &self.position) { + (H2State::Error, _) + | (H2State::Discard, _) + | (H2State::ClientPreface, Position::Server) + | (H2State::ClientSettings, Position::Server) + | (H2State::ServerSettings, Position::Client(..)) => unreachable!( + "Unexpected combination: (Writable, {:?}, {:?})", + self.state, self.position + ), + (H2State::GoAway, _) => self.force_disconnect(), + (H2State::ClientPreface, Position::Client(..)) => { + println_!("Preparing preface and settings"); + let pri = serializer::H2_PRI.as_bytes(); + let kawa = &mut self.zero; + + kawa.storage.space()[0..pri.len()].copy_from_slice(pri); + kawa.storage.fill(pri.len()); + match serializer::gen_settings(kawa.storage.space(), &self.local_settings) { + Ok((_, size)) => kawa.storage.fill(size), + Err(error) => { + error!("Could not serialize SettingsFrame: {:?}", error); + return self.force_disconnect(); + } + }; + + self.state = H2State::ClientSettings; + self.expect_write = Some(H2StreamId::Zero); + MuxResult::Continue + } + (H2State::ClientSettings, Position::Client(..)) => { + println_!("Sent preface and settings"); + self.state = H2State::ServerSettings; + self.expect_read = Some((H2StreamId::Zero, 9)); + self.readiness.interest.remove(Ready::WRITABLE); + MuxResult::Continue + } + (H2State::ServerSettings, Position::Server) => { + self.expect_header(); + self.readiness.interest.remove(Ready::WRITABLE); + MuxResult::Continue + } + // Proxying states + (H2State::Header, _) + | (H2State::Frame(_), _) + | (H2State::ContinuationFrame(_), _) + | (H2State::ContinuationHeader(_), _) => { + let mut dead_streams = Vec::new(); + + if let Some(write_stream @ H2StreamId::Other(stream_id, global_stream_id)) = + self.expect_write + { + let stream = &mut context.streams[global_stream_id]; + let stream_state = stream.state; + let parts = stream.split(&self.position); + let kawa = parts.wbuffer; + while !kawa.out.is_empty() { + let bufs = kawa.as_io_slice(); + let (size, status) = self.socket.socket_write_vectored(&bufs); + context + .debug + .push(DebugEvent::I3(2, global_stream_id, size)); + kawa.consume(size); + match self.position { + Position::Client(..) => { + count!("back_bytes_out", size as i64); + parts.metrics.backend_bout += size; + } + Position::Server => { + count!("bytes_out", size as i64); + parts.metrics.bout += size; + } + } + if let Some((read_stream, amount)) = self.expect_read { + if write_stream == read_stream + && kawa.storage.available_space() >= amount + { + self.readiness.interest.insert(Ready::READABLE); + } + } + if update_readiness_after_write(size, status, &mut self.readiness) { + return MuxResult::Continue; + } + } + self.expect_write = None; + if (kawa.is_terminated() || kawa.is_error()) && kawa.is_completed() { + match self.position { + Position::Client(..) => {} + Position::Server => { + // mark stream as reusable + context.debug.push(DebugEvent::I2(4, global_stream_id)); + println_!("Recycle stream: {global_stream_id}"); + incr!("http.e2e.h2"); + stream.generate_access_log( + false, + Some(String::from("H2::SplitFrame")), + context.listener.clone(), + ); + let state = + std::mem::replace(&mut stream.state, StreamState::Recycle); + if let StreamState::Linked(token) = state { + endpoint.end_stream(token, global_stream_id, context); + } + dead_streams.push(stream_id); + } + } + } + } + + let mut converter = converter::H2BlockConverter { + max_frame_size: self.peer_settings.settings_max_frame_size as usize, + window: 0, + stream_id: 0, + encoder: &mut self.encoder, + out: Vec::new(), + }; + let mut priorities = self.streams.keys().collect::>(); + priorities.sort(); + + println_!("PRIORITIES: {priorities:?}"); + let mut socket_write = false; + 'outer: for stream_id in priorities { + let global_stream_id = *self.streams.get(stream_id).unwrap(); + let stream = &mut context.streams[global_stream_id]; + let parts = stream.split(&self.position); + let kawa = parts.wbuffer; + if kawa.is_main_phase() || kawa.is_error() { + let window = min(*parts.window, self.window); + converter.window = window; + converter.stream_id = *stream_id; + kawa.prepare(&mut converter); + let consumed = window - converter.window; + *parts.window -= consumed; + self.window -= consumed; + debug_kawa(kawa); + } + context.debug.push(DebugEvent::S( + *stream_id, + global_stream_id, + kawa.parsing_phase, + kawa.blocks.len(), + kawa.out.len(), + )); + while !kawa.out.is_empty() { + socket_write = true; + let bufs = kawa.as_io_slice(); + let (size, status) = self.socket.socket_write_vectored(&bufs); + context + .debug + .push(DebugEvent::I3(3, global_stream_id, size)); + kawa.consume(size); + match self.position { + Position::Client(..) => { + count!("back_bytes_out", size as i64); + parts.metrics.backend_bout += size; + } + Position::Server => { + count!("bytes_out", size as i64); + parts.metrics.bout += size; + } + } + if update_readiness_after_write(size, status, &mut self.readiness) { + self.expect_write = + Some(H2StreamId::Other(*stream_id, global_stream_id)); + break 'outer; + } + } + self.expect_write = None; + if (kawa.is_terminated() || kawa.is_error()) && kawa.is_completed() { + match self.position { + Position::Client(..) => {} + Position::Server => { + // Handle 1xx, this code should probably be merged with the h2 SplitFrame case and h1 nominal case + // to avoid code duplication + + // mark stream as reusable + context.debug.push(DebugEvent::I2(5, global_stream_id)); + if context.debug.is_interesting() { + warn!("{:?}", context.debug.events); + context.debug.set_interesting(false); + } + println_!("Recycle1 stream: {global_stream_id}"); + incr!("http.e2e.h2"); + stream.generate_access_log( + false, + Some(String::from("H2::WholeFrame")), + context.listener.clone(), + ); + let state = + std::mem::replace(&mut stream.state, StreamState::Recycle); + if let StreamState::Linked(token) = state { + endpoint.end_stream(token, global_stream_id, context); + } + dead_streams.push(*stream_id); + } + } + } + } + for stream_id in dead_streams { + self.streams.remove(&stream_id).unwrap(); + } + + if self.socket.socket_wants_write() { + if !socket_write { + self.socket.socket_write(&[]); + } + } else if self.expect_write.is_none() { + // We wrote everything + context.debug.push(DebugEvent::Str(format!( + "Wrote everything: {:?}", + self.streams + ))); + self.readiness.interest.remove(Ready::WRITABLE); + } + MuxResult::Continue + } + } + } + + pub fn goaway(&mut self, error: H2Error) -> MuxResult { + self.state = H2State::Error; + self.expect_read = None; + let kawa = &mut self.zero; + kawa.storage.clear(); + error!("//////////////GOAWAY: {:?}", error); + + match serializer::gen_goaway(kawa.storage.space(), self.last_stream_id, error) { + Ok((_, size)) => { + kawa.storage.fill(size); + self.state = H2State::GoAway; + self.expect_write = Some(H2StreamId::Zero); + self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR; + MuxResult::Continue + } + Err(error) => { + error!("Could not serialize GoAwayFrame: {:?}", error); + self.force_disconnect() + } + } + } + + pub fn create_stream( + &mut self, + stream_id: StreamId, + context: &mut Context, + ) -> Option + where + L: ListenerHandler + L7ListenerHandler, + { + let global_stream_id = context.create_stream( + Ulid::generate(), + self.peer_settings.settings_initial_window_size, + )?; + self.last_stream_id = (stream_id + 2) & !1; + self.streams.insert(stream_id, global_stream_id); + Some(global_stream_id) + } + + pub fn new_stream_id(&mut self) -> StreamId { + self.last_stream_id += 2; + match self.position { + Position::Client(..) => self.last_stream_id - 1, + Position::Server => self.last_stream_id - 2, + } + } + + fn handle_frame( + &mut self, + frame: Frame, + context: &mut Context, + mut endpoint: E, + ) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + println_!("{frame:#?}"); + match frame { + Frame::Data(data) => { + let Some(global_stream_id) = self.streams.get(&data.stream_id).copied() else { + // the stream was terminated while data was expected, + // probably due to automatic answer for invalid/unauthorized access + // ignore this frame + return MuxResult::Continue; + }; + let mut slice = data.payload; + let stream = &mut context.streams[global_stream_id]; + let parts = stream.split(&self.position); + let kawa = parts.rbuffer; + match self.position { + Position::Client(..) => parts.metrics.backend_bin += slice.len(), + Position::Server => parts.metrics.bin += slice.len(), + } + slice.start += kawa.storage.head as u32; + kawa.storage.head += slice.len(); + kawa.push_block(kawa::Block::Chunk(kawa::Chunk { + data: kawa::Store::Slice(slice), + })); + if data.end_stream { + kawa.push_block(kawa::Block::Flags(kawa::Flags { + end_body: true, + end_chunk: false, + end_header: false, + end_stream: true, + })); + kawa.parsing_phase = kawa::ParsingPhase::Terminated; + stream.received_end_of_stream = true; + } + if let StreamState::Linked(token) = stream.state { + endpoint + .readiness_mut(token) + .interest + .insert(Ready::WRITABLE) + } + } + Frame::Headers(headers) => { + if !headers.end_headers { + debug!("FRAGMENT: {:?}", self.zero.storage.data()); + self.state = H2State::ContinuationHeader(headers); + return MuxResult::Continue; + } + // can this fail? + let stream_id = headers.stream_id; + let Some(global_stream_id) = self.streams.get(&stream_id).copied() else { + error!("Handling Headers frame with no attached stream {:#?}", self); + incr!("h2.headers_no_stream.error"); + return self.force_disconnect(); + }; + + if let Some(priority) = &headers.priority { + if self.prioriser.push_priority(stream_id, priority.clone()) { + self.reset_stream( + global_stream_id, + context, + endpoint, + H2Error::ProtocolError, + ); + return MuxResult::Continue; + } + } + + let kawa = &mut self.zero; + let buffer = headers.header_block_fragment.data(kawa.storage.buffer()); + let stream = &mut context.streams[global_stream_id]; + let parts = &mut stream.split(&self.position); + match self.position { + Position::Client(..) => parts.metrics.backend_bin += buffer.len(), + Position::Server => parts.metrics.bin += buffer.len(), + } + let was_initial = parts.rbuffer.is_initial(); + let status = pkawa::handle_header( + &mut self.decoder, + &mut self.prioriser, + stream_id, + parts.rbuffer, + buffer, + headers.end_stream, + parts.context, + ); + kawa.storage.clear(); + if let Err((error, global)) = status { + if global { + error!("GOT GLOBAL ERROR WHILE PROCESSING HEADERS"); + return self.goaway(error); + } else { + return self.reset_stream(global_stream_id, context, endpoint, error); + } + } + debug_kawa(parts.rbuffer); + stream.received_end_of_stream |= headers.end_stream; + if let StreamState::Linked(token) = stream.state { + endpoint + .readiness_mut(token) + .interest + .insert(Ready::WRITABLE) + } + // was_initial prevents trailers from triggering connection + if was_initial && self.position.is_server() { + gauge_add!("http.active_requests", 1); + stream.state = StreamState::Link; + } + } + Frame::PushPromise(_push_promise) => match self.position { + Position::Client(..) => { + if self.local_settings.settings_enable_push { + todo!("forward the push") + } else { + error!("DID NOT ALLOW PUSH"); + return self.goaway(H2Error::ProtocolError); + } + } + Position::Server => { + error!("INVALID PUSH FROM CLIENT"); + return self.goaway(H2Error::ProtocolError); + } + }, + Frame::Priority(priority) => { + if self + .prioriser + .push_priority(priority.stream_id, priority.inner) + { + if let Some(global_stream_id) = self.streams.get(&priority.stream_id) { + return self.reset_stream( + *global_stream_id, + context, + endpoint, + H2Error::ProtocolError, + ); + } else { + error!("INVALID PRIORITY RECEIVED ON INVALID STREAM"); + return self.goaway(H2Error::ProtocolError); + } + } + } + Frame::RstStream(rst_stream) => { + println_!( + "RstStream({} -> {})", + rst_stream.error_code, + error_code_to_str(rst_stream.error_code) + ); + if let Some(stream_id) = self.streams.remove(&rst_stream.stream_id) { + let stream = &mut context.streams[stream_id]; + if let StreamState::Linked(token) = stream.state { + endpoint.end_stream(token, stream_id, context); + } + let stream = &mut context.streams[stream_id]; + match self.position { + Position::Client(..) => {} + Position::Server => { + // This is a special case, normally, all stream are terminated by the server + // when the last byte of the response is written. Here, the reset is requested + // on the server endpoint and immediately terminates, shortcutting the other path + stream.generate_access_log( + true, + Some(String::from("H2::ResetFrame")), + context.listener.clone(), + ); + stream.state = StreamState::Recycle; + } + } + } + } + Frame::Settings(settings) => { + if settings.ack { + return MuxResult::Continue; + } + for setting in settings.settings { + let v = setting.value; + let mut is_error = false; + #[rustfmt::skip] + match setting.identifier { + 1 => { self.peer_settings.settings_header_table_size = v }, + 2 => { self.peer_settings.settings_enable_push = v == 1; is_error |= v > 1 }, + 3 => { self.peer_settings.settings_max_concurrent_streams = v }, + 4 => { is_error |= self.update_initial_window_size(v, context) }, + 5 => { self.peer_settings.settings_max_frame_size = v; is_error |= v >= 1<<24 || v < 1<<14 }, + 6 => { self.peer_settings.settings_max_header_list_size = v }, + 8 => { self.peer_settings.settings_enable_connect_protocol = v == 1; is_error |= v > 1 }, + 9 => { self.peer_settings.settings_no_rfc7540_priorities = v == 1; is_error |= v > 1 }, + other => warn!("Unknown setting_id: {}, we MUST ignore this", other), + }; + if is_error { + error!("INVALID SETTING"); + return self.goaway(H2Error::ProtocolError); + } + } + println_!("{:#?}", self.peer_settings); + + let kawa = &mut self.zero; + kawa.storage.space()[0..serializer::SETTINGS_ACKNOWLEDGEMENT.len()] + .copy_from_slice(&serializer::SETTINGS_ACKNOWLEDGEMENT); + kawa.storage + .fill(serializer::SETTINGS_ACKNOWLEDGEMENT.len()); + + self.readiness.interest.insert(Ready::WRITABLE); + self.readiness.interest.remove(Ready::READABLE); + self.expect_write = Some(H2StreamId::Zero); + } + Frame::Ping(ping) => { + if ping.ack { + return MuxResult::Continue; + } + let kawa = &mut self.zero; + match serializer::gen_ping_acknolegment(kawa.storage.space(), &ping.payload) { + Ok((_, size)) => kawa.storage.fill(size), + Err(error) => { + error!("Could not serialize PingFrame: {:?}", error); + return self.force_disconnect(); + } + }; + self.readiness.interest.insert(Ready::WRITABLE); + self.readiness.interest.remove(Ready::READABLE); + self.expect_write = Some(H2StreamId::Zero); + } + Frame::GoAway(goaway) => { + println_!( + "GoAway({} -> {})", + goaway.error_code, + error_code_to_str(goaway.error_code) + ); + // return self.goaway(H2Error::NoError); + } + Frame::WindowUpdate(WindowUpdate { + stream_id, + increment, + }) => { + let increment = increment as i32; + if stream_id == 0 { + if let Some(window) = self.window.checked_add(increment) { + if self.window <= 0 && window > 0 { + self.readiness.interest.insert(Ready::WRITABLE); + } + self.window = window; + } else { + error!("INVALID WINDOW INCREMENT"); + return self.goaway(H2Error::FlowControlError); + } + } else if let Some(global_stream_id) = self.streams.get(&stream_id) { + let stream = &mut context.streams[*global_stream_id]; + if let Some(window) = stream.window.checked_add(increment) { + if stream.window <= 0 && window > 0 { + self.readiness.interest.insert(Ready::WRITABLE); + } + stream.window = window; + } else { + return self.reset_stream( + *global_stream_id, + context, + endpoint, + H2Error::FlowControlError, + ); + } + } else { + println_!("Ignoring window update on closed stream {stream_id}: {increment}"); + }; + } + Frame::Continuation(_) => unreachable!(), + } + MuxResult::Continue + } + + fn update_initial_window_size(&mut self, value: u32, context: &mut Context) -> bool + where + L: ListenerHandler + L7ListenerHandler, + { + if value >= 1 << 31 { + return true; + } + let delta = value as i32 - self.peer_settings.settings_initial_window_size as i32; + let mut open_window = false; + for stream in context.streams.iter_mut() { + open_window |= stream.window <= 0 && stream.window + delta > 0; + stream.window += delta; + } + println_!( + "UPDATE INIT WINDOW: {delta} {open_window} {:?}", + self.readiness + ); + if open_window { + self.readiness.interest.insert(Ready::WRITABLE); + } + self.peer_settings.settings_initial_window_size = value; + false + } + + pub fn force_disconnect(&mut self) -> MuxResult { + self.state = H2State::Error; + match &mut self.position { + Position::Client(_, _, status) => { + *status = BackendStatus::Disconnecting; + self.readiness.event = Ready::HUP; + MuxResult::Continue + } + Position::Server => MuxResult::CloseSession, + } + } + + pub fn close(&mut self, context: &mut Context, mut endpoint: E) + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + match self.position { + Position::Client(_, _, BackendStatus::KeepAlive) => unreachable!(), + Position::Client(..) => {} + Position::Server => { + println_!("H2 SENDING CLOSE NOTIFY"); + self.socket.socket_close(); + let _ = self.socket.socket_write_vectored(&[]); + return; + } + } + // reconnection is handled by the server for each stream separately + for global_stream_id in self.streams.values() { + println_!("end stream: {global_stream_id}"); + let StreamState::Linked(token) = context.streams[*global_stream_id].state else { + unreachable!() + }; + endpoint.end_stream(token, *global_stream_id, context) + } + } + + pub fn reset_stream( + &mut self, + stream_id: GlobalStreamId, + context: &mut Context, + mut endpoint: E, + error: H2Error, + ) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + let stream = &mut context.streams[stream_id]; + println_!("reset H2 stream {stream_id}: {:#?}", stream.context); + let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked); + forcefully_terminate_answer(stream, &mut self.readiness, error); + if let StreamState::Linked(token) = old_state { + endpoint.end_stream(token, stream_id, context); + } + MuxResult::Continue + } + + pub fn end_stream(&mut self, stream_gid: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + let stream_context = &mut context.streams[stream_gid].context; + println_!("end H2 stream {}: {:#?}", stream_gid, stream_context); + match self.position { + Position::Client(..) => { + for (stream_id, global_stream_id) in &self.streams { + if *global_stream_id == stream_gid { + let id = *stream_id; + // if the stream is not in a closed state we should probably send an + // RST_STREAM frame here. We also need to handle frames coming from + // the backend on this stream after it was closed + self.streams.remove(&id); + return; + } + } + unreachable!() + } + Position::Server => { + let stream = &mut context.streams[stream_gid]; + match (stream.front.consumed, stream.back.is_main_phase()) { + (_, true) => { + // front might not have been consumed (in case of PushPromise) + // we have a "forwardable" answer from the back + // if the answer is not terminated we send an RstStream to properly clean the stream + // if it is terminated, we finish the transfer, the backend is not necessary anymore + if !stream.back.is_terminated() { + context + .debug + .push(DebugEvent::Str(format!("Close unterminated {stream_gid}"))); + warn!("CLOSING H2 UNTERMINATED STREAM {} {:?}", stream_gid, stream); + forcefully_terminate_answer( + stream, + &mut self.readiness, + H2Error::InternalError, + ); + } else { + context + .debug + .push(DebugEvent::Str(format!("Close terminated {stream_gid}"))); + warn!("CLOSING H2 TERMINATED STREAM {} {:?}", stream_gid, stream); + stream.state = StreamState::Unlinked; + self.readiness.interest.insert(Ready::WRITABLE); + } + context.debug.set_interesting(true); + } + (true, false) => { + // we do not have an answer, but the request has already been partially consumed + // so we can't retry, send a 502 bad gateway instead + // note: it might be possible to send a RstStream with an adequate error code + context.debug.push(DebugEvent::Str(format!( + "Can't retry, send 502 on {stream_gid}" + ))); + set_default_answer(stream, &mut self.readiness, 502); + } + (false, false) => { + // we do not have an answer, but the request is untouched so we can retry + debug!("H2 RECONNECT"); + context + .debug + .push(DebugEvent::Str(format!("Retry {stream_gid}"))); + stream.state = StreamState::Link + } + } + } + } + } + + pub fn start_stream(&mut self, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + println_!("start new H2 stream {stream} {:?}", self.readiness); + let stream_id = self.new_stream_id(); + self.streams.insert(stream_id, stream); + self.readiness.interest.insert(Ready::WRITABLE); + } +} diff --git a/lib/src/protocol/mux/mod.rs b/lib/src/protocol/mux/mod.rs new file mode 100644 index 000000000..d4665d0c8 --- /dev/null +++ b/lib/src/protocol/mux/mod.rs @@ -0,0 +1,1815 @@ +use std::{ + cell::RefCell, + collections::HashMap, + fmt::Debug, + io::ErrorKind, + net::{Shutdown, SocketAddr}, + rc::{Rc, Weak}, + time::{Duration, Instant}, +}; + +use kawa::ParsingPhase; +use mio::{net::TcpStream, Interest, Token}; +use rusty_ulid::Ulid; +use sozu_command::{ + logging::EndpointRecord, + proto::command::{Event, EventKind, ListenerType}, + ready::Ready, +}; + +mod converter; +mod h1; +mod h2; +mod parser; +mod pkawa; +mod serializer; + +use crate::{ + backends::{Backend, BackendError}, + http::HttpListener, + https::HttpsListener, + pool::{Checkout, Pool}, + protocol::{ + http::editor::HttpContext, + mux::h2::{H2Settings, H2State, H2StreamId, Prioriser}, + SessionState, + }, + retry::RetryPolicy, + router::Route, + server::{push_event, CONN_RETRIES}, + socket::{FrontRustls, SocketHandler, SocketResult}, + timer::TimeoutContainer, + BackendConnectionError, L7ListenerHandler, L7Proxy, ListenerHandler, Protocol, ProxySession, + Readiness, RetrieveClusterError, SessionIsToBeClosed, SessionMetrics, SessionResult, + StateResult, +}; + +pub use crate::protocol::mux::{ + h1::ConnectionH1, + h2::ConnectionH2, + parser::{error_code_to_str, H2Error}, +}; + +#[macro_export] +macro_rules! println_ { + ($($t:expr),*) => { + // print!("{}:{} ", file!(), line!()); + // println!($($t),*) + $(let _ = &$t;)* + }; +} +fn debug_kawa(_kawa: &GenericHttpStream) { + // kawa::debug_kawa(_kawa); +} + +/// Generic Http representation using the Kawa crate using the Checkout of Sozu as buffer +type GenericHttpStream = kawa::Kawa; +type StreamId = u32; +type GlobalStreamId = usize; +pub type MuxClear = Mux; +pub type MuxTls = Mux; + +pub fn fill_default_301_answer(kawa: &mut kawa::Kawa, host: &str, uri: &str) { + kawa.detached.status_line = kawa::StatusLine::Response { + version: kawa::Version::V20, + code: 301, + status: kawa::Store::Static(b"301"), + reason: kawa::Store::Static(b"Moved Permanently"), + }; + kawa.push_block(kawa::Block::StatusLine); + kawa.push_block(kawa::Block::Header(kawa::Pair { + key: kawa::Store::Static(b"Location"), + val: kawa::Store::from_string(format!("https://{host}{uri}")), + })); + terminate_default_answer(kawa, false); +} + +pub fn fill_default_answer(kawa: &mut kawa::Kawa, code: u16) { + kawa.detached.status_line = kawa::StatusLine::Response { + version: kawa::Version::V20, + code, + status: kawa::Store::from_string(code.to_string()), + reason: kawa::Store::Static(b"Sozu Default Answer"), + }; + kawa.push_block(kawa::Block::StatusLine); + terminate_default_answer(kawa, true); +} + +pub fn terminate_default_answer(kawa: &mut kawa::Kawa, close: bool) { + if close { + kawa.push_block(kawa::Block::Header(kawa::Pair { + key: kawa::Store::Static(b"Cache-Control"), + val: kawa::Store::Static(b"no-cache"), + })); + kawa.push_block(kawa::Block::Header(kawa::Pair { + key: kawa::Store::Static(b"Connection"), + val: kawa::Store::Static(b"close"), + })); + } + kawa.push_block(kawa::Block::Header(kawa::Pair { + key: kawa::Store::Static(b"Content-Length"), + val: kawa::Store::Static(b"0"), + })); + kawa.push_block(kawa::Block::Flags(kawa::Flags { + end_body: false, + end_chunk: false, + end_header: true, + end_stream: true, + })); + kawa.parsing_phase = kawa::ParsingPhase::Terminated; +} + +/// Replace the content of the kawa message with a default Sozu answer for a given status code +fn set_default_answer(stream: &mut Stream, readiness: &mut Readiness, code: u16) { + let context = &mut stream.context; + let kawa = &mut stream.back; + kawa.clear(); + kawa.storage.clear(); + let key = match code { + 301 => "http.301.redirection", + 400 => "http.400.errors", + 401 => "http.401.errors", + 404 => "http.404.errors", + 408 => "http.408.errors", + 413 => "http.413.errors", + 502 => "http.502.errors", + 503 => "http.503.errors", + 504 => "http.504.errors", + 507 => "http.507.errors", + _ => "http.other.errors", + }; + // if context.cluster_id.is_some() { + // incr!(key); + // } + incr!( + key, + context.cluster_id.as_deref(), + context.backend_id.as_deref() + ); + if code == 301 { + let host = context.authority.as_deref().unwrap(); + let uri = context.path.as_deref().unwrap(); + fill_default_301_answer(kawa, host, uri); + } else { + fill_default_answer(kawa, code); + context.keep_alive_frontend = false; + } + context.status = Some(code); + stream.state = StreamState::Unlinked; + readiness.interest.insert(Ready::WRITABLE); +} + +/// Forcefully terminates a kawa message by setting the "end_stream" flag and setting the parsing_phase to Error. +/// An H2 converter will produce an RstStream frame. +fn forcefully_terminate_answer(stream: &mut Stream, readiness: &mut Readiness, error: H2Error) { + let kawa = &mut stream.back; + kawa.out.clear(); + kawa.blocks.clear(); + // kawa.push_block(kawa::Block::Flags(kawa::Flags { + // end_body: false, + // end_chunk: false, + // end_header: false, + // end_stream: true, + // })); + kawa.parsing_phase + .error(error_code_to_str(error as u32).into()); + debug_kawa(kawa); + stream.state = StreamState::Unlinked; + readiness.interest.insert(Ready::WRITABLE); +} + +pub enum Position { + Client(String, Rc>, BackendStatus), + Server, +} + +impl Debug for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Client(cluster_id, _, status) => f + .debug_tuple("Client") + .field(cluster_id) + .field(status) + .finish(), + Self::Server => write!(f, "Server"), + } + } +} + +#[allow(dead_code)] +impl Position { + fn is_server(&self) -> bool { + match self { + Position::Client(..) => false, + Position::Server => true, + } + } + fn is_client(&self) -> bool { + match self { + Position::Client(..) => true, + Position::Server => false, + } + } +} + +#[derive(Debug)] +pub enum BackendStatus { + Connecting(Instant), + Connected, + KeepAlive, + Disconnecting, +} + +#[derive(Debug, Clone, Copy)] +pub enum MuxResult { + Continue, + Upgrade, + CloseSession, +} + +pub trait Endpoint: Debug { + fn readiness(&self, token: Token) -> &Readiness; + fn readiness_mut(&mut self, token: Token) -> &mut Readiness; + /// If end_stream is called on a client it means the stream has PROPERLY finished, + /// the server has completed serving the response and informs the endpoint that this stream won't be used anymore. + /// If end_stream is called on a server it means the stream was BROKEN, the client was most likely disconnected or encountered an error + /// it is for the server to decide if the stream can be retried or an error should be sent. It should be GUARANTEED that all bytes from + /// the backend were read. However it is almost certain that all bytes were not already sent to the client. + fn end_stream( + &mut self, + token: Token, + stream: GlobalStreamId, + context: &mut Context, + ); + /// If start_stream is called on a client it means the stream should be attached to this endpoint, + /// the stream might be recovering from a disconnection, in any case at this point its response MUST be empty. + /// If the start_stream is called on a H2 server it means the stream is a server push and its request MUST be empty. + fn start_stream( + &mut self, + token: Token, + stream: GlobalStreamId, + context: &mut Context, + ); +} + +#[derive(Debug)] +pub enum Connection { + H1(ConnectionH1), + H2(ConnectionH2), +} + +impl Connection { + pub fn new_h1_server( + front_stream: Front, + timeout_container: TimeoutContainer, + ) -> Connection { + Connection::H1(ConnectionH1 { + socket: front_stream, + position: Position::Server, + readiness: Readiness { + interest: Ready::READABLE | Ready::HUP | Ready::ERROR, + event: Ready::EMPTY, + }, + requests: 0, + stream: 0, + timeout_container, + }) + } + pub fn new_h1_client( + front_stream: Front, + cluster_id: String, + backend: Rc>, + timeout_container: TimeoutContainer, + ) -> Connection { + Connection::H1(ConnectionH1 { + socket: front_stream, + position: Position::Client( + cluster_id, + backend, + BackendStatus::Connecting(Instant::now()), + ), + readiness: Readiness { + interest: Ready::WRITABLE | Ready::READABLE | Ready::HUP | Ready::ERROR, + event: Ready::EMPTY, + }, + stream: usize::MAX - 1, + requests: 0, + timeout_container, + }) + } + + pub fn new_h2_server( + front_stream: Front, + pool: Weak>, + timeout_container: TimeoutContainer, + ) -> Option> { + let buffer = pool + .upgrade() + .and_then(|pool| pool.borrow_mut().checkout())?; + Some(Connection::H2(ConnectionH2 { + decoder: hpack::Decoder::new(), + encoder: hpack::Encoder::new(), + expect_read: Some((H2StreamId::Zero, 24 + 9)), + expect_write: None, + last_stream_id: 0, + local_settings: H2Settings::default(), + peer_settings: H2Settings::default(), + position: Position::Server, + prioriser: Prioriser::default(), + readiness: Readiness { + interest: Ready::READABLE | Ready::HUP | Ready::ERROR, + event: Ready::EMPTY, + }, + socket: front_stream, + state: H2State::ClientPreface, + streams: HashMap::new(), + timeout_container, + window: (1 << 16) - 1, + zero: kawa::Kawa::new(kawa::Kind::Request, kawa::Buffer::new(buffer)), + })) + } + pub fn new_h2_client( + front_stream: Front, + cluster_id: String, + backend: Rc>, + pool: Weak>, + timeout_container: TimeoutContainer, + ) -> Option> { + let buffer = pool + .upgrade() + .and_then(|pool| pool.borrow_mut().checkout())?; + Some(Connection::H2(ConnectionH2 { + decoder: hpack::Decoder::new(), + encoder: hpack::Encoder::new(), + expect_read: None, + expect_write: None, + last_stream_id: 0, + local_settings: H2Settings::default(), + peer_settings: H2Settings::default(), + position: Position::Client( + cluster_id, + backend, + BackendStatus::Connecting(Instant::now()), + ), + prioriser: Prioriser::default(), + readiness: Readiness { + interest: Ready::WRITABLE | Ready::HUP | Ready::ERROR, + event: Ready::EMPTY, + }, + socket: front_stream, + state: H2State::ClientPreface, + streams: HashMap::new(), + timeout_container, + window: (1 << 16) - 1, + zero: kawa::Kawa::new(kawa::Kind::Request, kawa::Buffer::new(buffer)), + })) + } + + pub fn readiness(&self) -> &Readiness { + match self { + Connection::H1(c) => &c.readiness, + Connection::H2(c) => &c.readiness, + } + } + pub fn readiness_mut(&mut self) -> &mut Readiness { + match self { + Connection::H1(c) => &mut c.readiness, + Connection::H2(c) => &mut c.readiness, + } + } + pub fn position(&self) -> &Position { + match self { + Connection::H1(c) => &c.position, + Connection::H2(c) => &c.position, + } + } + pub fn position_mut(&mut self) -> &mut Position { + match self { + Connection::H1(c) => &mut c.position, + Connection::H2(c) => &mut c.position, + } + } + pub fn socket(&self) -> &TcpStream { + match self { + Connection::H1(c) => c.socket.socket_ref(), + Connection::H2(c) => c.socket.socket_ref(), + } + } + pub fn socket_mut(&mut self) -> &mut TcpStream { + match self { + Connection::H1(c) => c.socket.socket_mut(), + Connection::H2(c) => c.socket.socket_mut(), + } + } + pub fn timeout_container(&mut self) -> &mut TimeoutContainer { + match self { + Connection::H1(c) => &mut c.timeout_container, + Connection::H2(c) => &mut c.timeout_container, + } + } + fn force_disconnect(&mut self) -> MuxResult { + match self { + Connection::H1(c) => c.force_disconnect(), + Connection::H2(c) => c.force_disconnect(), + } + } + + fn readable(&mut self, context: &mut Context, endpoint: E) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + match self { + Connection::H1(c) => c.readable(context, endpoint), + Connection::H2(c) => c.readable(context, endpoint), + } + } + fn writable(&mut self, context: &mut Context, endpoint: E) -> MuxResult + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + match self { + Connection::H1(c) => c.writable(context, endpoint), + Connection::H2(c) => c.writable(context, endpoint), + } + } + + fn close(&mut self, context: &mut Context, endpoint: E) + where + E: Endpoint, + L: ListenerHandler + L7ListenerHandler, + { + match self.position() { + Position::Client(cluster_id, backend, _) => { + let mut backend_borrow = backend.borrow_mut(); + backend_borrow.dec_connections(); + gauge_add!("backend.connections", -1); + gauge_add!( + "connections_per_backend", + -1, + Some(cluster_id), + Some(&backend_borrow.backend_id) + ); + println_!("--------------- CONNECTION CLOSE: {backend_borrow:#?}"); + } + Position::Server => {} + } + match self { + Connection::H1(c) => c.close(context, endpoint), + Connection::H2(c) => c.close(context, endpoint), + } + } + + fn end_stream(&mut self, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + if let Position::Client(_, backend, BackendStatus::Connected) = self.position() { + let mut backend_borrow = backend.borrow_mut(); + backend_borrow.active_requests = backend_borrow.active_requests.saturating_sub(1); + println_!("--------------- CONNECTION END STREAM: {backend_borrow:#?}"); + } + match self { + Connection::H1(c) => c.end_stream(stream, context), + Connection::H2(c) => c.end_stream(stream, context), + } + } + + fn start_stream(&mut self, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + if let Position::Client(_, backend, BackendStatus::Connected) = self.position() { + let mut backend_borrow = backend.borrow_mut(); + backend_borrow.active_requests += 1; + println_!("--------------- CONNECTION START STREAM: {backend_borrow:#?}"); + } + match self { + Connection::H1(c) => c.start_stream(stream, context), + Connection::H2(c) => c.start_stream(stream, context), + } + } +} + +#[derive(Debug)] +struct EndpointServer<'a, Front: SocketHandler>(&'a mut Connection); +#[derive(Debug)] +struct EndpointClient<'a>(&'a mut Router); + +// note: EndpointServer are used by client Connection, they do not know the frontend Token +// they will use the Stream's Token which is their backend token +impl<'a, Front: SocketHandler + Debug> Endpoint for EndpointServer<'a, Front> { + fn readiness(&self, _token: Token) -> &Readiness { + self.0.readiness() + } + fn readiness_mut(&mut self, _token: Token) -> &mut Readiness { + self.0.readiness_mut() + } + + fn end_stream(&mut self, _token: Token, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + // this may be used to forward H2<->H2 RstStream + // or to handle backend hup + self.0.end_stream(stream, context); + } + + fn start_stream(&mut self, token: Token, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + // this may be used to forward H2<->H2 PushPromise + todo!() + // self.0.start_stream(stream, context); + } +} +impl<'a> Endpoint for EndpointClient<'a> { + fn readiness(&self, token: Token) -> &Readiness { + self.0.backends.get(&token).unwrap().readiness() + } + fn readiness_mut(&mut self, token: Token) -> &mut Readiness { + self.0.backends.get_mut(&token).unwrap().readiness_mut() + } + + fn end_stream(&mut self, token: Token, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + self.0 + .backends + .get_mut(&token) + .unwrap() + .end_stream(stream, context); + } + + fn start_stream(&mut self, token: Token, stream: GlobalStreamId, context: &mut Context) + where + L: ListenerHandler + L7ListenerHandler, + { + self.0 + .backends + .get_mut(&token) + .unwrap() + .start_stream(stream, context); + } +} + +fn update_readiness_after_read( + size: usize, + status: SocketResult, + readiness: &mut Readiness, +) -> bool { + println_!(" size={size}, status={status:?}"); + match status { + SocketResult::Continue => {} + SocketResult::Closed | SocketResult::Error => { + readiness.event.remove(Ready::ALL); + } + SocketResult::WouldBlock => { + readiness.event.remove(Ready::READABLE); + } + } + if size > 0 { + false + } else { + readiness.event.remove(Ready::READABLE); + true + } +} +fn update_readiness_after_write( + size: usize, + status: SocketResult, + readiness: &mut Readiness, +) -> bool { + println_!(" size={size}, status={status:?}"); + match status { + SocketResult::Continue => {} + SocketResult::Closed | SocketResult::Error => { + // even if the socket closed there might be something left to read + readiness.event.remove(Ready::WRITABLE); + } + SocketResult::WouldBlock => { + readiness.event.remove(Ready::WRITABLE); + } + } + if size > 0 { + false + } else { + readiness.event.remove(Ready::WRITABLE); + true + } +} + +// enum Stream { +// Idle { +// window: i32, +// }, +// Open { +// window: i32, +// token: Token, +// front: GenericHttpStream, +// back: GenericHttpStream, +// context: HttpContext, +// }, +// Reserved { +// window: i32, +// token: Token, +// position: Position, +// buffer: GenericHttpStream, +// context: HttpContext, +// }, +// HalfClosed { +// window: i32, +// token: Token, +// position: Position, +// buffer: GenericHttpStream, +// context: HttpContext, +// }, +// Closed, +// } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StreamState { + Idle, + /// the Stream is asking for connection, this will trigger a call to connect + Link, + /// the Stream is linked to a Client (note that the client might not be connected) + Linked(Token), + /// the Stream was linked to a Client, but the connection closed, the client was removed + /// and this Stream could not be retried (it should be terminated) + Unlinked, + /// the Stream is unlinked and can be reused + Recycle, +} + +impl StreamState { + fn is_open(&self) -> bool { + match self { + StreamState::Idle | StreamState::Recycle => false, + _ => true, + } + } +} + +pub struct Stream { + // pub request_id: Ulid, + pub window: i32, + pub attempts: u8, + pub state: StreamState, + pub received_end_of_stream: bool, + pub front: GenericHttpStream, + pub back: GenericHttpStream, + pub context: HttpContext, + pub metrics: SessionMetrics, +} + +struct KawaSummary<'a>(&'a GenericHttpStream); +impl Debug for KawaSummary<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Kawa") + .field("kind", &self.0.kind) + .field("parsing_phase", &self.0.parsing_phase) + .field("body_size", &self.0.body_size) + .field("consumed", &self.0.consumed) + .field("expects", &self.0.expects) + .field("blocks", &self.0.blocks.len()) + .field("out", &self.0.out.len()) + .field("storage_start", &self.0.storage.start) + .field("storage_head", &self.0.storage.head) + .field("storage_end", &self.0.storage.end) + .finish() + } +} +impl Debug for Stream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Stream") + .field("window", &self.window) + .field("attempts", &self.attempts) + .field("state", &self.state) + .field("received_end_of_stream", &self.received_end_of_stream) + .field("front", &KawaSummary(&self.front)) + .field("back", &KawaSummary(&self.back)) + .field("context", &self.context) + .field("metrics", &self.metrics) + .finish() + } +} + +/// This struct allows to mutably borrow the read and write buffers (dependant on the position) +/// as well as the context and metrics of a Stream at the same time +pub struct StreamParts<'a> { + pub window: &'a mut i32, + pub rbuffer: &'a mut GenericHttpStream, + pub wbuffer: &'a mut GenericHttpStream, + pub context: &'a mut HttpContext, + pub metrics: &'a mut SessionMetrics, +} + +impl Stream { + pub fn new(pool: Weak>, context: HttpContext, window: u32) -> Option { + let (front_buffer, back_buffer) = match pool.upgrade() { + Some(pool) => { + let mut pool = pool.borrow_mut(); + match (pool.checkout(), pool.checkout()) { + (Some(front_buffer), Some(back_buffer)) => (front_buffer, back_buffer), + _ => return None, + } + } + None => return None, + }; + Some(Self { + state: StreamState::Idle, + attempts: 0, + window: window as i32, + received_end_of_stream: false, + front: GenericHttpStream::new(kawa::Kind::Request, kawa::Buffer::new(front_buffer)), + back: GenericHttpStream::new(kawa::Kind::Response, kawa::Buffer::new(back_buffer)), + context, + metrics: SessionMetrics::new(None), // FIXME + }) + } + pub fn split(&mut self, position: &Position) -> StreamParts<'_> { + match position { + Position::Client(..) => StreamParts { + window: &mut self.window, + rbuffer: &mut self.back, + wbuffer: &mut self.front, + context: &mut self.context, + metrics: &mut self.metrics, + }, + Position::Server => StreamParts { + window: &mut self.window, + rbuffer: &mut self.front, + wbuffer: &mut self.back, + context: &mut self.context, + metrics: &mut self.metrics, + }, + } + } + pub fn generate_access_log( + &mut self, + error: bool, + message: Option, + listener: Rc>, + ) where + L: ListenerHandler + L7ListenerHandler, + { + let context = &self.context; + gauge_add!("http.active_requests", -1); + let protocol = match context.protocol { + Protocol::HTTP => "http", + Protocol::HTTPS => "https", + _ => unreachable!(), + }; + + // Save the HTTP status code of the backend response + let key = if let Some(status) = context.status { + match status { + 100..=199 => "http.status.1xx", + 200..=299 => "http.status.2xx", + 300..=399 => "http.status.3xx", + 400..=499 => "http.status.4xx", + 500..=599 => "http.status.5xx", + _ => "http.status.other", + } + } else { + "http.status.none" + }; + // if context.cluster_id.is_some() { + // incr!(key); + // } + incr!( + key, + context.cluster_id.as_deref(), + context.backend_id.as_deref() + ); + + let endpoint = EndpointRecord::Http { + method: context.method.as_deref(), + authority: context.authority.as_deref(), + path: context.path.as_deref(), + reason: context.reason.as_deref(), + status: context.status, + }; + + let listener = listener.borrow(); + let tags = context.authority.as_deref().and_then(|host| { + let hostname = match host.split_once(':') { + None => host, + Some((hostname, _)) => hostname, + }; + listener.get_tags(hostname) + }); + + log_access! { + error, + on_failure: { incr!("unsent-access-logs") }, + message: message.as_deref(), + context: context.log_context(), + session_address: context.session_address, + backend_address: context.backend_address, + protocol, + endpoint, + tags, + client_rtt: None, //socket_rtt(self.front_socket()), + server_rtt: None, //self.backend_socket.as_ref().and_then(socket_rtt), + service_time: self.metrics.service_time(), + response_time: self.metrics.backend_response_time(), + request_time: self.metrics.request_time(), + bytes_in: self.metrics.bin, + bytes_out: self.metrics.bout, + user_agent: context.user_agent.as_deref(), + }; + } +} + +// trait DebugHistory { +// fn push(&mut self, event: DebugEvent); +// fn set_interesting(&mut self, val: bool); +// } +pub struct DebugHistory { + pub events: Vec, + pub is_interesting: bool, +} +impl DebugHistory { + pub fn new() -> Self { + Self { + events: Vec::new(), + is_interesting: false, + } + } + pub fn push(&mut self, event: DebugEvent) { + //self.events.push(event); + } + pub fn set_interesting(&mut self, val: bool) { + //self.is_interesting = val; + } + pub fn is_interesting(&mut self) -> bool { + self.is_interesting + } +} + +pub struct Context { + pub streams: Vec, + pub pool: Weak>, + pub listener: Rc>, + pub session_address: Option, + pub public_address: SocketAddr, + pub debug: DebugHistory, +} + +impl Context { + pub fn new( + pool: Weak>, + listener: Rc>, + session_address: Option, + public_address: SocketAddr, + ) -> Self { + Self { + streams: Vec::new(), + pool, + listener, + session_address, + public_address, + debug: DebugHistory::new(), + } + } + + pub fn active_len(&self) -> usize { + self.streams + .iter() + .filter(|s| !matches!(s.state, StreamState::Recycle)) + .count() + } + + pub fn create_stream(&mut self, request_id: Ulid, window: u32) -> Option { + let listener = self.listener.borrow(); + let http_context = HttpContext::new( + request_id, + listener.protocol(), + listener.sticky_name().to_string(), + self.public_address, + self.session_address, + ); + for (stream_id, stream) in self.streams.iter_mut().enumerate() { + if stream.state == StreamState::Recycle { + println_!("Reuse stream: {stream_id}"); + stream.state = StreamState::Idle; + stream.attempts = 0; + stream.received_end_of_stream = false; + stream.window = window as i32; + stream.context = http_context; + stream.back.clear(); + stream.back.storage.clear(); + stream.front.clear(); + stream.front.storage.clear(); + stream.metrics.reset(); + stream.metrics.start = Some(Instant::now()); + return Some(stream_id); + } + } + self.streams + .push(Stream::new(self.pool.clone(), http_context, window)?); + Some(self.streams.len() - 1) + } +} + +#[derive(Debug)] +pub struct Router { + pub backends: HashMap>, + pub configured_backend_timeout: Duration, + pub configured_connect_timeout: Duration, +} + +impl Router { + pub fn new(configured_backend_timeout: Duration, configured_connect_timeout: Duration) -> Self { + Self { + backends: HashMap::new(), + configured_backend_timeout, + configured_connect_timeout, + } + } + + fn connect( + &mut self, + stream_id: GlobalStreamId, + context: &mut Context, + session: Rc>, + proxy: Rc>, + ) -> Result<(), BackendConnectionError> { + let stream = &mut context.streams[stream_id]; + // when reused, a stream should be detached from its old connection, if not we could end + // with concurrent connections on a single endpoint + assert!(matches!(stream.state, StreamState::Link)); + context + .debug + .push(DebugEvent::Str(stream.context.get_route())); + if stream.attempts >= CONN_RETRIES { + return Err(BackendConnectionError::MaxConnectionRetries( + stream.context.cluster_id.clone(), + )); + } + stream.attempts += 1; + + let stream_context = &mut stream.context; + let cluster_id = self + .route_from_request(stream_context, &context.listener) + .map_err(BackendConnectionError::RetrieveClusterError)?; + stream_context.cluster_id = Some(cluster_id.clone()); + + let (frontend_should_stick, frontend_should_redirect_https, h2) = proxy + .borrow() + .clusters() + .get(&cluster_id) + .map(|cluster| { + ( + cluster.sticky_session, + cluster.https_redirect, + cluster.http2, + ) + }) + .unwrap_or((false, false, false)); + + if frontend_should_redirect_https && matches!(proxy.borrow().kind(), ListenerType::Http) { + return Err(BackendConnectionError::RetrieveClusterError( + RetrieveClusterError::HttpsRedirect, + )); + } + + /* + Current h2 connecting strategy: + - look at every backend connection + - reuse the first connected backend that belongs to the same cluster + - or, reuse the last connecting backend that belonds to the same cluster + - if no backend is to reuse, ask the router for a socket to the "next in line" backend for that cluster + + We may want to change to: + - ask the router the name of the "next in line" backend for that cluster + - if we already have a backend connected to this name, reuse it + - if not, create a new socket to it + */ + + let mut reuse_token = None; + // let mut priority = 0; + let mut reuse_connecting = true; + for (token, backend) in &self.backends { + match (h2, reuse_connecting, backend.position()) { + (_, _, Position::Server) => { + unreachable!("Backend connection behaves like a server") + } + (_, _, Position::Client(_, _, BackendStatus::Disconnecting)) => {} + (true, false, Position::Client(_, _, BackendStatus::Connecting(_))) => {} + + (true, _, Position::Client(other_cluster_id, _, BackendStatus::Connected)) => { + if *other_cluster_id == cluster_id { + reuse_token = Some(*token); + reuse_connecting = false; + break; + } + } + ( + true, + true, + Position::Client(other_cluster_id, _, BackendStatus::Connecting(_)), + ) => { + if *other_cluster_id == cluster_id { + reuse_token = Some(*token) + } + } + (true, _, Position::Client(other_cluster_id, _, BackendStatus::KeepAlive)) => { + if *other_cluster_id == cluster_id { + unreachable!("ConnectionH2 behaves like H1") + } + } + + (false, _, Position::Client(old_cluster_id, _, BackendStatus::KeepAlive)) => { + if *old_cluster_id == cluster_id { + reuse_token = Some(*token); + reuse_connecting = false; + break; + } + } + // can't bundle H1 streams together + (false, _, Position::Client(_, _, BackendStatus::Connected)) + | (false, _, Position::Client(_, _, BackendStatus::Connecting(_))) => {} + } + } + println_!("connect: {cluster_id} (stick={frontend_should_stick}, h2={h2}) -> (reuse={reuse_token:?})"); + + let token = if let Some(token) = reuse_token { + println_!("reused backend: {:#?}", self.backends.get(&token).unwrap()); + // stream.metrics.backend_start(); + // stream.metrics.backend_connected(); + token + } else { + let (mut socket, backend) = self.backend_from_request( + &cluster_id, + frontend_should_stick, + stream_context, + proxy.clone(), + &context.listener, + )?; + stream.metrics.backend_start(); + gauge_add!("backend.connections", 1); + gauge_add!( + "connections_per_backend", + 1, + Some(&cluster_id), + Some(&backend.borrow().backend_id) + ); + + if let Err(e) = socket.set_nodelay(true) { + error!( + "error setting nodelay on back socket({:?}): {:?}", + socket, e + ); + } + + let token = proxy.borrow().add_session(session); + + if let Err(e) = proxy.borrow().register_socket( + &mut socket, + token, + Interest::READABLE | Interest::WRITABLE, + ) { + error!("error registering back socket({:?}): {:?}", socket, e); + } + + let timeout_container = TimeoutContainer::new(self.configured_connect_timeout, token); + let connection = if h2 { + match Connection::new_h2_client( + socket, + cluster_id, + backend, + context.pool.clone(), + timeout_container, + ) { + Some(connection) => connection, + None => return Err(BackendConnectionError::MaxBuffers), + } + } else { + Connection::new_h1_client(socket, cluster_id, backend, timeout_container) + }; + self.backends.insert(token, connection); + token + }; + + // link stream to backend + stream.state = StreamState::Linked(token); + // link backend to stream + self.backends + .get_mut(&token) + .unwrap() + .start_stream(stream_id, context); + Ok(()) + } + + fn route_from_request( + &mut self, + context: &mut HttpContext, + listener: &Rc>, + ) -> Result { + let (host, uri, method) = match context.extract_route() { + Ok(tuple) => tuple, + Err(cluster_error) => { + // we are past kawa parsing if it succeeded this can't fail + // if the request was malformed it was caught by kawa and we sent a 400 + error!( + "Malformed request in connect (should be caught at parsing) {:?}", + context + ); + unreachable!("{cluster_error}"); + } + }; + + let route_result = listener.borrow().frontend_from_request(host, uri, method); + + let route = match route_result { + Ok(route) => route, + Err(frontend_error) => { + println_!("{}", frontend_error); + // self.set_answer(DefaultAnswerStatus::Answer404, None); + return Err(RetrieveClusterError::RetrieveFrontend(frontend_error)); + } + }; + + let cluster_id = match route { + Route::Cluster(id) => id, + Route::Deny => { + println_!("Route::Deny"); + // self.set_answer(DefaultAnswerStatus::Answer401, None); + return Err(RetrieveClusterError::UnauthorizedRoute); + } + }; + + Ok(cluster_id) + } + + pub fn backend_from_request( + &mut self, + cluster_id: &str, + frontend_should_stick: bool, + context: &mut HttpContext, + proxy: Rc>, + listener: &Rc>, + ) -> Result<(TcpStream, Rc>), BackendConnectionError> { + let (backend, conn) = self + .get_backend_for_sticky_session( + cluster_id, + frontend_should_stick, + context.sticky_session_found.as_deref(), + proxy, + ) + .map_err(|backend_error| { + println_!("{backend_error}"); + // self.set_answer(DefaultAnswerStatus::Answer503, None); + BackendConnectionError::Backend(backend_error) + })?; + + if frontend_should_stick { + // update sticky name in case it changed I guess? + context.sticky_name = listener.borrow().sticky_name().to_string(); + + context.sticky_session = Some( + backend + .borrow() + .sticky_id + .clone() + .unwrap_or_else(|| backend.borrow().backend_id.clone()), + ); + } + + context.backend_id = Some(backend.borrow().backend_id.clone()); + context.backend_address = Some(backend.borrow().address); + + Ok((conn, backend)) + } + + fn get_backend_for_sticky_session( + &self, + cluster_id: &str, + frontend_should_stick: bool, + sticky_session: Option<&str>, + proxy: Rc>, + ) -> Result<(Rc>, TcpStream), BackendError> { + match (frontend_should_stick, sticky_session) { + (true, Some(sticky_session)) => proxy + .borrow() + .backends() + .borrow_mut() + .backend_from_sticky_session(cluster_id, sticky_session), + _ => proxy + .borrow() + .backends() + .borrow_mut() + .backend_from_cluster_id(cluster_id), + } + } +} + +pub struct Mux { + pub configured_frontend_timeout: Duration, + pub frontend_token: Token, + pub frontend: Connection, + pub router: Router, + pub context: Context, +} + +impl Mux { + pub fn front_socket(&self) -> &TcpStream { + self.frontend.socket() + } +} + +#[derive(Debug)] +pub enum DebugEvent { + EV(Token, Ready), + R(usize), + L1, + L2(i32), + SR(Token, MuxResult, Readiness), + SW(Token, MuxResult, Readiness), + CW(Token, MuxResult, Readiness), + CR(Token, MuxResult, Readiness), + CC(usize, StreamState), + CCS(Token, String), + CCF(usize, BackendConnectionError), + CH(Token, Readiness), + S(u32, usize, ParsingPhase, usize, usize), + Str(String), + I1(usize), + I2(usize, usize), + I3(usize, usize, usize), +} + +impl SessionState + for Mux +{ + fn ready( + &mut self, + session: Rc>, + proxy: Rc>, + _metrics: &mut SessionMetrics, + ) -> SessionResult { + let mut counter = 0; + let max_loop_iterations = 100000; + + if self.frontend.readiness().event.is_hup() { + return SessionResult::Close; + } + + let start = Instant::now(); + self.context.debug.push(DebugEvent::R( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as usize, + )); + println_!("{:?}", start); + loop { + self.context.debug.push(DebugEvent::L1); + loop { + let context = &mut self.context; + context.debug.push(DebugEvent::L2(counter)); + if self.frontend.readiness().filter_interest().is_readable() { + let res = self + .frontend + .readable(context, EndpointClient(&mut self.router)); + context.debug.push(DebugEvent::SR( + self.frontend_token, + res, + self.frontend.readiness().clone(), + )); + match res { + MuxResult::Continue => {} + MuxResult::CloseSession => return SessionResult::Close, + MuxResult::Upgrade => return SessionResult::Upgrade, + } + } + + let mut all_backends_readiness_are_empty = true; + let context = &mut self.context; + let mut dead_backends = Vec::new(); + for (token, client) in self.router.backends.iter_mut() { + let readiness = client.readiness_mut(); + let dead = readiness.filter_interest().is_hup() + || readiness.filter_interest().is_error(); + if dead { + println_!("Backend({token:?}) -> {readiness:?}"); + readiness.event.remove(Ready::WRITABLE); + } + + if client.readiness().filter_interest().is_writable() { + let position = client.position_mut(); + match position { + Position::Client( + cluster_id, + backend, + BackendStatus::Connecting(start), + ) => { + context + .debug + .push(DebugEvent::CCS(*token, cluster_id.clone())); + + let mut backend_borrow = backend.borrow_mut(); + if backend_borrow.retry_policy.is_down() { + info!( + "backend server {} at {} is up", + backend_borrow.backend_id, backend_borrow.address + ); + incr!( + "backend.up", + Some(cluster_id), + Some(&backend_borrow.backend_id) + ); + push_event(Event { + kind: EventKind::BackendUp as i32, + backend_id: Some(backend_borrow.backend_id.to_owned()), + address: Some(backend_borrow.address.into()), + cluster_id: Some(cluster_id.to_owned()), + }); + } + + //successful connection, reset failure counter + backend_borrow.failures = 0; + backend_borrow.set_connection_time(start.elapsed()); + backend_borrow.retry_policy.succeed(); + + for stream in &mut context.streams { + match stream.state { + StreamState::Linked(back_token) if back_token == *token => { + stream.metrics.backend_connected(); + backend_borrow.active_requests += 1; + } + _ => {} + } + } + println_!( + "--------------- CONNECTION SUCCESS: {backend_borrow:#?}" + ); + drop(backend_borrow); + *position = Position::Client( + std::mem::take(cluster_id), + backend.clone(), + BackendStatus::Connected, + ); + client + .timeout_container() + .set_duration(self.router.configured_backend_timeout); + } + Position::Client(..) => {} + Position::Server => unreachable!(), + } + let res = client.writable(context, EndpointServer(&mut self.frontend)); + context + .debug + .push(DebugEvent::CW(*token, res, client.readiness().clone())); + match res { + MuxResult::Continue => {} + MuxResult::Upgrade => unreachable!(), // only frontend can upgrade + MuxResult::CloseSession => return SessionResult::Close, + } + } + + if client.readiness().filter_interest().is_readable() { + let res = client.readable(context, EndpointServer(&mut self.frontend)); + context + .debug + .push(DebugEvent::CR(*token, res, client.readiness().clone())); + match res { + MuxResult::Continue => {} + MuxResult::Upgrade => unreachable!(), // only frontend can upgrade + MuxResult::CloseSession => return SessionResult::Close, + } + } + + if dead && !client.readiness().filter_interest().is_readable() { + context + .debug + .push(DebugEvent::CH(*token, client.readiness().clone())); + println_!("Closing {:#?}", client); + match client.position() { + Position::Client(cluster_id, backend, BackendStatus::Connecting(_)) => { + let mut backend_borrow = backend.borrow_mut(); + backend_borrow.failures += 1; + + let already_unavailable = backend_borrow.retry_policy.is_down(); + backend_borrow.retry_policy.fail(); + incr!( + "backend.connections.error", + Some(cluster_id), + Some(&backend_borrow.backend_id) + ); + if !already_unavailable && backend_borrow.retry_policy.is_down() { + error!( + "backend server {} at {} is down", + backend_borrow.backend_id, backend_borrow.address + ); + incr!( + "backend.down", + Some(cluster_id), + Some(&backend_borrow.backend_id) + ); + push_event(Event { + kind: EventKind::BackendDown as i32, + backend_id: Some(backend_borrow.backend_id.to_owned()), + address: Some(backend_borrow.address.into()), + cluster_id: Some(cluster_id.to_owned()), + }); + } + println_!("--------------- CONNECTION FAIL: {backend_borrow:#?}"); + } + Position::Client(_, backend, _) => { + let mut backend_borrow = backend.borrow_mut(); + for stream in &mut context.streams { + match stream.state { + StreamState::Linked(back_token) if back_token == *token => { + backend_borrow.active_requests = + backend_borrow.active_requests.saturating_sub(1); + } + _ => {} + } + } + } + Position::Server => unreachable!(), + } + client.close(context, EndpointServer(&mut self.frontend)); + dead_backends.push(*token); + } + + if !client.readiness().filter_interest().is_empty() { + all_backends_readiness_are_empty = false; + } + } + if !dead_backends.is_empty() { + for token in &dead_backends { + let proxy_borrow = proxy.borrow(); + if let Some(mut client) = self.router.backends.remove(token) { + client.timeout_container().cancel(); + let socket = client.socket_mut(); + if let Err(e) = proxy_borrow.deregister_socket(socket) { + error!("error deregistering back socket({:?}): {:?}", socket, e); + } + if let Err(e) = socket.shutdown(Shutdown::Both) { + if e.kind() != ErrorKind::NotConnected { + error!( + "error shutting down back socket({:?}): {:?}", + socket, e + ); + } + } + } else { + error!("session {:?} has no backend!", token); + } + if !proxy_borrow.remove_session(*token) { + error!("session {:?} was already removed!", token); + } + } + println_!("FRONTEND: {:#?}", self.frontend); + println_!("BACKENDS: {:#?}", self.router.backends); + } + + let context = &mut self.context; + if self.frontend.readiness().filter_interest().is_writable() { + let res = self + .frontend + .writable(context, EndpointClient(&mut self.router)); + context.debug.push(DebugEvent::SW( + self.frontend_token, + res, + self.frontend.readiness().clone(), + )); + match res { + MuxResult::Continue => {} + MuxResult::CloseSession => return SessionResult::Close, + MuxResult::Upgrade => return SessionResult::Upgrade, + } + } + + if self.frontend.readiness().filter_interest().is_empty() + && all_backends_readiness_are_empty + { + break; + } + + counter += 1; + if counter >= max_loop_iterations { + incr!("http.infinite_loop.error"); + return SessionResult::Close; + } + } + + let context = &mut self.context; + let mut dirty = false; + for stream_id in 0..context.streams.len() { + if context.streams[stream_id].state == StreamState::Link { + // Before the first request triggers a stream Link, the frontend timeout is set + // to a shorter request_timeout, here we switch to the longer nominal timeout + self.frontend + .timeout_container() + .set_duration(self.configured_frontend_timeout); + let front_readiness = self.frontend.readiness_mut(); + dirty = true; + match self + .router + .connect(stream_id, context, session.clone(), proxy.clone()) + { + Ok(_) => { + let state = context.streams[stream_id].state.clone(); + context.debug.push(DebugEvent::CC(stream_id, state)); + } + Err(error) => { + println_!("Connection error: {error}"); + let stream = &mut context.streams[stream_id]; + use BackendConnectionError as BE; + match error { + BE::Backend(BackendError::NoBackendForCluster(_)) + | BE::MaxConnectionRetries(_) + | BE::MaxSessionsMemory + | BE::MaxBuffers => { + set_default_answer(stream, front_readiness, 503); + } + BE::RetrieveClusterError( + RetrieveClusterError::RetrieveFrontend(_), + ) => { + set_default_answer(stream, front_readiness, 404); + } + BE::RetrieveClusterError( + RetrieveClusterError::UnauthorizedRoute, + ) => { + set_default_answer(stream, front_readiness, 401); + } + BE::RetrieveClusterError(RetrieveClusterError::HttpsRedirect) => { + set_default_answer(stream, front_readiness, 301); + } + + BE::Backend(_) => {} + BE::RetrieveClusterError(_) => unreachable!(), + // TCP specific error + BE::NotFound(_) => unreachable!(), + } + context.debug.push(DebugEvent::CCF(stream_id, error)); + } + } + } + } + if !dirty { + break; + } + } + + SessionResult::Continue + } + + fn update_readiness(&mut self, token: Token, events: Ready) { + println_!("EVENTS: {events:?} on {token:?}"); + self.context.debug.push(DebugEvent::EV(token, events)); + if token == self.frontend_token { + self.frontend.readiness_mut().event |= events; + } else if let Some(c) = self.router.backends.get_mut(&token) { + c.readiness_mut().event |= events; + } + } + + fn timeout(&mut self, token: Token, _metrics: &mut SessionMetrics) -> StateResult { + println_!("MuxState::timeout({token:?})"); + let front_is_h2 = match self.frontend { + Connection::H1(_) => false, + Connection::H2(_) => true, + }; + let mut should_close = true; + let mut should_write = false; + if self.frontend_token == token { + println_!("MuxState::timeout_frontend({:#?})", self.frontend); + self.frontend.timeout_container().triggered(); + let front_readiness = self.frontend.readiness_mut(); + for stream in &mut self.context.streams { + match stream.state { + StreamState::Idle => { + // In h1 an Idle stream is always the first request, so we can send a 408 + // In h2 an Idle stream doesn't necessarily hold a request yet, + // in most cases it was just reserved, so we can just ignore them. + if !front_is_h2 { + set_default_answer(stream, front_readiness, 408); + should_write = true; + } + } + StreamState::Link => { + // This is an unusual case, as we have both a complete request and no + // available backend yet. For now, we answer with 503 + set_default_answer(stream, front_readiness, 503); + should_write = true; + } + StreamState::Linked(_) => { + // A stream Linked to a backend is waiting for the response, not the request. + // For streaming or malformed requests, it is possible that the request is not + // terminated at this point. For now, we do nothing + should_close = false; + } + StreamState::Unlinked => { + // A stream Unlinked already has a response and its backend closed. + // In case it hasn't finished proxying we wait. Otherwise it is a stream + // kept alive for a new request, which can be killed. + if !stream.back.is_completed() { + should_close = false; + } + } + StreamState::Recycle => { + // A recycled stream is an h2 stream which doesn't hold a request anymore. + // We can ignore it. + } + } + } + } else if let Some(backend) = self.router.backends.get_mut(&token) { + println_!("MuxState::timeout_backend({:#?})", backend); + backend.timeout_container().triggered(); + let front_readiness = self.frontend.readiness_mut(); + for stream_id in 0..self.context.streams.len() { + let stream = &mut self.context.streams[stream_id]; + if let StreamState::Linked(back_token) = stream.state { + if token == back_token { + // This stream is linked to the backend that timedout + if stream.back.is_terminated() || stream.back.is_error() { + println_!( + "Stream terminated or in error, do nothing, just wait a bit more" + ); + // Nothing to do, simply wait for the remaining bytes to be proxied + if !stream.back.is_completed() { + should_close = false; + } + } else if !stream.back.consumed { + // The response has not started yet + println_!("Stream still waiting for response, send 504"); + set_default_answer(stream, front_readiness, 504); + should_write = true; + } else { + println_!( + "Stream waiting for end of response, forcefully terminate it" + ); + forcefully_terminate_answer( + stream, + front_readiness, + H2Error::InternalError, + ); + should_write = true; + } + backend.end_stream(stream_id, &mut self.context); + // backend.force_disconnect(); + } + } + } + } else { + // Session received a timeout for an unknown token, ignore it + return StateResult::Continue; + } + if should_write { + return match self + .frontend + .writable(&mut self.context, EndpointClient(&mut self.router)) + { + MuxResult::Continue => StateResult::Continue, + MuxResult::Upgrade => StateResult::Upgrade, + MuxResult::CloseSession => StateResult::CloseSession, + }; + } + if should_close { + StateResult::CloseSession + } else { + StateResult::Continue + } + } + + fn cancel_timeouts(&mut self) { + println_!("MuxState::cancel_timeouts"); + self.frontend.timeout_container().cancel(); + for backend in self.router.backends.values_mut() { + backend.timeout_container().cancel(); + } + } + + fn print_state(&self, context: &str) { + error!( + "\ +{} Session(Mux) +\tFrontend: +\t\ttoken: {:?}\treadiness: {:?} +\tBackend(s):", + context, + self.frontend_token, + self.frontend.readiness() + ); + for (backend_token, backend) in &self.router.backends { + error!( + "\t\ttoken: {:?}\treadiness: {:?}", + backend_token, + backend.readiness() + ) + } + } + + fn close(&mut self, proxy: Rc>, _metrics: &mut SessionMetrics) { + if self.context.debug.is_interesting() { + warn!("{:?}", self.context.debug.events); + } + debug!("MUX CLOSE"); + println_!("FRONTEND: {:#?}", self.frontend); + println_!("BACKENDS: {:#?}", self.router.backends); + + self.frontend + .close(&mut self.context, EndpointClient(&mut self.router)); + + for (token, client) in &mut self.router.backends { + let proxy_borrow = proxy.borrow(); + client.timeout_container().cancel(); + let socket = client.socket_mut(); + if let Err(e) = proxy_borrow.deregister_socket(socket) { + error!("error deregistering back socket({:?}): {:?}", socket, e); + } + if let Err(e) = socket.shutdown(Shutdown::Both) { + if e.kind() != ErrorKind::NotConnected { + error!("error shutting down back socket({:?}): {:?}", socket, e); + } + } + if !proxy_borrow.remove_session(*token) { + error!("session {:?} was already removed!", token); + } + + match client.position() { + Position::Client(cluster_id, backend, _) => { + let mut backend_borrow = backend.borrow_mut(); + backend_borrow.dec_connections(); + gauge_add!("backend.connections", -1); + gauge_add!( + "connections_per_backend", + -1, + Some(cluster_id), + Some(&backend_borrow.backend_id) + ); + for stream in &mut self.context.streams { + match stream.state { + StreamState::Linked(back_token) if back_token == *token => { + backend_borrow.active_requests = + backend_borrow.active_requests.saturating_sub(1); + } + _ => {} + } + } + println_!("--------------- CONNECTION(SESSION) CLOSED: {backend_borrow:#?}"); + } + Position::Server => unreachable!(), + } + } + /* + let s = match &mut self.frontend { + Connection::H1(c) => &mut c.socket, + Connection::H2(c) => &mut c.socket, + }; + let mut b = [0; 1024]; + let (size, status) = s.socket_read(&mut b); + println_!("socket: {size} {status:?} {:?}", &b[..size]); + for stream in &mut self.context.streams { + for kawa in [&mut stream.front, &mut stream.back] { + kawa::debug_kawa(kawa); + kawa.prepare(&mut kawa::h1::BlockConverter); + let out = kawa.as_io_slice(); + let mut writer = std::io::BufWriter::new(Vec::new()); + let amount = writer.write_vectored(&out).unwrap(); + println_!( + "amount: {amount}\n{}", + String::from_utf8_lossy(writer.buffer()) + ); + } + } + */ + } + + fn shutting_down(&mut self) -> SessionIsToBeClosed { + let mut can_stop = true; + for stream in &mut self.context.streams { + match stream.state { + StreamState::Linked(_) => { + can_stop = false; + } + StreamState::Unlinked => { + let front = &stream.front; + let back = &stream.back; + kawa::debug_kawa(front); + kawa::debug_kawa(back); + if front.is_initial() + && front.storage.is_empty() + && back.is_initial() + && back.storage.is_empty() + { + continue; + } + stream.context.closing = true; + can_stop = false; + } + _ => {} + } + } + can_stop + } +} diff --git a/lib/src/protocol/mux/parser.rs b/lib/src/protocol/mux/parser.rs new file mode 100644 index 000000000..10a2256fa --- /dev/null +++ b/lib/src/protocol/mux/parser.rs @@ -0,0 +1,680 @@ +use std::convert::From; + +use kawa::repr::Slice; +use nom::{ + bytes::complete::{tag, take}, + combinator::{complete, map}, + error::{ErrorKind, ParseError}, + multi::many0, + number::complete::{be_u16, be_u24, be_u32, be_u8}, + sequence::tuple, + Err, IResult, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct FrameHeader { + pub payload_len: u32, + pub frame_type: FrameType, + pub flags: u8, + pub stream_id: u32, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum FrameType { + Data, + Headers, + Priority, + RstStream, + Settings, + PushPromise, + Ping, + GoAway, + WindowUpdate, + Continuation, +} + +const NO_ERROR: u32 = 0x0; +const PROTOCOL_ERROR: u32 = 0x1; +const INTERNAL_ERROR: u32 = 0x2; +const FLOW_CONTROL_ERROR: u32 = 0x3; +const SETTINGS_TIMEOUT: u32 = 0x4; +const STREAM_CLOSED: u32 = 0x5; +const FRAME_SIZE_ERROR: u32 = 0x6; +const REFUSED_STREAM: u32 = 0x7; +const CANCEL: u32 = 0x8; +const COMPRESSION_ERROR: u32 = 0x9; +const CONNECT_ERROR: u32 = 0xa; +const ENHANCE_YOUR_CALM: u32 = 0xb; +const INADEQUATE_SECURITY: u32 = 0xc; +const HTTP_1_1_REQUIRED: u32 = 0xd; + +pub fn error_code_to_str(error_code: u32) -> &'static str { + match error_code { + NO_ERROR => "NO_ERROR", + PROTOCOL_ERROR => "PROTOCOL_ERROR", + INTERNAL_ERROR => "INTERNAL_ERROR", + FLOW_CONTROL_ERROR => "FLOW_CONTROL_ERROR", + SETTINGS_TIMEOUT => "SETTINGS_TIMEOUT", + STREAM_CLOSED => "STREAM_CLOSED", + FRAME_SIZE_ERROR => "FRAME_SIZE_ERROR", + REFUSED_STREAM => "REFUSED_STREAM", + CANCEL => "CANCEL", + COMPRESSION_ERROR => "COMPRESSION_ERROR", + CONNECT_ERROR => "CONNECT_ERROR", + ENHANCE_YOUR_CALM => "ENHANCE_YOUR_CALM", + INADEQUATE_SECURITY => "INADEQUATE_SECURITY", + HTTP_1_1_REQUIRED => "HTTP_1_1_REQUIRED", + _ => "UNKNOWN_ERROR", + } +} + +pub fn str_to_error_code(str: &str) -> H2Error { + match str { + "NO_ERROR" => H2Error::NoError, + "PROTOCOL_ERROR" => H2Error::ProtocolError, + "INTERNAL_ERROR" => H2Error::InternalError, + "FLOW_CONTROL_ERROR" => H2Error::FlowControlError, + "SETTINGS_TIMEOUT" => H2Error::SettingsTimeout, + "STREAM_CLOSED" => H2Error::StreamClosed, + "FRAME_SIZE_ERROR" => H2Error::FrameSizeError, + "REFUSED_STREAM" => H2Error::RefusedStream, + "CANCEL" => H2Error::Cancel, + "COMPRESSION_ERROR" => H2Error::CompressionError, + "CONNECT_ERROR" => H2Error::ConnectError, + "ENHANCE_YOUR_CALM" => H2Error::EnhanceYourCalm, + "INADEQUATE_SECURITY" => H2Error::InadequateSecurity, + "HTTP_1_1_REQUIRED" => H2Error::HTTP11Required, + _ => H2Error::InternalError, + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ParserError<'a> { + pub input: &'a [u8], + pub kind: ParserErrorKind, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ParserErrorKind { + Nom(ErrorKind), + H2(H2Error), + UnknownFrame(u32), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum H2Error { + NoError, + ProtocolError, + InternalError, + FlowControlError, + SettingsTimeout, + StreamClosed, + FrameSizeError, + RefusedStream, + Cancel, + CompressionError, + ConnectError, + EnhanceYourCalm, + InadequateSecurity, + HTTP11Required, +} + +impl<'a> ParserError<'a> { + pub fn new(input: &'a [u8], error: ParserErrorKind) -> ParserError<'a> { + ParserError { input, kind: error } + } + pub fn new_h2(input: &'a [u8], error: H2Error) -> ParserError<'a> { + ParserError { + input, + kind: ParserErrorKind::H2(error), + } + } +} + +impl<'a> ParseError<&'a [u8]> for ParserError<'a> { + fn from_error_kind(input: &'a [u8], kind: ErrorKind) -> Self { + ParserError { + input, + kind: ParserErrorKind::Nom(kind), + } + } + + fn append(input: &'a [u8], kind: ErrorKind, _other: Self) -> Self { + ParserError { + input, + kind: ParserErrorKind::Nom(kind), + } + } +} + +impl<'a> From<(&'a [u8], ErrorKind)> for ParserError<'a> { + fn from((input, kind): (&'a [u8], ErrorKind)) -> Self { + ParserError { + input, + kind: ParserErrorKind::Nom(kind), + } + } +} + +pub fn preface(i: &[u8]) -> IResult<&[u8], &[u8]> { + tag(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")(i) +} + +// https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 +/*named!(pub frame_header, + do_parse!( + payload_len: dbg_dmp!(be_u24) >> + frame_type: map_opt!(be_u8, convert_frame_type) >> + flags: dbg_dmp!(be_u8) >> + stream_id: dbg_dmp!(verify!(be_u32, |id| { + match frame_type { + + } + }) >> + (FrameHeader { payload_len, frame_type, flags, stream_id }) + ) +); + */ + +pub fn frame_header(input: &[u8], max_frame_size: u32) -> IResult<&[u8], FrameHeader, ParserError> { + let (i, payload_len) = be_u24(input)?; + if payload_len > max_frame_size { + return Err(Err::Failure(ParserError::new_h2( + i, + H2Error::FrameSizeError, + ))); + } + + let (i, t) = be_u8(i)?; + let Some(frame_type) = convert_frame_type(t) else { + return Err(Err::Failure(ParserError::new( + i, + ParserErrorKind::UnknownFrame(payload_len), + ))); + }; + let (i, flags) = be_u8(i)?; + let (i, stream_id) = be_u32(i)?; + let stream_id = stream_id & 0x7FFFFFFF; + + let valid_stream_id = match frame_type { + FrameType::Data + | FrameType::Headers + | FrameType::Priority + | FrameType::RstStream + | FrameType::PushPromise + | FrameType::Continuation => stream_id != 0, + FrameType::Settings | FrameType::Ping | FrameType::GoAway => stream_id == 0, + FrameType::WindowUpdate => true, + }; + if !valid_stream_id { + error!("invalid stream_id: {}", stream_id); + return Err(Err::Failure(ParserError::new_h2(i, H2Error::ProtocolError))); + } + + Ok(( + i, + FrameHeader { + payload_len, + frame_type, + flags, + stream_id, + }, + )) +} + +fn convert_frame_type(t: u8) -> Option { + debug!("got frame type: {}", t); + match t { + 0 => Some(FrameType::Data), + 1 => Some(FrameType::Headers), + 2 => Some(FrameType::Priority), + 3 => Some(FrameType::RstStream), + 4 => Some(FrameType::Settings), + 5 => Some(FrameType::PushPromise), + 6 => Some(FrameType::Ping), + 7 => Some(FrameType::GoAway), + 8 => Some(FrameType::WindowUpdate), + 9 => Some(FrameType::Continuation), + _ => None, + } +} + +#[derive(Clone, Debug)] +pub enum Frame { + Data(Data), + Headers(Headers), + Priority(Priority), + RstStream(RstStream), + Settings(Settings), + PushPromise(PushPromise), + Ping(Ping), + GoAway(GoAway), + WindowUpdate(WindowUpdate), + Continuation(Continuation), +} + +impl Frame { + pub fn is_stream_specific(&self) -> bool { + match self { + Frame::Data(_) + | Frame::Headers(_) + | Frame::Priority(_) + | Frame::RstStream(_) + | Frame::PushPromise(_) + | Frame::Continuation(_) => true, + Frame::Settings(_) | Frame::Ping(_) | Frame::GoAway(_) => false, + Frame::WindowUpdate(w) => w.stream_id != 0, + } + } + + pub fn stream_id(&self) -> u32 { + match self { + Frame::Data(d) => d.stream_id, + Frame::Headers(h) => h.stream_id, + Frame::Priority(p) => p.stream_id, + Frame::RstStream(r) => r.stream_id, + Frame::PushPromise(p) => p.stream_id, + Frame::Continuation(c) => c.stream_id, + Frame::Settings(_) | Frame::Ping(_) | Frame::GoAway(_) => 0, + Frame::WindowUpdate(w) => w.stream_id, + } + } +} + +pub fn frame_body<'a>( + i: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let f = match header.frame_type { + FrameType::Data => data_frame(i, header)?, + FrameType::Headers => headers_frame(i, header)?, + FrameType::Priority => { + if header.payload_len != 5 { + return Err(Err::Failure(ParserError::new_h2( + i, + H2Error::FrameSizeError, + ))); + } + priority_frame(i, header)? + } + FrameType::RstStream => { + if header.payload_len != 4 { + return Err(Err::Failure(ParserError::new_h2( + i, + H2Error::FrameSizeError, + ))); + } + rst_stream_frame(i, header)? + } + FrameType::PushPromise => push_promise_frame(i, header)?, + FrameType::Continuation => continuation_frame(i, header)?, + FrameType::Settings => { + if header.payload_len % 6 != 0 { + return Err(Err::Failure(ParserError::new_h2( + i, + H2Error::FrameSizeError, + ))); + } + settings_frame(i, header)? + } + FrameType::Ping => { + if header.payload_len != 8 { + return Err(Err::Failure(ParserError::new_h2( + i, + H2Error::FrameSizeError, + ))); + } + ping_frame(i, header)? + } + FrameType::GoAway => goaway_frame(i, header)?, + FrameType::WindowUpdate => { + if header.payload_len != 4 { + return Err(Err::Failure(ParserError::new_h2( + i, + H2Error::FrameSizeError, + ))); + } + window_update_frame(i, header)? + } + }; + + Ok(f) +} + +#[derive(Clone, Debug)] +pub struct Data { + pub stream_id: u32, + pub payload: Slice, + pub end_stream: bool, +} + +pub fn data_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (remaining, i) = take(header.payload_len)(input)?; + + let (i, pad_length) = if header.flags & 0x8 != 0 { + let (i, pad_length) = be_u8(i)?; + (i, Some(pad_length)) + } else { + (i, None) + }; + + if pad_length.is_some() && i.len() <= pad_length.unwrap() as usize { + return Err(Err::Failure(ParserError::new_h2( + input, + H2Error::ProtocolError, + ))); + } + + let (_, payload) = take(i.len() - pad_length.unwrap_or(0) as usize)(i)?; + + Ok(( + remaining, + Frame::Data(Data { + stream_id: header.stream_id, + payload: Slice::new(input, payload), + end_stream: header.flags & 0x1 != 0, + }), + )) +} + +#[derive(Clone, Debug)] +pub struct Headers { + pub stream_id: u32, + pub priority: Option, + pub header_block_fragment: Slice, + // pub header_block_fragment: &'a [u8], + pub end_stream: bool, + pub end_headers: bool, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct StreamDependency { + pub exclusive: bool, + pub stream_id: u32, +} + +fn stream_dependency(i: &[u8]) -> IResult<&[u8], StreamDependency, ParserError<'_>> { + let (i, stream) = map(be_u32, |i| StreamDependency { + exclusive: i & 0x8000 != 0, + stream_id: i & 0x7FFFFFFF, + })(i)?; + Ok((i, stream)) +} + +pub fn headers_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (remaining, i) = take(header.payload_len)(input)?; + + let (i, pad_length) = if header.flags & 0x8 != 0 { + let (i, pad_length) = be_u8(i)?; + (i, Some(pad_length)) + } else { + (i, None) + }; + + let (i, priority) = if header.flags & 0x20 != 0 { + let (i, stream_dependency) = stream_dependency(i)?; + let (i, weight) = be_u8(i)?; + ( + i, + Some(PriorityPart::Rfc7540 { + stream_dependency, + weight, + }), + ) + } else { + (i, None) + }; + + if pad_length.is_some() && i.len() <= pad_length.unwrap() as usize { + return Err(Err::Failure(ParserError::new_h2( + input, + H2Error::ProtocolError, + ))); + } + + let (_, header_block_fragment) = take(i.len() - pad_length.unwrap_or(0) as usize)(i)?; + + Ok(( + remaining, + Frame::Headers(Headers { + stream_id: header.stream_id, + priority, + header_block_fragment: Slice::new(input, header_block_fragment), + end_stream: header.flags & 0x1 != 0, + end_headers: header.flags & 0x4 != 0, + }), + )) +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PriorityPart { + Rfc7540 { + stream_dependency: StreamDependency, + weight: u8, + }, + Rfc9218 { + urgency: u8, // should be between 0 and 7 inclusive + incremental: bool, + }, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Priority { + pub stream_id: u32, + pub inner: PriorityPart, +} + +pub fn priority_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (i, stream_dependency) = stream_dependency(input)?; + let (i, weight) = be_u8(i)?; + Ok(( + i, + Frame::Priority(Priority { + stream_id: header.stream_id, + inner: PriorityPart::Rfc7540 { + stream_dependency, + weight, + }, + }), + )) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RstStream { + pub stream_id: u32, + pub error_code: u32, +} + +pub fn rst_stream_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (i, error_code) = be_u32(input)?; + Ok(( + i, + Frame::RstStream(RstStream { + stream_id: header.stream_id, + error_code, + }), + )) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Settings { + pub settings: Vec, + pub ack: bool, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Setting { + pub identifier: u16, + pub value: u32, +} + +pub fn settings_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (i, data) = take(header.payload_len)(input)?; + + let (_, settings) = many0(map( + complete(tuple((be_u16, be_u32))), + |(identifier, value)| Setting { identifier, value }, + ))(data)?; + + Ok(( + i, + Frame::Settings(Settings { + settings, + ack: header.flags & 0x1 != 0, + }), + )) +} + +#[derive(Clone, Debug)] +pub struct PushPromise { + pub stream_id: u32, + pub promised_stream_id: u32, + pub header_block_fragment: Slice, + pub end_headers: bool, +} + +pub fn push_promise_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (remaining, i) = take(header.payload_len)(input)?; + + let (i, pad_length) = if header.flags & 0x8 != 0 { + let (i, pad_length) = be_u8(i)?; + (i, Some(pad_length)) + } else { + (i, None) + }; + + if pad_length.is_some() && i.len() <= pad_length.unwrap() as usize { + return Err(Err::Failure(ParserError::new_h2( + input, + H2Error::ProtocolError, + ))); + } + + let (i, promised_stream_id) = be_u32(i)?; + let (_, header_block_fragment) = take(i.len() - pad_length.unwrap_or(0) as usize)(i)?; + + Ok(( + remaining, + Frame::PushPromise(PushPromise { + stream_id: header.stream_id, + promised_stream_id, + header_block_fragment: Slice::new(input, header_block_fragment), + end_headers: header.flags & 0x4 != 0, + }), + )) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Ping { + pub payload: [u8; 8], + pub ack: bool, +} + +pub fn ping_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (i, data) = take(8usize)(input)?; + + let mut p = Ping { + payload: [0; 8], + ack: header.flags & 1 != 0, + }; + p.payload[..8].copy_from_slice(&data[..8]); + + Ok((i, Frame::Ping(p))) +} + +#[derive(Clone, Debug)] +pub struct GoAway { + pub last_stream_id: u32, + pub error_code: u32, + pub additional_debug_data: Slice, +} + +pub fn goaway_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (remaining, i) = take(header.payload_len)(input)?; + let (i, last_stream_id) = be_u32(i)?; + let (additional_debug_data, error_code) = be_u32(i)?; + Ok(( + remaining, + Frame::GoAway(GoAway { + last_stream_id, + error_code, + additional_debug_data: Slice::new(input, additional_debug_data), + }), + )) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct WindowUpdate { + pub stream_id: u32, + pub increment: u32, +} + +pub fn window_update_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (i, increment) = be_u32(input)?; + let increment = increment & 0x7FFFFFFF; + + //FIXME: if stream id is 0, trat it as connection error? + if increment == 0 { + return Err(Err::Failure(ParserError::new_h2( + input, + H2Error::ProtocolError, + ))); + } + + Ok(( + i, + Frame::WindowUpdate(WindowUpdate { + stream_id: header.stream_id, + increment, + }), + )) +} + +#[derive(Clone, Debug)] +pub struct Continuation { + pub stream_id: u32, + pub header_block_fragment: Slice, + pub end_headers: bool, +} + +pub fn continuation_frame<'a>( + input: &'a [u8], + header: &FrameHeader, +) -> IResult<&'a [u8], Frame, ParserError<'a>> { + let (i, header_block_fragment) = take(header.payload_len)(input)?; + Ok(( + i, + Frame::Continuation(Continuation { + stream_id: header.stream_id, + header_block_fragment: Slice::new(input, header_block_fragment), + end_headers: header.flags & 0x4 != 0, + }), + )) +} diff --git a/lib/src/protocol/mux/pkawa.rs b/lib/src/protocol/mux/pkawa.rs new file mode 100644 index 000000000..4f1dcd570 --- /dev/null +++ b/lib/src/protocol/mux/pkawa.rs @@ -0,0 +1,289 @@ +use std::{io::Write, str::from_utf8}; + +use kawa::{ + h1::ParserCallbacks, repr::Slice, Block, BodySize, Flags, Kind, Pair, ParsingPhase, StatusLine, + Store, Version, +}; + +use crate::{ + pool::Checkout, + protocol::{ + http::parser::compare_no_case, + mux::{ + h2::Prioriser, + parser::{H2Error, PriorityPart}, + GenericHttpStream, StreamId, + }, + }, +}; + +pub fn handle_header( + decoder: &mut hpack::Decoder, + prioriser: &mut Prioriser, + stream_id: StreamId, + kawa: &mut GenericHttpStream, + input: &[u8], + end_stream: bool, + callbacks: &mut C, +) -> Result<(), (H2Error, bool)> +where + C: ParserCallbacks, +{ + if !kawa.is_initial() { + return handle_trailer(kawa, input, end_stream, decoder); + } + kawa.push_block(Block::StatusLine); + // kawa.detached.status_line = match kawa.kind { + // Kind::Request => StatusLine::Request { + // version: Version::V20, + // method: Store::Static(b"GET"), + // uri: Store::Static(b"/"), + // authority: Store::Static(b"lolcatho.st:8443"), + // path: Store::Static(b"/"), + // }, + // Kind::Response => StatusLine::Response { + // version: Version::V20, + // code: 200, + // status: Store::Static(b"200"), + // reason: Store::Static(b"FromH2"), + // }, + // }; + kawa.detached.status_line = match kawa.kind { + Kind::Request => { + let mut method = Store::Empty; + let mut authority = Store::Empty; + let mut path = Store::Empty; + let mut scheme = Store::Empty; + let mut invalid_headers = false; + let mut regular_headers = false; + let decode_status = decoder.decode_with_cb(input, |k, v| { + let start = kawa.storage.end as u32; + kawa.storage.write_all(&v).unwrap(); + let len_key = k.len() as u32; + let len_val = v.len() as u32; + let val = Store::Slice(Slice { + start, + len: len_val, + }); + + if compare_no_case(&k, b":method") { + if !method.is_empty() || regular_headers { + invalid_headers = true; + } + method = val; + } else if compare_no_case(&k, b":scheme") { + if !scheme.is_empty() || regular_headers { + invalid_headers = true; + } + scheme = val; + } else if compare_no_case(&k, b":path") { + if !path.is_empty() || regular_headers { + invalid_headers = true; + } + path = val; + } else if compare_no_case(&k, b":authority") { + if !authority.is_empty() || regular_headers { + invalid_headers = true; + } + authority = val; + } else if k.starts_with(b":") { + invalid_headers = true; + } else if compare_no_case(&k, b"cookie---") { + regular_headers = true; + todo!("cookies should be split in pairs"); + } else { + regular_headers = true; + if compare_no_case(&k, b"content-length") { + if let Some(length) = + from_utf8(&v).ok().and_then(|v| v.parse::().ok()) + { + kawa.body_size = BodySize::Length(length); + } else { + invalid_headers = true; + } + } else if compare_no_case(&k, b"priority") { + // todo!("decode priority"); + prioriser.push_priority( + stream_id, + PriorityPart::Rfc9218 { + urgency: 0, + incremental: false, + }, + ); + } + kawa.storage.write_all(&k).unwrap(); + let key = Store::Slice(Slice { + start: start + len_val, + len: len_key, + }); + kawa.push_block(Block::Header(Pair { key, val })); + } + }); + if let Err(error) = decode_status { + error!("INVALID FRAGMENT: {:?}", error); + return Err((H2Error::CompressionError, true)); + } + if invalid_headers + || method.len() == 0 + || authority.len() == 0 + || path.len() == 0 + || scheme.len() == 0 + { + error!("INVALID HEADERS"); + return Err((H2Error::ProtocolError, false)); + } + // uri is only used by H1 statusline, in most cases it only consists of the path + // a better algorithm should be used though + // let buffer = kawa.storage.data(); + // let uri = unsafe { + // format!( + // "{}://{}{}", + // from_utf8_unchecked(scheme.data(buffer)), + // from_utf8_unchecked(authority.data(buffer)), + // from_utf8_unchecked(path.data(buffer)) + // ) + // }; + // println!("Reconstructed URI: {uri}"); + StatusLine::Request { + version: Version::V20, + method, + uri: path.clone(), //Store::from_string(uri), + authority, + path, + } + } + Kind::Response => { + let mut code = 0; + let mut status = Store::Empty; + let mut invalid_headers = false; + let mut regular_headers = false; + let decode_status = decoder.decode_with_cb(input, |k, v| { + let start = kawa.storage.end as u32; + kawa.storage.write_all(&v).unwrap(); + let len_key = k.len() as u32; + let len_val = v.len() as u32; + let val = Store::Slice(Slice { + start, + len: len_val, + }); + + if compare_no_case(&k, b":status") { + if !status.is_empty() || regular_headers { + invalid_headers = true; + } + status = val; + if let Some(parsed_code) = + from_utf8(&v).ok().and_then(|v| v.parse::().ok()) + { + code = parsed_code; + } else { + invalid_headers = true; + } + } else if k.starts_with(b":") { + invalid_headers = true; + } else { + regular_headers = true; + kawa.storage.write_all(&k).unwrap(); + let key = Store::Slice(Slice { + start: start + len_val, + len: len_key, + }); + kawa.push_block(Block::Header(Pair { key, val })); + } + }); + if let Err(error) = decode_status { + error!("INVALID FRAGMENT: {:?}", error); + return Err((H2Error::CompressionError, true)); + } + if invalid_headers || status.len() == 0 { + error!("INVALID HEADERS"); + return Err((H2Error::ProtocolError, false)); + } + StatusLine::Response { + version: Version::V20, + code, + status, + reason: Store::Static(b"FromH2"), + } + } + }; + + // everything has been parsed + kawa.storage.head = kawa.storage.end; + debug!( + "index: {}/{}/{}", + kawa.storage.start, kawa.storage.head, kawa.storage.end + ); + + callbacks.on_headers(kawa); + + if end_stream { + if let BodySize::Empty = kawa.body_size { + kawa.body_size = BodySize::Length(0); + kawa.push_block(Block::Header(Pair { + key: Store::Static(b"Content-Length"), + val: Store::Static(b"0"), + })); + } + } + + kawa.push_block(Block::Flags(Flags { + end_body: end_stream, + end_chunk: false, + end_header: true, + end_stream, + })); + + if kawa.parsing_phase == ParsingPhase::Terminated { + return Ok(()); + } + + kawa.parsing_phase = match kawa.body_size { + BodySize::Chunked => ParsingPhase::Chunks { first: true }, + BodySize::Length(0) => ParsingPhase::Terminated, + BodySize::Length(_) => ParsingPhase::Body, + BodySize::Empty => ParsingPhase::Chunks { first: true }, + }; + Ok(()) +} + +pub fn handle_trailer( + kawa: &mut GenericHttpStream, + input: &[u8], + end_stream: bool, + decoder: &mut hpack::Decoder, +) -> Result<(), (H2Error, bool)> { + if !end_stream { + return Err((H2Error::ProtocolError, false)); + } + let decode_status = decoder.decode_with_cb(input, |k, v| { + let start = kawa.storage.end as u32; + kawa.storage.write_all(&k).unwrap(); + kawa.storage.write_all(&v).unwrap(); + let len_key = k.len() as u32; + let len_val = v.len() as u32; + let key = Store::Slice(Slice { + start, + len: len_key, + }); + let val = Store::Slice(Slice { + start: start + len_key, + len: len_val, + }); + kawa.push_block(Block::Header(Pair { key, val })); + }); + + if let Err(error) = decode_status { + println!("INVALID FRAGMENT: {error:?}"); + return Err((H2Error::CompressionError, true)); + } + + kawa.push_block(Block::Flags(Flags { + end_body: false, + end_chunk: false, + end_header: true, + end_stream: true, + })); + kawa.parsing_phase = ParsingPhase::Terminated; + Ok(()) +} diff --git a/lib/src/protocol/mux/serializer.rs b/lib/src/protocol/mux/serializer.rs new file mode 100644 index 000000000..28e0643a0 --- /dev/null +++ b/lib/src/protocol/mux/serializer.rs @@ -0,0 +1,143 @@ +use cookie_factory::{ + bytes::{be_u16, be_u24, be_u32, be_u8}, + combinator::slice, + gen, + sequence::tuple, + GenError, +}; + +use crate::protocol::mux::{ + h2::H2Settings, + parser::{FrameHeader, FrameType, H2Error}, +}; + +pub const H2_PRI: &str = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; +pub const SETTINGS_ACKNOWLEDGEMENT: [u8; 9] = [0, 0, 0, 4, 1, 0, 0, 0, 0]; +pub const PING_ACKNOWLEDGEMENT_HEADER: [u8; 9] = [0, 0, 8, 6, 1, 0, 0, 0, 0]; + +pub fn gen_frame_header<'a>( + buf: &'a mut [u8], + frame: &FrameHeader, +) -> Result<(&'a mut [u8], usize), GenError> { + let serializer = tuple(( + be_u24(frame.payload_len), + be_u8(serialize_frame_type(&frame.frame_type)), + be_u8(frame.flags), + be_u32(frame.stream_id), + )); + + gen(serializer, buf).map(|(buf, size)| (buf, size as usize)) +} + +pub fn serialize_frame_type(f: &FrameType) -> u8 { + match *f { + FrameType::Data => 0, + FrameType::Headers => 1, + FrameType::Priority => 2, + FrameType::RstStream => 3, + FrameType::Settings => 4, + FrameType::PushPromise => 5, + FrameType::Ping => 6, + FrameType::GoAway => 7, + FrameType::WindowUpdate => 8, + FrameType::Continuation => 9, + } +} + +// pub fn gen_settings_acknoledgement<'a>(buf: &'a mut [u8]) { +// for (i, b) in SETTINGS_ACKNOWLEDGEMENT.iter().enumerate() { +// buf[i] = *b; +// } +// } + +pub fn gen_ping_acknolegment<'a>( + buf: &'a mut [u8], + payload: &[u8], +) -> Result<(&'a mut [u8], usize), GenError> { + gen( + tuple((slice(PING_ACKNOWLEDGEMENT_HEADER), slice(payload))), + buf, + ) + .map(|(buf, size)| (buf, size as usize)) +} + +pub fn gen_settings<'a>( + buf: &'a mut [u8], + settings: &H2Settings, +) -> Result<(&'a mut [u8], usize), GenError> { + gen_frame_header( + buf, + &FrameHeader { + payload_len: 6 * 8, + frame_type: FrameType::Settings, + flags: 0, + stream_id: 0, + }, + ) + .and_then(|(buf, old_size)| { + gen( + tuple(( + be_u16(1), + be_u32(settings.settings_header_table_size), + be_u16(2), + be_u32(settings.settings_enable_push as u32), + be_u16(3), + be_u32(settings.settings_max_concurrent_streams), + be_u16(4), + be_u32(settings.settings_initial_window_size), + be_u16(5), + be_u32(settings.settings_max_frame_size), + be_u16(6), + be_u32(settings.settings_max_header_list_size), + be_u16(8), + be_u32(settings.settings_enable_connect_protocol as u32), + be_u16(9), + be_u32(settings.settings_no_rfc7540_priorities as u32), + )), + buf, + ) + .map(|(buf, size)| (buf, (old_size + size as usize))) + }) +} + +pub fn gen_rst_stream( + buf: &mut [u8], + stream_id: u32, + error_code: H2Error, +) -> Result<(&mut [u8], usize), GenError> { + gen_frame_header( + buf, + &FrameHeader { + payload_len: 4, + frame_type: FrameType::RstStream, + flags: 0, + stream_id, + }, + ) + .and_then(|(buf, old_size)| { + gen(be_u32(error_code as u32), buf).map(|(buf, size)| (buf, (old_size + size as usize))) + }) +} + +pub fn gen_goaway( + buf: &mut [u8], + last_stream_id: u32, + error_code: H2Error, +) -> Result<(&mut [u8], usize), GenError> { + gen_frame_header( + buf, + &FrameHeader { + payload_len: 8, + frame_type: FrameType::GoAway, + flags: 0, + stream_id: 0, + }, + ) + .and_then(|(buf, old_size)| { + gen( + tuple((be_u32(last_stream_id), be_u32(error_code as u32))), + buf, + ) + .map(|(buf, size)| (buf, (old_size + size as usize))) + }) +} diff --git a/lib/src/protocol/pipe.rs b/lib/src/protocol/pipe.rs index 63d9dffc0..9703069fb 100644 --- a/lib/src/protocol/pipe.rs +++ b/lib/src/protocol/pipe.rs @@ -245,7 +245,7 @@ impl Pipe { backend_address: self.get_backend_address(), protocol: self.protocol_string(), endpoint, - tags: listener.get_tags(&listener.get_addr().to_string()), + tags: listener.get_tags(&listener.address().to_string()), client_rtt: socket_rtt(self.front_socket()), server_rtt: self.backend_socket.as_ref().and_then(socket_rtt), service_time: metrics.service_time(), @@ -658,10 +658,10 @@ impl Pipe { } impl SessionState for Pipe { - fn ready( + fn ready( &mut self, _session: Rc>, - _proxy: Rc>, + _proxy: Rc>, metrics: &mut SessionMetrics, ) -> SessionResult { let mut counter = 0; @@ -791,7 +791,7 @@ impl SessionState for Pipe { self.container_backend_timeout.as_mut().map(|t| t.cancel()); } - fn close(&mut self, _proxy: Rc>, _metrics: &mut SessionMetrics) { + fn close(&mut self, _proxy: Rc>, _metrics: &mut SessionMetrics) { if let Some(backend) = self.backend.as_mut() { let mut backend = backend.borrow_mut(); backend.active_requests = backend.active_requests.saturating_sub(1); diff --git a/lib/src/protocol/proxy_protocol/expect.rs b/lib/src/protocol/proxy_protocol/expect.rs index 031d1cdf9..59386da2b 100644 --- a/lib/src/protocol/proxy_protocol/expect.rs +++ b/lib/src/protocol/proxy_protocol/expect.rs @@ -15,7 +15,7 @@ use crate::{ sozu_command::ready::Ready, tcp::TcpListener, timer::TimeoutContainer, - Protocol, Readiness, SessionMetrics, StateResult, + L7Proxy, Protocol, Readiness, SessionMetrics, StateResult, }; use super::{header::ProxyAddr, parser::parse_v2_header}; @@ -204,13 +204,20 @@ impl ExpectProxyProtocol { backend_id: None, } } + + fn print_state(&self, context: &str) { + error!( + "{} Session(Expect)\n\tFrontend:\n\t\ttoken: {:?}\treadiness: {:?}", + context, self.frontend_token, self.frontend_readiness + ); + } } impl SessionState for ExpectProxyProtocol { - fn ready( + fn ready( &mut self, _session: Rc>, - _proxy: Rc>, + _proxy: Rc>, metrics: &mut SessionMetrics, ) -> SessionResult { let mut counter = 0; @@ -292,10 +299,7 @@ impl SessionState for ExpectProxyProtocol { } fn print_state(&self, context: &str) { - error!( - "{} Session(Expect)\n\tFrontend:\n\t\ttoken: {:?}\treadiness: {:?}", - context, self.frontend_token, self.frontend_readiness - ); + self.print_state(context) } } diff --git a/lib/src/protocol/rustls.rs b/lib/src/protocol/rustls.rs index e84aa8cb5..a581df2b9 100644 --- a/lib/src/protocol/rustls.rs +++ b/lib/src/protocol/rustls.rs @@ -6,7 +6,7 @@ use rusty_ulid::Ulid; use sozu_command::{config::MAX_LOOP_ITERATIONS, logging::LogContext}; use crate::{ - protocol::SessionState, timer::TimeoutContainer, Readiness, Ready, SessionMetrics, + protocol::SessionState, timer::TimeoutContainer, L7Proxy, Readiness, Ready, SessionMetrics, SessionResult, StateResult, }; @@ -223,10 +223,10 @@ impl TlsHandshake { } impl SessionState for TlsHandshake { - fn ready( + fn ready( &mut self, _session: Rc>, - _proxy: Rc>, + _proxy: Rc>, _metrics: &mut SessionMetrics, ) -> SessionResult { let mut counter = 0; diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 5f91baa7c..824813b43 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -132,7 +132,7 @@ impl Router { let method_rule = MethodRule::new(front.method.clone()); let route = match &front.cluster_id { - Some(cluster_id) => Route::ClusterId(cluster_id.clone()), + Some(cluster_id) => Route::Cluster(cluster_id.clone()), None => Route::Deny, }; @@ -160,7 +160,7 @@ impl Router { } }; if !success { - return Err(RouterError::AddRoute(format!("{:?}", front))); + return Err(RouterError::AddRoute(format!("{front:?}"))); } Ok(()) } @@ -195,7 +195,7 @@ impl Router { } }; if !remove_success { - return Err(RouterError::RemoveRoute(format!("{:?}", front))); + return Err(RouterError::RemoveRoute(format!("{front:?}"))); } Ok(()) } @@ -596,7 +596,7 @@ pub enum Route { /// send a 401 default answer Deny, /// the cluster to which the frontend belongs - ClusterId(ClusterId), + Cluster(ClusterId), } #[cfg(test)] @@ -715,27 +715,27 @@ mod tests { b"*.sozu.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("base".to_string()) + &Route::Cluster("base".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(Route::Cluster("base".to_string())) ); assert!(router.add_tree_rule( b"*.sozu.io", &PathRule::Prefix("/api".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("api".to_string()) + &Route::Cluster("api".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/ap", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(Route::Cluster("base".to_string())) ); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("api".to_string())) + Ok(Route::Cluster("api".to_string())) ); } @@ -754,27 +754,27 @@ mod tests { b"*.sozu.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("base".to_string()) + &Route::Cluster("base".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(Route::Cluster("base".to_string())) ); assert!(router.add_tree_rule( b"api.sozu.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("api".to_string()) + &Route::Cluster("api".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(Route::Cluster("base".to_string())) ); assert_eq!( router.lookup("api.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("api".to_string())) + Ok(Route::Cluster("api".to_string())) ); } @@ -786,23 +786,23 @@ mod tests { b"www./.*/.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("base".to_string()) + &Route::Cluster("base".to_string()) )); println!("{:#?}", router.tree); assert!(router.add_tree_rule( b"www.doc./.*/.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("doc".to_string()) + &Route::Cluster("doc".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(Route::Cluster("base".to_string())) ); assert_eq!( router.lookup("www.doc.sozu.io", "/", &Method::Get), - Ok(Route::ClusterId("doc".to_string())) + Ok(Route::Cluster("doc".to_string())) ); assert!(router.remove_tree_rule( b"www./.*/.io", @@ -813,7 +813,7 @@ mod tests { assert!(router.lookup("www.sozu.io", "/", &Method::Get).is_err()); assert_eq!( router.lookup("www.doc.sozu.io", "/", &Method::Get), - Ok(Route::ClusterId("doc".to_string())) + Ok(Route::Cluster("doc".to_string())) ); } @@ -825,30 +825,30 @@ mod tests { &"*".parse::().unwrap(), &PathRule::Prefix("/.well-known/acme-challenge".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("acme".to_string()) + &Route::Cluster("acme".to_string()) )); assert!(router.add_tree_rule( "www.example.com".as_bytes(), &PathRule::Prefix("/".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("example".to_string()) + &Route::Cluster("example".to_string()) )); assert!(router.add_tree_rule( "*.test.example.com".as_bytes(), &PathRule::Regex(Regex::new("/hello[A-Z]+/").unwrap()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("examplewildcard".to_string()) + &Route::Cluster("examplewildcard".to_string()) )); assert!(router.add_tree_rule( "/test[0-9]/.example.com".as_bytes(), &PathRule::Prefix("/".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("exampleregex".to_string()) + &Route::Cluster("exampleregex".to_string()) )); assert_eq!( router.lookup("www.example.com", "/helloA", &Method::new(&b"GET"[..])), - Ok(Route::ClusterId("example".to_string())) + Ok(Route::Cluster("example".to_string())) ); assert_eq!( router.lookup( @@ -856,7 +856,7 @@ mod tests { "/.well-known/acme-challenge", &Method::new(&b"GET"[..]) ), - Ok(Route::ClusterId("acme".to_string())) + Ok(Route::Cluster("acme".to_string())) ); assert!(router .lookup("www.test.example.com", "/", &Method::new(&b"GET"[..])) @@ -867,11 +867,19 @@ mod tests { "/helloAB/", &Method::new(&b"GET"[..]) ), - Ok(Route::ClusterId("examplewildcard".to_string())) + Ok(Route::Cluster("examplewildcard".to_string())) + ); + assert_eq!( + router.lookup( + "www.test.example.com", + "/helloAB/", + &Method::new(&b"GET"[..]) + ), + Ok(Route::Cluster("examplewildcard".to_string())) ); assert_eq!( router.lookup("test1.example.com", "/helloAB/", &Method::new(&b"GET"[..])), - Ok(Route::ClusterId("exampleregex".to_string())) + Ok(Route::Cluster("exampleregex".to_string())) ); } } diff --git a/lib/src/socket.rs b/lib/src/socket.rs index c3014726f..f6ca3cf64 100644 --- a/lib/src/socket.rs +++ b/lib/src/socket.rs @@ -173,6 +173,12 @@ pub struct FrontRustls { pub session: ServerConnection, } +impl std::fmt::Debug for FrontRustls { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("FrontRustls") + } +} + impl SocketHandler for FrontRustls { fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) { let mut size = 0usize; @@ -188,11 +194,36 @@ impl SocketHandler for FrontRustls { incr!("rustls.read.infinite_loop.error"); } + while !self.session.wants_read() { + match self.session.reader().read(&mut buf[size..]) { + Ok(0) => break, + Ok(sz) => { + size += sz; + } + Err(e) => match e.kind() { + ErrorKind::WouldBlock => { + break; + } + ErrorKind::ConnectionReset + | ErrorKind::ConnectionAborted + | ErrorKind::BrokenPipe => { + is_closed = true; + break; + } + _ => { + error!("could not read data from TLS stream: {:?}", e); + is_error = true; + break; + } + }, + } + } + if size == buf.len() { break; } - if !can_read | is_error | is_closed { + if !can_read || is_error || is_closed { break; } @@ -228,31 +259,6 @@ impl SocketHandler for FrontRustls { is_error = true; break; } - - while !self.session.wants_read() { - match self.session.reader().read(&mut buf[size..]) { - Ok(0) => break, - Ok(sz) => { - size += sz; - } - Err(e) => match e.kind() { - ErrorKind::WouldBlock => { - break; - } - ErrorKind::ConnectionReset - | ErrorKind::ConnectionAborted - | ErrorKind::BrokenPipe => { - is_closed = true; - break; - } - _ => { - error!("could not read data from TLS stream: {:?}", e); - is_error = true; - break; - } - }, - } - } } if is_error { diff --git a/lib/src/tcp.rs b/lib/src/tcp.rs index ea6955e86..996d2d34f 100644 --- a/lib/src/tcp.rs +++ b/lib/src/tcp.rs @@ -224,7 +224,7 @@ impl TcpSession { backend_address: None, protocol: "TCP", endpoint: EndpointRecord::Tcp, - tags: listener.get_tags(&listener.get_addr().to_string()), + tags: listener.get_tags(&listener.address().to_string()), client_rtt: socket_rtt(self.state.front_socket()), server_rtt: None, user_agent: None, @@ -1099,10 +1099,22 @@ pub struct TcpListener { } impl ListenerHandler for TcpListener { - fn get_addr(&self) -> &SocketAddr { + fn protocol(&self) -> Protocol { + Protocol::TCP + } + + fn address(&self) -> &SocketAddr { &self.address } + fn public_address(&self) -> SocketAddr { + self.config + .public_address + .as_ref() + .map(|addr| addr.clone().into()) + .unwrap_or(self.address.clone()) + } + fn get_tags(&self, key: &str) -> Option<&CachedTags> { self.tags.get(key) } diff --git a/os-build/archlinux/PKGBUILD b/os-build/archlinux/PKGBUILD index edd268fad..c2fa5f13b 100644 --- a/os-build/archlinux/PKGBUILD +++ b/os-build/archlinux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Jan-Erik Rediger pkgname=sozu-git -pkgver=1.0.6 +pkgver=1.1.0-rc.2 pkgrel=1 pkgdesc="HTTP reverse proxy, configurable at runtime, fast and safe, built in Rust" arch=('i686' 'x86_64') diff --git a/os-build/linux-rpm/sozu.spec b/os-build/linux-rpm/sozu.spec index 69e536498..8d4602eaa 100755 --- a/os-build/linux-rpm/sozu.spec +++ b/os-build/linux-rpm/sozu.spec @@ -6,7 +6,7 @@ Summary: A lightweight, fast, always-up reverse proxy server. Name: sozu -Version: 1.0.6 +Version: 1.1.0-rc.2 Release: 1%{?dist} Epoch: 1 License: AGPL-3.0