From 4bc8068e5fa1dde5193c37546a6d55bd8ce237e9 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 16:21:31 +0800 Subject: [PATCH 01/34] feat(http): add cookie feature for client --- volo-http/src/client/mod.rs | 159 +++++++++++++++++++++++++++++++++- volo-http/src/utils/cookie.rs | 83 +++++++++++++++++- 2 files changed, 237 insertions(+), 5 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index eff1f5f0..33785976 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -2,8 +2,9 @@ //! //! See [`Client`] for more details. -use std::{cell::RefCell, error::Error, sync::Arc, time::Duration}; +use std::{borrow::Cow, cell::RefCell, error::Error, sync::Arc, time::Duration}; +use cookie::Cookie; use faststr::FastStr; use http::{ header::{self, HeaderMap, HeaderName, HeaderValue}, @@ -45,6 +46,7 @@ use crate::{ }, request::ClientRequest, response::ClientResponse, + utils::cookie::CookieJar, }; pub mod callopt; @@ -83,6 +85,7 @@ pub struct ClientBuilder { call_opt: Option, target_parser: TargetParser, headers: HeaderMap, + cookie_jar: Option, inner_layer: IL, outer_layer: OL, mk_client: C, @@ -128,6 +131,7 @@ impl ClientBuilder { call_opt: Default::default(), target_parser: parse_target, headers: Default::default(), + cookie_jar: Default::default(), inner_layer: Identity::new(), outer_layer: Identity::new(), mk_client: DefaultMkClient, @@ -160,6 +164,7 @@ impl ClientBuilder> { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -181,6 +186,7 @@ impl ClientBuilder> { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -205,6 +211,7 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: new_mk_client, @@ -238,6 +245,7 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: Stack::new(layer, self.inner_layer), outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -274,6 +282,7 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: Stack::new(self.inner_layer, layer), outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -307,6 +316,7 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: self.inner_layer, outer_layer: Stack::new(layer, self.outer_layer), mk_client: self.mk_client, @@ -343,6 +353,7 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: self.inner_layer, outer_layer: Stack::new(self.outer_layer, layer), mk_client: self.mk_client, @@ -364,6 +375,7 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, + cookie_jar: self.cookie_jar, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -477,6 +489,58 @@ impl ClientBuilder { Ok(self) } + /// Add single cookie + /// + /// # Example + /// + /// ```rust + /// use volo_http::ClientBuilder; + /// let mut client = ClientBuilder::new(); + /// client.cookie(("foo", "bar")); + /// ``` + pub fn cookie(&mut self, cookie: CK) -> &mut Self + where + CK: Into>, + { + if self.cookie_jar.is_none() { + self.cookie_jar = Some(CookieJar::new()); + } + + let cookie_jar = self.cookie_jar.as_mut().unwrap(); + + cookie_jar.add(cookie.into()); + + self + } + + /// Add cookie with a whole cookie str + /// + /// ```rust + /// use volo_http::ClientBuilder; + /// let client = ClientBuilder::new().cookies("foo=bar; ;foo1=bar1"); + /// ``` + pub fn cookies(&mut self, s: S) -> &mut Self + where + S: Into>, + { + self.cookie_jar = Some(CookieJar::from_cookie_str(s)); + self + } + + /// Set cookie jar + /// + /// ```rust + /// use volo_http::{utils::cookie::CookieJar, ClientBuilder}; + /// + /// let mut cookie_jar = CookieJar::new(); + /// cookie_jar.add(("foo", "bar")); + /// let client = ClientBuilder::new().cookie_jar(cookie_jar); + /// ``` + pub fn cookie_jar(&mut self, cookie_jar: CookieJar) -> &mut Self { + self.cookie_jar = Some(cookie_jar); + self + } + /// Get a reference of [`Target`]. pub fn target_ref(&self) -> &Target { &self.target @@ -675,6 +739,14 @@ impl ClientBuilder { HeaderValue::from_str(caller_name.as_str()).expect("Invalid caller name"), ); } + + // Add cookies + if let Some(cookie_jar) = self.cookie_jar.as_ref() { + if let Some(cookie) = cookie_jar.cookies() { + self.headers.insert(header::COOKIE, cookie); + } + } + let config = Config { timeout: self.builder_config.timeout, fail_on_error_status: self.builder_config.fail_on_error_status, @@ -947,9 +1019,9 @@ where #[cfg(feature = "json")] #[cfg(test)] mod client_tests { - use std::{collections::HashMap, future::Future}; + use cookie::Cookie; use http::{header, StatusCode}; use motore::{ layer::{Layer, Stack}, @@ -964,7 +1036,9 @@ mod client_tests { get, Client, DefaultClient, Target, }; use crate::{ - body::BodyConversion, error::client::status_error, utils::consts::HTTP_DEFAULT_PORT, + body::BodyConversion, + error::client::status_error, + utils::{consts::HTTP_DEFAULT_PORT, cookie::CookieJar}, ClientBuilder, }; @@ -1284,4 +1358,83 @@ mod client_tests { let resp = client.get(HTTPBIN_GET).send().await; assert!(resp.is_ok()); } + + #[tokio::test] + async fn with_cookie() { + let mut builder = Client::builder(); + builder.cookie(("foo", "bar")); + builder.cookie(("foo1", "bar1")); + let client = builder.build(); + + let resp = client + .get(HTTPBIN_GET) + .send() + .await + .unwrap() + .into_json::() + .await + .unwrap(); + + let mut actual: Vec<_> = Cookie::split_parse(resp.headers.get("Cookie").unwrap()) + .filter_map(|parse| parse.ok()) + .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) + .collect(); + // Hence the order is not guaranteed, so we need to sort before compare + actual.sort(); + + assert_eq!(actual, vec![("foo", "bar"), ("foo1", "bar1")]); + } + + #[tokio::test] + async fn with_cookies() { + let mut builder = Client::builder(); + builder.cookies("foo=bar; foo1=bar1"); + let client = builder.build(); + + let resp = client + .get(HTTPBIN_GET) + .send() + .await + .unwrap() + .into_json::() + .await + .unwrap(); + + let mut actual: Vec<_> = Cookie::split_parse(resp.headers.get("Cookie").unwrap()) + .filter_map(|parse| parse.ok()) + .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) + .collect(); + // Hence the order is not guaranteed, so we need to sort before compare + actual.sort(); + + assert_eq!(actual, vec![("foo", "bar"), ("foo1", "bar1")]); + } + + #[tokio::test] + async fn with_cookie_jar() { + let mut cookie_jar = CookieJar::new(); + cookie_jar.add(("foo", "bar")); + cookie_jar.add(("foo1", "bar1")); + let mut builder = Client::builder(); + builder.cookie_jar(cookie_jar); + let client = builder.build(); + + let resp = client + .get(HTTPBIN_GET) + .send() + .await + .unwrap() + .into_json::() + .await + .unwrap(); + + let mut actual: Vec<_> = Cookie::split_parse(resp.headers.get("Cookie").unwrap()) + .filter_map(|parse| parse.ok()) + .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) + .collect(); + // Hence the order is not guaranteed, so we need to sort before compare + actual.sort(); + + assert_eq!(actual, vec![("foo", "bar"), ("foo1", "bar1")]); + } } diff --git a/volo-http/src/utils/cookie.rs b/volo-http/src/utils/cookie.rs index 3fc3f061..9f469547 100644 --- a/volo-http/src/utils/cookie.rs +++ b/volo-http/src/utils/cookie.rs @@ -2,10 +2,15 @@ //! //! [`CookieJar`] currently only supports the server side. -use std::{convert::Infallible, ops::Deref}; +use std::{ + borrow::Cow, + convert::Infallible, + ops::{Deref, DerefMut}, +}; +use bytes::Bytes; pub use cookie::{time::Duration, Cookie}; -use http::{header, request::Parts, HeaderMap}; +use http::{header, request::Parts, HeaderMap, HeaderValue}; use crate::context::ServerContext; #[cfg(feature = "server")] @@ -32,6 +37,74 @@ impl CookieJar { Self { inner: jar } } + + /// Create a [`CookieJar`] from given string + /// + /// # Example + /// + /// ```rust + /// use volo_http::utils::cookie::CookieJar; + /// let cookie_jar = CookieJar::from_cookie_str("foo=bar; ;foo1=bar1"); + /// ``` + #[cfg(feature = "client")] + pub fn from_cookie_str(s: S) -> Self + where + S: Into>, + { + let cookies: Vec = Cookie::split_parse(s) + .filter_map(|parse| parse.ok()) + .collect(); + + let mut jar = cookie::CookieJar::new(); + for cookie in cookies { + jar.add_original(cookie); + } + + Self { inner: jar } + } + + #[cfg(feature = "client")] + /// Create a empty [`CookieJar`] + pub fn new() -> Self { + Self { + inner: cookie::CookieJar::new(), + } + } + + #[cfg(feature = "client")] + /// Add a cookie to the cookie jar + /// + /// # Example + /// + /// ```rust + /// use volo_http::utils::cookie::CookieJar; + /// let mut cookie_jar = CookieJar::new(); + /// cookie_jar.add(("foo", "bar")); + /// cookie_jar.add(("foo1", "bar1")) + /// ``` + pub fn add(&mut self, cookie: C) + where + C: Into>, + { + self.inner.add(cookie); + } + + /// Get [`HeaderValue`] from the cookie jar + #[cfg(feature = "client")] + pub(crate) fn cookies(&self) -> Option { + let s = self + .inner + .iter() + .map(|c| c.to_string()) + .collect::>() + .join("; "); + + if s.is_empty() { + return None; + } + + HeaderValue::from_maybe_shared(Bytes::from(s)).ok() + } } impl Deref for CookieJar { @@ -42,6 +115,12 @@ impl Deref for CookieJar { } } +impl DerefMut for CookieJar { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + #[cfg(feature = "server")] impl FromContext for CookieJar { type Rejection = Infallible; From c4ceb7e4d9a4badd3d560c61f068bde995232cdb Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 16:50:26 +0800 Subject: [PATCH 02/34] feat(http): add cookie feature for client --- volo-http/src/client/mod.rs | 8 ++++---- volo-http/src/utils/cookie.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 33785976..b93893d7 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -508,7 +508,7 @@ impl ClientBuilder { let cookie_jar = self.cookie_jar.as_mut().unwrap(); - cookie_jar.add(cookie.into()); + cookie_jar.add_original(cookie.into()); self } @@ -533,7 +533,7 @@ impl ClientBuilder { /// use volo_http::{utils::cookie::CookieJar, ClientBuilder}; /// /// let mut cookie_jar = CookieJar::new(); - /// cookie_jar.add(("foo", "bar")); + /// cookie_jar.add_original(("foo", "bar")); /// let client = ClientBuilder::new().cookie_jar(cookie_jar); /// ``` pub fn cookie_jar(&mut self, cookie_jar: CookieJar) -> &mut Self { @@ -1413,8 +1413,8 @@ mod client_tests { #[tokio::test] async fn with_cookie_jar() { let mut cookie_jar = CookieJar::new(); - cookie_jar.add(("foo", "bar")); - cookie_jar.add(("foo1", "bar1")); + cookie_jar.add_original(("foo", "bar")); + cookie_jar.add_original(("foo1", "bar1")); let mut builder = Client::builder(); builder.cookie_jar(cookie_jar); let client = builder.build(); diff --git a/volo-http/src/utils/cookie.rs b/volo-http/src/utils/cookie.rs index 9f469547..a305bc88 100644 --- a/volo-http/src/utils/cookie.rs +++ b/volo-http/src/utils/cookie.rs @@ -79,14 +79,14 @@ impl CookieJar { /// ```rust /// use volo_http::utils::cookie::CookieJar; /// let mut cookie_jar = CookieJar::new(); - /// cookie_jar.add(("foo", "bar")); - /// cookie_jar.add(("foo1", "bar1")) + /// cookie_jar.add_original(("foo", "bar")); + /// cookie_jar.add_original(("foo1", "bar1")) /// ``` - pub fn add(&mut self, cookie: C) + pub fn add_original(&mut self, cookie: C) where C: Into>, { - self.inner.add(cookie); + self.inner.add_original(cookie); } /// Get [`HeaderValue`] from the cookie jar From 80d948b422d525719ddde95a9e92ae242e1edc15 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 17:41:24 +0800 Subject: [PATCH 03/34] feat(http): add multipart for server --- volo-http/src/utils/cookie.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/utils/cookie.rs b/volo-http/src/utils/cookie.rs index a305bc88..9753742c 100644 --- a/volo-http/src/utils/cookie.rs +++ b/volo-http/src/utils/cookie.rs @@ -82,7 +82,7 @@ impl CookieJar { /// cookie_jar.add_original(("foo", "bar")); /// cookie_jar.add_original(("foo1", "bar1")) /// ``` - pub fn add_original(&mut self, cookie: C) + pub(crate) fn add_original(&mut self, cookie: C) where C: Into>, { From 6ccaa68eb7754fc5dee4cc48abb3ae45fea81294 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 17:41:36 +0800 Subject: [PATCH 04/34] feat(http): add multipart for server --- volo-http/src/utils/cookie.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/utils/cookie.rs b/volo-http/src/utils/cookie.rs index 9753742c..f283e622 100644 --- a/volo-http/src/utils/cookie.rs +++ b/volo-http/src/utils/cookie.rs @@ -64,7 +64,7 @@ impl CookieJar { } #[cfg(feature = "client")] - /// Create a empty [`CookieJar`] + /// Create an empty [`CookieJar`] pub fn new() -> Self { Self { inner: cookie::CookieJar::new(), From d75e64de01b6d6dea16f181e5a33c531f247f7cd Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 17:48:44 +0800 Subject: [PATCH 05/34] feat(http): add multipart for server --- volo-http/src/utils/cookie.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/utils/cookie.rs b/volo-http/src/utils/cookie.rs index f283e622..2ea9a29a 100644 --- a/volo-http/src/utils/cookie.rs +++ b/volo-http/src/utils/cookie.rs @@ -82,7 +82,7 @@ impl CookieJar { /// cookie_jar.add_original(("foo", "bar")); /// cookie_jar.add_original(("foo1", "bar1")) /// ``` - pub(crate) fn add_original(&mut self, cookie: C) + pub fn add_original(&mut self, cookie: C) where C: Into>, { From 709de2f875122231abf18d8c7fbec6feaeaf6e79 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 15:26:51 +0800 Subject: [PATCH 06/34] feat(http): add cookie for client --- Cargo.lock | 311 ++++++++++++++++++++++++++++ Cargo.toml | 3 + volo-http/Cargo.toml | 5 +- volo-http/src/client/mod.rs | 159 +++----------- volo-http/src/client/transport.rs | 43 +++- volo-http/src/request.rs | 32 +++ volo-http/src/utils/cookie.rs | 134 ------------ volo-http/src/utils/cookie/jar.rs | 55 +++++ volo-http/src/utils/cookie/mod.rs | 11 + volo-http/src/utils/cookie/store.rs | 63 ++++++ 10 files changed, 548 insertions(+), 268 deletions(-) delete mode 100644 volo-http/src/utils/cookie.rs create mode 100644 volo-http/src/utils/cookie/jar.rs create mode 100644 volo-http/src/utils/cookie/mod.rs create mode 100644 volo-http/src/utils/cookie/store.rs diff --git a/Cargo.lock b/Cargo.lock index d074e385..6200cd89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie_store" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +dependencies = [ + "cookie", + "idna 0.5.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -603,6 +620,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "downcast" version = "0.11.0" @@ -1292,6 +1320,134 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -1312,6 +1468,18 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1488,6 +1656,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -2241,6 +2415,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quanta" version = "0.12.3" @@ -2883,6 +3073,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" @@ -2929,6 +3125,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "sysinfo" version = "0.31.4" @@ -3063,6 +3270,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -3534,6 +3751,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3704,6 +3933,7 @@ dependencies = [ "bytes", "chrono", "cookie", + "cookie_store", "faststr", "futures", "futures-util", @@ -3713,6 +3943,7 @@ dependencies = [ "http-body-util", "hyper 1.4.1", "hyper-util", + "idna 1.0.2", "itoa", "libc", "matchit 0.8.4", @@ -3738,6 +3969,7 @@ dependencies = [ "tokio-util", "tracing", "tungstenite", + "url", "volo", ] @@ -4168,6 +4400,42 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -4189,8 +4457,51 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] diff --git a/Cargo.toml b/Cargo.toml index 6886bb77..5ae0652d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ chrono = { version = "0.4", default-features = false, features = [ clap = "4" colored = "2" cookie = "0.18" +cookie_store = "0.21" dashmap = "6" dirs = "5" faststr = { version = "0.2.21", features = ["serde"] } @@ -68,6 +69,7 @@ http-body-util = "0.1" hyper = "1" hyper-timeout = "0.5" hyper-util = "0.1" +idna = "1.0" itertools = "0.13" itoa = "1" libc = "0.2" @@ -121,6 +123,7 @@ tower = "0.5" tracing = "0.1" tracing-subscriber = "0.3" update-informer = "1" +url = "2.5" url_path = "0.1" walkdir = "2" diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 825262ab..2305a272 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -74,6 +74,9 @@ tokio-native-tls = { workspace = true, optional = true } # cookie support cookie = { workspace = true, optional = true, features = ["percent-encode"] } +cookie_store = { workspace = true, optional = true } +idna = { workspace = true, optional = true } +url = { workspace = true, optional = true } # serde and form, query, json serde = { workspace = true, optional = true } @@ -105,7 +108,7 @@ rustls = ["__tls", "dep:tokio-rustls", "volo/rustls"] native-tls = ["__tls", "dep:tokio-native-tls", "volo/native-tls"] native-tls-vendored = ["native-tls", "volo/native-tls-vendored"] -cookie = ["dep:cookie"] +cookie = ["dep:cookie", "dep:cookie_store", "dep:idna", "dep:url"] __serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx` query = ["__serde", "dep:serde_urlencoded"] diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index b93893d7..2e404591 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -2,9 +2,8 @@ //! //! See [`Client`] for more details. -use std::{borrow::Cow, cell::RefCell, error::Error, sync::Arc, time::Duration}; +use std::{cell::RefCell, error::Error, sync::Arc, time::Duration}; -use cookie::Cookie; use faststr::FastStr; use http::{ header::{self, HeaderMap, HeaderName, HeaderValue}, @@ -46,7 +45,7 @@ use crate::{ }, request::ClientRequest, response::ClientResponse, - utils::cookie::CookieJar, + utils::cookie::CookieStore, }; pub mod callopt; @@ -85,7 +84,8 @@ pub struct ClientBuilder { call_opt: Option, target_parser: TargetParser, headers: HeaderMap, - cookie_jar: Option, + #[cfg(feature = "cookie")] + cookie_store: Option, inner_layer: IL, outer_layer: OL, mk_client: C, @@ -131,7 +131,8 @@ impl ClientBuilder { call_opt: Default::default(), target_parser: parse_target, headers: Default::default(), - cookie_jar: Default::default(), + #[cfg(feature = "cookie")] + cookie_store: Default::default(), inner_layer: Identity::new(), outer_layer: Identity::new(), mk_client: DefaultMkClient, @@ -164,7 +165,8 @@ impl ClientBuilder> { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -186,7 +188,8 @@ impl ClientBuilder> { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -211,7 +214,8 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: new_mk_client, @@ -245,7 +249,8 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: Stack::new(layer, self.inner_layer), outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -282,7 +287,8 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: Stack::new(self.inner_layer, layer), outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -316,7 +322,8 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: Stack::new(layer, self.outer_layer), mk_client: self.mk_client, @@ -353,7 +360,8 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: Stack::new(self.outer_layer, layer), mk_client: self.mk_client, @@ -375,7 +383,8 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - cookie_jar: self.cookie_jar, + #[cfg(feature = "cookie")] + cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -489,58 +498,6 @@ impl ClientBuilder { Ok(self) } - /// Add single cookie - /// - /// # Example - /// - /// ```rust - /// use volo_http::ClientBuilder; - /// let mut client = ClientBuilder::new(); - /// client.cookie(("foo", "bar")); - /// ``` - pub fn cookie(&mut self, cookie: CK) -> &mut Self - where - CK: Into>, - { - if self.cookie_jar.is_none() { - self.cookie_jar = Some(CookieJar::new()); - } - - let cookie_jar = self.cookie_jar.as_mut().unwrap(); - - cookie_jar.add_original(cookie.into()); - - self - } - - /// Add cookie with a whole cookie str - /// - /// ```rust - /// use volo_http::ClientBuilder; - /// let client = ClientBuilder::new().cookies("foo=bar; ;foo1=bar1"); - /// ``` - pub fn cookies(&mut self, s: S) -> &mut Self - where - S: Into>, - { - self.cookie_jar = Some(CookieJar::from_cookie_str(s)); - self - } - - /// Set cookie jar - /// - /// ```rust - /// use volo_http::{utils::cookie::CookieJar, ClientBuilder}; - /// - /// let mut cookie_jar = CookieJar::new(); - /// cookie_jar.add_original(("foo", "bar")); - /// let client = ClientBuilder::new().cookie_jar(cookie_jar); - /// ``` - pub fn cookie_jar(&mut self, cookie_jar: CookieJar) -> &mut Self { - self.cookie_jar = Some(cookie_jar); - self - } - /// Get a reference of [`Target`]. pub fn target_ref(&self) -> &Target { &self.target @@ -718,6 +675,8 @@ impl ClientBuilder { self.http_config, transport_config, self.connector, + #[cfg(feature = "cookie")] + self.cookie_store, #[cfg(feature = "__tls")] self.tls_config.unwrap_or_default(), ); @@ -727,6 +686,9 @@ impl ClientBuilder { .make() .layer(self.inner_layer.layer(meta_service)), ); + // TODO: add cookie layer + + // TODO: how to implement redirect layer let caller_name = if self.caller_name.is_empty() { FastStr::from_static_str(PKG_NAME_WITH_VER) @@ -740,13 +702,6 @@ impl ClientBuilder { ); } - // Add cookies - if let Some(cookie_jar) = self.cookie_jar.as_ref() { - if let Some(cookie) = cookie_jar.cookies() { - self.headers.insert(header::COOKIE, cookie); - } - } - let config = Config { timeout: self.builder_config.timeout, fail_on_error_status: self.builder_config.fail_on_error_status, @@ -1036,9 +991,7 @@ mod client_tests { get, Client, DefaultClient, Target, }; use crate::{ - body::BodyConversion, - error::client::status_error, - utils::{consts::HTTP_DEFAULT_PORT, cookie::CookieJar}, + body::BodyConversion, error::client::status_error, utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, }; @@ -1362,61 +1315,9 @@ mod client_tests { #[tokio::test] async fn with_cookie() { let mut builder = Client::builder(); - builder.cookie(("foo", "bar")); - builder.cookie(("foo1", "bar1")); - let client = builder.build(); - - let resp = client - .get(HTTPBIN_GET) - .send() - .await - .unwrap() - .into_json::() - .await - .unwrap(); - - let mut actual: Vec<_> = Cookie::split_parse(resp.headers.get("Cookie").unwrap()) - .filter_map(|parse| parse.ok()) - .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) - .collect(); - // Hence the order is not guaranteed, so we need to sort before compare - actual.sort(); - - assert_eq!(actual, vec![("foo", "bar"), ("foo1", "bar1")]); - } - - #[tokio::test] - async fn with_cookies() { - let mut builder = Client::builder(); - builder.cookies("foo=bar; foo1=bar1"); - let client = builder.build(); - - let resp = client - .get(HTTPBIN_GET) - .send() - .await - .unwrap() - .into_json::() - .await + builder + .header(header::COOKIE.as_str(), "foo=bar; foo1=bar1") .unwrap(); - - let mut actual: Vec<_> = Cookie::split_parse(resp.headers.get("Cookie").unwrap()) - .filter_map(|parse| parse.ok()) - .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) - .collect(); - // Hence the order is not guaranteed, so we need to sort before compare - actual.sort(); - - assert_eq!(actual, vec![("foo", "bar"), ("foo1", "bar1")]); - } - - #[tokio::test] - async fn with_cookie_jar() { - let mut cookie_jar = CookieJar::new(); - cookie_jar.add_original(("foo", "bar")); - cookie_jar.add_original(("foo1", "bar1")); - let mut builder = Client::builder(); - builder.cookie_jar(cookie_jar); let client = builder.build(); let resp = client diff --git a/volo-http/src/client/transport.rs b/volo-http/src/client/transport.rs index a111ba58..df99fad4 100644 --- a/volo-http/src/client/transport.rs +++ b/volo-http/src/client/transport.rs @@ -1,8 +1,9 @@ -use std::error::Error; +use std::{error::Error, str::FromStr, sync::RwLock}; use hyper::client::conn::http1; use hyper_util::rt::TokioIo; use motore::{make::MakeConnection, service::Service}; +use url::Url; #[cfg(feature = "__tls")] use volo::net::tls::Connector; use volo::{ @@ -10,10 +11,12 @@ use volo::{ net::{conn::Conn, dial::DefaultMakeTransport, Address}, }; +#[cfg(feature = "cookie")] +use crate::utils::cookie::CookieStore; use crate::{ context::ClientContext, error::client::{no_address, request_error, ClientError}, - request::ClientRequest, + request::{ClientRequest, RequestPartsExt}, response::ClientResponse, }; @@ -26,11 +29,12 @@ use crate::{ #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "native-tls"))))] pub struct TlsTransport; -#[derive(Clone)] pub struct ClientTransport { client: http1::Builder, mk_conn: DefaultMakeTransport, config: ClientTransportConfig, + #[cfg(feature = "cookie")] + cookie_store: Option>, #[cfg(feature = "__tls")] tls_connector: volo::net::tls::TlsConnector, } @@ -40,6 +44,7 @@ impl ClientTransport { http_config: ClientConfig, transport_config: ClientTransportConfig, mk_conn: DefaultMakeTransport, + #[cfg(feature = "cookie")] cookie_store: Option, #[cfg(feature = "__tls")] tls_connector: volo::net::tls::TlsConnector, ) -> Self { let mut builder = http1::Builder::new(); @@ -54,6 +59,8 @@ impl ClientTransport { client: builder, mk_conn, config: transport_config, + #[cfg(feature = "cookie")] + cookie_store: cookie_store.map(RwLock::new), #[cfg(feature = "__tls")] tls_connector, } @@ -112,13 +119,30 @@ impl ClientTransport { async fn request( &self, cx: &ClientContext, - req: ClientRequest, + mut req: ClientRequest, ) -> Result where B: http_body::Body + Send + 'static, B::Data: Send, B::Error: Into> + 'static, { + let url = req.url().unwrap(); + + #[cfg(feature = "cookie")] + { + let (mut parts, body) = req.into_parts(); + if let Some(cookie_store) = self.cookie_store.as_ref() { + if parts.headers.get(http::header::COOKIE).is_none() { + cookie_store + .read() + .unwrap() + .add_cookie_header(&mut parts.headers, &url); + } + } + + req = ClientRequest::from_parts(parts, body); + } + tracing::trace!("[Volo-HTTP] requesting {}", req.uri()); let conn = self.make_connection(cx).await?; let io = TokioIo::new(conn); @@ -131,6 +155,17 @@ impl ClientTransport { tracing::error!("[Volo-HTTP] failed to send request, error: {err}"); request_error(err) })?; + + #[cfg(feature = "cookie")] + { + if let Some(ref cookie_store) = self.cookie_store { + cookie_store + .write() + .unwrap() + .with_response_headers(resp.headers(), &url); + } + } + Ok(resp.map(crate::body::Body::from_incoming)) } } diff --git a/volo-http/src/request.rs b/volo-http/src/request.rs index 0eccf9b2..9543afa9 100644 --- a/volo-http/src/request.rs +++ b/volo-http/src/request.rs @@ -3,6 +3,8 @@ use http::{ header::{self, HeaderMap, HeaderName}, request::{Parts, Request}, + uri::Scheme, + Uri, }; /// [`Request`] with [`Body`] as default body. @@ -29,11 +31,13 @@ pub const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip"); pub trait RequestPartsExt: sealed::SealedRequestPartsExt { /// Get host name of the request URI from header `Host`. fn host(&self) -> Option<&str>; + fn url(&self) -> Option; } mod sealed { pub trait SealedRequestPartsExt { fn headers(&self) -> &http::header::HeaderMap; + fn uri(&self) -> &http::Uri; } } @@ -41,11 +45,17 @@ impl sealed::SealedRequestPartsExt for Parts { fn headers(&self) -> &HeaderMap { &self.headers } + fn uri(&self) -> &Uri { + &self.uri + } } impl sealed::SealedRequestPartsExt for Request { fn headers(&self) -> &HeaderMap { self.headers() } + fn uri(&self) -> &Uri { + self.uri() + } } impl RequestPartsExt for T @@ -55,4 +65,26 @@ where fn host(&self) -> Option<&str> { simdutf8::basic::from_utf8(self.headers().get(header::HOST)?.as_bytes()).ok() } + + fn url(&self) -> Option { + let host = self.host(); + let uri = self.uri(); + if host.is_none() { + return None; + } + + let mut url_str = String::new(); + + if let Some(scheme) = uri.scheme() { + url_str.push_str(scheme.as_str()); + url_str.push_str("://"); + } else { + url_str.push_str("http://"); + } + + url_str.push_str(host.unwrap()); + url_str.push_str(uri.path()); + + url::Url::parse(url_str.as_str()).ok() + } } diff --git a/volo-http/src/utils/cookie.rs b/volo-http/src/utils/cookie.rs deleted file mode 100644 index 2ea9a29a..00000000 --- a/volo-http/src/utils/cookie.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Cookie utilities of Volo-HTTP. -//! -//! [`CookieJar`] currently only supports the server side. - -use std::{ - borrow::Cow, - convert::Infallible, - ops::{Deref, DerefMut}, -}; - -use bytes::Bytes; -pub use cookie::{time::Duration, Cookie}; -use http::{header, request::Parts, HeaderMap, HeaderValue}; - -use crate::context::ServerContext; -#[cfg(feature = "server")] -use crate::server::extract::FromContext; - -/// A cooke jar that can be extracted from a handler. -pub struct CookieJar { - inner: cookie::CookieJar, -} - -impl CookieJar { - /// Create a [`CookieJar`] from given [`HeaderMap`] - pub fn from_header(headers: &HeaderMap) -> Self { - let mut jar = cookie::CookieJar::new(); - for cookie in headers - .get_all(header::COOKIE) - .into_iter() - .filter_map(|val| val.to_str().ok()) - .flat_map(|val| val.split(';')) - .filter_map(|cookie| Cookie::parse_encoded(cookie.to_owned()).ok()) - { - jar.add_original(cookie); - } - - Self { inner: jar } - } - - /// Create a [`CookieJar`] from given string - /// - /// # Example - /// - /// ```rust - /// use volo_http::utils::cookie::CookieJar; - /// let cookie_jar = CookieJar::from_cookie_str("foo=bar; ;foo1=bar1"); - /// ``` - #[cfg(feature = "client")] - pub fn from_cookie_str(s: S) -> Self - where - S: Into>, - { - let cookies: Vec = Cookie::split_parse(s) - .filter_map(|parse| parse.ok()) - .collect(); - - let mut jar = cookie::CookieJar::new(); - for cookie in cookies { - jar.add_original(cookie); - } - - Self { inner: jar } - } - - #[cfg(feature = "client")] - /// Create an empty [`CookieJar`] - pub fn new() -> Self { - Self { - inner: cookie::CookieJar::new(), - } - } - - #[cfg(feature = "client")] - /// Add a cookie to the cookie jar - /// - /// # Example - /// - /// ```rust - /// use volo_http::utils::cookie::CookieJar; - /// let mut cookie_jar = CookieJar::new(); - /// cookie_jar.add_original(("foo", "bar")); - /// cookie_jar.add_original(("foo1", "bar1")) - /// ``` - pub fn add_original(&mut self, cookie: C) - where - C: Into>, - { - self.inner.add_original(cookie); - } - - /// Get [`HeaderValue`] from the cookie jar - #[cfg(feature = "client")] - pub(crate) fn cookies(&self) -> Option { - let s = self - .inner - .iter() - .map(|c| c.to_string()) - .collect::>() - .join("; "); - - if s.is_empty() { - return None; - } - - HeaderValue::from_maybe_shared(Bytes::from(s)).ok() - } -} - -impl Deref for CookieJar { - type Target = cookie::CookieJar; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for CookieJar { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -#[cfg(feature = "server")] -impl FromContext for CookieJar { - type Rejection = Infallible; - - async fn from_context( - _cx: &mut ServerContext, - parts: &mut Parts, - ) -> Result { - Ok(Self::from_header(&parts.headers)) - } -} diff --git a/volo-http/src/utils/cookie/jar.rs b/volo-http/src/utils/cookie/jar.rs new file mode 100644 index 00000000..2848e5cb --- /dev/null +++ b/volo-http/src/utils/cookie/jar.rs @@ -0,0 +1,55 @@ +//! Cookie utilities of Volo-HTTP. +//! +//! [`CookieJar`] currently only supports the server side. + +use std::{convert::Infallible, ops::Deref}; + +use cookie::Cookie; +use http::{header, request::Parts, HeaderMap}; + +use crate::context::ServerContext; +#[cfg(feature = "server")] +use crate::server::extract::FromContext; + +/// A cooke jar that can be extracted from a handler. +pub struct CookieJar { + inner: cookie::CookieJar, +} + +impl CookieJar { + /// Create a [`CookieJar`] from given [`HeaderMap`] + pub fn from_header(headers: &HeaderMap) -> Self { + let mut jar = cookie::CookieJar::new(); + for cookie in headers + .get_all(header::COOKIE) + .into_iter() + .filter_map(|val| val.to_str().ok()) + .flat_map(|val| val.split(';')) + .filter_map(|cookie| Cookie::parse_encoded(cookie.to_owned()).ok()) + { + jar.add_original(cookie); + } + + Self { inner: jar } + } +} + +impl Deref for CookieJar { + type Target = cookie::CookieJar; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[cfg(feature = "server")] +impl FromContext for CookieJar { + type Rejection = Infallible; + + async fn from_context( + _cx: &mut ServerContext, + parts: &mut Parts, + ) -> Result { + Ok(Self::from_header(&parts.headers)) + } +} diff --git a/volo-http/src/utils/cookie/mod.rs b/volo-http/src/utils/cookie/mod.rs new file mode 100644 index 00000000..7bcbb629 --- /dev/null +++ b/volo-http/src/utils/cookie/mod.rs @@ -0,0 +1,11 @@ +//! Cookie utilities of Volo-HTTP. +//! +//! [`CookieJar`] currently only supports the server side. + +mod jar; +mod store; + +pub use cookie::{time::Duration, Cookie}; +pub use jar::CookieJar; +#[cfg(feature = "client")] +pub(crate) use store::CookieStore; diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs new file mode 100644 index 00000000..3c239587 --- /dev/null +++ b/volo-http/src/utils/cookie/store.rs @@ -0,0 +1,63 @@ +use std::ops::{Deref, DerefMut}; + +use bytes::Bytes; +use cookie::Cookie; +use http::{header, HeaderMap, HeaderValue}; + +/// A cooke jar that can be extracted from a handler. +#[derive(Default)] +pub(crate) struct CookieStore { + inner: cookie_store::CookieStore, +} + +impl CookieStore { + pub fn add_cookie_header(&self, headers: &mut HeaderMap, request_url: &url::Url) { + if let Some(header_value) = self.cookies(request_url) { + headers.insert(header::COOKIE, header_value); + } + } + + pub fn with_response_headers(&mut self, headers: &HeaderMap, request_url: &url::Url) { + let mut set_cookie_headers = headers.get_all(header::SET_COOKIE).iter().peekable(); + + if set_cookie_headers.peek().is_some() { + let cookie_iter = set_cookie_headers.filter_map(|val| { + std::str::from_utf8(val.as_bytes()) + .ok() + .and_then(|val| Cookie::parse(val).map(|c| c.into_owned()).ok()) + }); + self.inner.store_response_cookies(cookie_iter, request_url); + } + } + + /// Get [`HeaderValue`] from the cookie jar + #[cfg(feature = "client")] + pub(crate) fn cookies(&self, request_url: &url::Url) -> Option { + let s = self + .inner + .get_request_values(request_url) + .map(|(name, value)| format!("{name}={value}")) + .collect::>() + .join("; "); + + if s.is_empty() { + return None; + } + + HeaderValue::from_maybe_shared(Bytes::from(s)).ok() + } +} + +impl Deref for CookieStore { + type Target = cookie_store::CookieStore; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for CookieStore { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} From 66ff8d137a6420eba0c5472509f87320cc139be4 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 15:38:11 +0800 Subject: [PATCH 07/34] feat(http): add cookie for client --- volo-http/src/client/transport.rs | 28 ++++++++++++++++------------ volo-http/src/request.rs | 9 +++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/volo-http/src/client/transport.rs b/volo-http/src/client/transport.rs index df99fad4..d5bc41c1 100644 --- a/volo-http/src/client/transport.rs +++ b/volo-http/src/client/transport.rs @@ -1,9 +1,8 @@ -use std::{error::Error, str::FromStr, sync::RwLock}; +use std::{error::Error, sync::RwLock}; use hyper::client::conn::http1; use hyper_util::rt::TokioIo; use motore::{make::MakeConnection, service::Service}; -use url::Url; #[cfg(feature = "__tls")] use volo::net::tls::Connector; use volo::{ @@ -126,17 +125,20 @@ impl ClientTransport { B::Data: Send, B::Error: Into> + 'static, { - let url = req.url().unwrap(); + #[cfg(feature = "cookie")] + let url = req.url(); #[cfg(feature = "cookie")] { let (mut parts, body) = req.into_parts(); if let Some(cookie_store) = self.cookie_store.as_ref() { - if parts.headers.get(http::header::COOKIE).is_none() { - cookie_store - .read() - .unwrap() - .add_cookie_header(&mut parts.headers, &url); + if let Some(url) = &url { + if parts.headers.get(http::header::COOKIE).is_none() { + cookie_store + .read() + .unwrap() + .add_cookie_header(&mut parts.headers, url); + } } } @@ -159,10 +161,12 @@ impl ClientTransport { #[cfg(feature = "cookie")] { if let Some(ref cookie_store) = self.cookie_store { - cookie_store - .write() - .unwrap() - .with_response_headers(resp.headers(), &url); + if let Some(url) = &url { + cookie_store + .write() + .unwrap() + .with_response_headers(resp.headers(), url); + } } } diff --git a/volo-http/src/request.rs b/volo-http/src/request.rs index 9543afa9..14090a63 100644 --- a/volo-http/src/request.rs +++ b/volo-http/src/request.rs @@ -3,7 +3,6 @@ use http::{ header::{self, HeaderMap, HeaderName}, request::{Parts, Request}, - uri::Scheme, Uri, }; @@ -31,6 +30,7 @@ pub const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip"); pub trait RequestPartsExt: sealed::SealedRequestPartsExt { /// Get host name of the request URI from header `Host`. fn host(&self) -> Option<&str>; + /// Get URL of the request URI. fn url(&self) -> Option; } @@ -67,11 +67,8 @@ where } fn url(&self) -> Option { - let host = self.host(); + let host = self.host()?; let uri = self.uri(); - if host.is_none() { - return None; - } let mut url_str = String::new(); @@ -82,7 +79,7 @@ where url_str.push_str("http://"); } - url_str.push_str(host.unwrap()); + url_str.push_str(host); url_str.push_str(uri.path()); url::Url::parse(url_str.as_str()).ok() From a05fdc3cacf29a597c753c048de4c01029df80f9 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 17:15:59 +0800 Subject: [PATCH 08/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 69 ++++++++++++++++++++++++++++++++++--- volo-http/src/response.rs | 37 ++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 2e404591..20185554 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -974,7 +974,12 @@ where #[cfg(feature = "json")] #[cfg(test)] mod client_tests { - use std::{collections::HashMap, future::Future}; + use std::{ + collections::HashMap, + convert::Infallible, + future::Future, + net::{IpAddr, Ipv4Addr, SocketAddr}, + }; use cookie::Cookie; use http::{header, StatusCode}; @@ -983,7 +988,7 @@ mod client_tests { service::Service, }; use serde::Deserialize; - use volo::{context::Endpoint, layer::Identity}; + use volo::{context::Endpoint, layer::Identity, net::Address}; use super::{ callopt::CallOpt, @@ -991,8 +996,14 @@ mod client_tests { get, Client, DefaultClient, Target, }; use crate::{ - body::BodyConversion, error::client::status_error, utils::consts::HTTP_DEFAULT_PORT, - ClientBuilder, + body::BodyConversion, + context::ServerContext, + error::client::status_error, + request::ServerRequest, + response::ServerResponse, + server::{test_helpers, IntoResponse}, + utils::consts::HTTP_DEFAULT_PORT, + ClientBuilder, Server, }; #[derive(Deserialize)] @@ -1312,6 +1323,56 @@ mod client_tests { assert!(resp.is_ok()); } + async fn run_handler(service: S, port: u16) + where + S: Service + + Send + + Sync + + 'static, + { + let addr = Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + port, + )); + + tokio::spawn(Server::new(service).run(addr)); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + #[tokio::test] + async fn extract_cookie_from_server() { + async fn handler() -> impl IntoResponse { + http::Response::builder() + .header("Set-Cookie", "key=val") + .header( + "Set-Cookie", + "expires=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT", + ) + .header("Set-Cookie", "path=1; Path=/the-path") + .header("Set-Cookie", "maxage=1; Max-Age=100") + .header("Set-Cookie", "domain=1; Domain=mydomain") + .header("Set-Cookie", "secure=1; Secure") + .header("Set-Cookie", "httponly=1; HttpOnly") + .header("Set-Cookie", "samesitelax=1; SameSite=Lax") + .header("Set-Cookie", "samesitestrict=1; SameSite=Strict") + .body(Default::default()) + .unwrap() + } + + let port = 11000; + + run_handler(test_helpers::to_service(handler), port).await; + + let url_str = format!("http://127.0.0.1:{}/", port); + let url = url::Url::parse(&url_str).unwrap(); + + let mut builder = Client::builder(); + let client = builder.build(); + + let resp = client.get(url).send().await.unwrap(); + } + } + #[tokio::test] async fn with_cookie() { let mut builder = Client::builder(); diff --git a/volo-http/src/response.rs b/volo-http/src/response.rs index 260bd1d7..5c1608f4 100644 --- a/volo-http/src/response.rs +++ b/volo-http/src/response.rs @@ -1,5 +1,8 @@ //! Response types for client and server. +use cookie::Cookie; +use http::Response; + /// [`Response`] with [`Body`] as default body /// /// [`Response`]: http::response::Response @@ -13,3 +16,37 @@ pub type ServerResponse = http::response::Response; /// [`Body`]: crate::body::Body #[cfg(feature = "client")] pub type ClientResponse = http::response::Response; + +/// Utilities of [`http::response::Response`]. +pub trait ResponseExt: sealed::SealedResponseExt { + /// Get all cookies from `Set-Cookie` header. + fn cookies(&self) -> impl Iterator; +} + +mod sealed { + pub trait SealedResponseExt { + fn headers(&self) -> &http::HeaderMap; + } +} + +impl sealed::SealedResponseExt for Response { + fn headers(&self) -> &http::HeaderMap { + self.headers() + } +} + +impl ResponseExt for T +where + T: sealed::SealedResponseExt, +{ + fn cookies(&self) -> impl Iterator { + self.headers() + .get_all(http::header::SET_COOKIE) + .iter() + .filter_map(|value| { + std::str::from_utf8(value.as_bytes()) + .ok() + .and_then(|val| Cookie::parse(val).map(|c| c.into_owned()).ok()) + }) + } +} From ab0cf6c8bc63880aee80d565544c0b65616cb66c Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 17:51:07 +0800 Subject: [PATCH 09/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 255 +++++++++++++++++++++++++++++------- volo-http/src/response.rs | 3 + 2 files changed, 213 insertions(+), 45 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 20185554..a5f2a5a6 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -132,7 +132,7 @@ impl ClientBuilder { target_parser: parse_target, headers: Default::default(), #[cfg(feature = "cookie")] - cookie_store: Default::default(), + cookie_store: Some(Default::default()), inner_layer: Identity::new(), outer_layer: Identity::new(), mk_client: DefaultMkClient, @@ -979,9 +979,10 @@ mod client_tests { convert::Infallible, future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, + time::SystemTime, }; - use cookie::Cookie; + use cookie::Expiration; use http::{header, StatusCode}; use motore::{ layer::{Layer, Stack}, @@ -1000,7 +1001,7 @@ mod client_tests { context::ServerContext, error::client::status_error, request::ServerRequest, - response::ServerResponse, + response::{ResponseExt, ServerResponse}, server::{test_helpers, IntoResponse}, utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, Server, @@ -1338,65 +1339,229 @@ mod client_tests { tokio::spawn(Server::new(service).run(addr)); tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + + #[tokio::test] + async fn extract_cookie_from_server() { + async fn handler() -> impl IntoResponse { + http::Response::builder() + .header("Set-Cookie", "key=val") + .header( + "Set-Cookie", + "expires=1; Expires=Tue, 29 Oct 2024 09:49:37 GMT", + ) + .header("Set-Cookie", "path=1; Path=/the-path") + .header("Set-Cookie", "maxage=1; Max-Age=100") + .header("Set-Cookie", "domain=1; Domain=mydomain") + .header("Set-Cookie", "secure=1; Secure") + .header("Set-Cookie", "httponly=1; HttpOnly") + .header("Set-Cookie", "samesitelax=1; SameSite=Lax") + .header("Set-Cookie", "samesitestrict=1; SameSite=Strict") + .body("") + .unwrap() + } + + let port = 11000; + + run_handler(test_helpers::to_service(handler), port).await; + + let url = format!("http://127.0.0.1:{}/", port); + + let builder = Client::builder(); + let client = builder.build(); + + let resp = client.get(url).send().await.unwrap(); + + let cookies = resp.cookies().collect::>(); + + // key=val + assert_eq!(cookies[0].name(), "key"); + assert_eq!(cookies[0].value(), "val"); + + // expires + assert_eq!(cookies[1].name(), "expires"); + match cookies[1].expires() { + Some(Expiration::DateTime(offset)) => { + assert_eq!( + SystemTime::from(offset), + SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_730_195_377) + ); + } + _ => {} + } + + // path + assert_eq!(cookies[2].name(), "path"); + assert_eq!(cookies[2].path().unwrap(), "/the-path"); + + // max-age + assert_eq!(cookies[3].name(), "maxage"); + assert_eq!( + cookies[3].max_age().unwrap(), + std::time::Duration::from_secs(100) + ); + + // domain + assert_eq!(cookies[4].name(), "domain"); + assert_eq!(cookies[4].domain().unwrap(), "mydomain"); - #[tokio::test] - async fn extract_cookie_from_server() { - async fn handler() -> impl IntoResponse { + // secure + assert_eq!(cookies[5].name(), "secure"); + assert_eq!(cookies[5].secure().unwrap_or(false), true); + + // httponly + assert_eq!(cookies[6].name(), "httponly"); + assert_eq!(cookies[6].http_only().unwrap_or(false), true); + + // samesitelax + assert_eq!(cookies[7].name(), "samesitelax"); + assert_eq!(cookies[7].same_site(), Some(cookie::SameSite::Lax)); + + // samesitestrict + assert_eq!(cookies[8].name(), "samesitestrict"); + assert_eq!(cookies[8].same_site(), Some(cookie::SameSite::Strict)); + } + + #[tokio::test] + async fn cookie_store_simple() { + async fn handler(req: ServerRequest) -> impl IntoResponse { + if req.uri() == "/2" { + assert_eq!(req.headers()["cookie"], "key=val"); + } + + http::Response::builder() + .header("Set-Cookie", "key=val; HttpOnly") + .body("") + .unwrap() + } + + let port = 11001; + + run_handler(test_helpers::to_service(handler), port).await; + + let builder = Client::builder(); + let client = builder.build(); + + let url = format!("http://127.0.0.1:{}/", port); + client.get(&url).send().await.unwrap(); + + let url = format!("http://127.0.0.1:{}/2", port); + client.get(&url).send().await.unwrap(); + } + + #[tokio::test] + async fn cookie_store_overwrite_existing() { + async fn handler(req: ServerRequest) -> impl IntoResponse { + if req.uri() == "/" { http::Response::builder() .header("Set-Cookie", "key=val") - .header( - "Set-Cookie", - "expires=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT", - ) - .header("Set-Cookie", "path=1; Path=/the-path") - .header("Set-Cookie", "maxage=1; Max-Age=100") - .header("Set-Cookie", "domain=1; Domain=mydomain") - .header("Set-Cookie", "secure=1; Secure") - .header("Set-Cookie", "httponly=1; HttpOnly") - .header("Set-Cookie", "samesitelax=1; SameSite=Lax") - .header("Set-Cookie", "samesitestrict=1; SameSite=Strict") - .body(Default::default()) + .body("") .unwrap() + } else if req.uri() == "/2" { + assert_eq!(req.headers()["cookie"], "key=val"); + http::Response::builder() + .header("Set-Cookie", "key=val2") + .body("") + .unwrap() + } else { + assert_eq!(req.uri(), "/3"); + assert_eq!(req.headers()["cookie"], "key=val2"); + http::Response::default() } + } + + let port = 11002; - let port = 11000; + run_handler(test_helpers::to_service(handler), port).await; + + let builder = Client::builder(); + let client = builder.build(); - run_handler(test_helpers::to_service(handler), port).await; + let url = format!("http://127.0.0.1:{}/", port); + client.get(&url).send().await.unwrap(); - let url_str = format!("http://127.0.0.1:{}/", port); - let url = url::Url::parse(&url_str).unwrap(); + let url = format!("http://127.0.0.1:{}/2", port); + client.get(&url).send().await.unwrap(); - let mut builder = Client::builder(); - let client = builder.build(); + let url = format!("http://127.0.0.1:{}/3", port); + client.get(&url).send().await.unwrap(); + } - let resp = client.get(url).send().await.unwrap(); + #[tokio::test] + async fn cookie_store_max_age() { + async fn handler(req: ServerRequest) -> impl IntoResponse { + assert_eq!(req.headers().get("cookie"), None); + http::Response::builder() + .header("Set-Cookie", "key=val; Max-Age=0") + .body("") + .unwrap() } + + let port = 11003; + + run_handler(test_helpers::to_service(handler), port).await; + + let builder = Client::builder(); + let client = builder.build(); + + let url = format!("http://127.0.0.1:{}/", port); + client.get(&url).send().await.unwrap(); + client.get(&url).send().await.unwrap(); } #[tokio::test] - async fn with_cookie() { - let mut builder = Client::builder(); - builder - .header(header::COOKIE.as_str(), "foo=bar; foo1=bar1") - .unwrap(); + async fn cookie_store_expires() { + async fn handler(req: ServerRequest) -> impl IntoResponse { + assert_eq!(req.headers().get("cookie"), None); + http::Response::builder() + .header( + "Set-Cookie", + "key=val; Expires=Wed, 21 Oct 2015 07:28:00 GMT", + ) + .body("") + .unwrap() + } + + let port = 11004; + + run_handler(test_helpers::to_service(handler), port).await; + + let builder = Client::builder(); let client = builder.build(); - let resp = client - .get(HTTPBIN_GET) - .send() - .await - .unwrap() - .into_json::() - .await - .unwrap(); + let url = format!("http://127.0.0.1:{}/", port); + client.get(&url).send().await.unwrap(); + client.get(&url).send().await.unwrap(); + } + + #[tokio::test] + async fn cookie_store_path() { + async fn handler(req: ServerRequest) -> impl IntoResponse { + if req.uri() == "/" { + assert_eq!(req.headers().get("cookie"), None); + http::Response::builder() + .header("Set-Cookie", "key=val; Path=/subpath") + .body("") + .unwrap() + } else { + assert_eq!(req.uri(), "/subpath"); + assert_eq!(req.headers()["cookie"], "key=val"); + http::Response::default() + } + } + + let port = 11005; + + run_handler(test_helpers::to_service(handler), port).await; + + let builder = Client::builder(); + let client = builder.build(); - let mut actual: Vec<_> = Cookie::split_parse(resp.headers.get("Cookie").unwrap()) - .filter_map(|parse| parse.ok()) - .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) - .collect(); - // Hence the order is not guaranteed, so we need to sort before compare - actual.sort(); + let url = format!("http://127.0.0.1:{}/", port); + client.get(&url).send().await.unwrap(); + client.get(&url).send().await.unwrap(); - assert_eq!(actual, vec![("foo", "bar"), ("foo1", "bar1")]); + let url = format!("http://127.0.0.1:{}/subpath", port); + client.get(&url).send().await.unwrap(); } } diff --git a/volo-http/src/response.rs b/volo-http/src/response.rs index 5c1608f4..68e6c81b 100644 --- a/volo-http/src/response.rs +++ b/volo-http/src/response.rs @@ -1,5 +1,6 @@ //! Response types for client and server. +#[cfg(feature = "cookie")] use cookie::Cookie; use http::Response; @@ -20,6 +21,7 @@ pub type ClientResponse = http::response::Response; /// Utilities of [`http::response::Response`]. pub trait ResponseExt: sealed::SealedResponseExt { /// Get all cookies from `Set-Cookie` header. + #[cfg(feature = "cookie")] fn cookies(&self) -> impl Iterator; } @@ -39,6 +41,7 @@ impl ResponseExt for T where T: sealed::SealedResponseExt, { + #[cfg(feature = "cookie")] fn cookies(&self) -> impl Iterator { self.headers() .get_all(http::header::SET_COOKIE) From c39b0655e9519cdcfdb3437fd8a2eb33c999e5bc Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 17:51:56 +0800 Subject: [PATCH 10/34] feat(http): add cookie for client --- Cargo.toml | 1 - volo-http/Cargo.toml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 20f07c02..0492e04c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,6 @@ http-body-util = "0.1" hyper = "1" hyper-timeout = "0.5" hyper-util = "0.1" -idna = "1.0" itertools = "0.13" itoa = "1" libc = "0.2" diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index ec6cf304..f21a4927 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -76,7 +76,6 @@ tokio-native-tls = { workspace = true, optional = true } # cookie support cookie = { workspace = true, optional = true, features = ["percent-encode"] } cookie_store = { workspace = true, optional = true } -idna = { workspace = true, optional = true } url = { workspace = true, optional = true } # serde and form, query, json @@ -112,7 +111,7 @@ rustls = ["__tls", "dep:tokio-rustls", "volo/rustls"] native-tls = ["__tls", "dep:tokio-native-tls", "volo/native-tls"] native-tls-vendored = ["native-tls", "volo/native-tls-vendored"] -cookie = ["dep:cookie", "dep:cookie_store", "dep:idna", "dep:url"] +cookie = ["dep:cookie", "dep:cookie_store", "dep:url"] __serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx` query = ["__serde", "dep:serde_urlencoded"] From 8c7bc945a9788f6cbc1ec22c4e2d8fbfbabb5de1 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 17:52:00 +0800 Subject: [PATCH 11/34] feat(http): add cookie for client --- Cargo.lock | 266 ----------------------------------------------------- 1 file changed, 266 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f656526..190d1fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,17 +620,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "downcast" version = "0.11.0" @@ -1353,124 +1342,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "idna" version = "0.3.0" @@ -1501,18 +1372,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" -dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -1689,12 +1548,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -3167,12 +3020,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "strsim" version = "0.11.1" @@ -3222,17 +3069,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "sysinfo" version = "0.31.4" @@ -3388,16 +3224,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.8.0" @@ -3880,18 +3706,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -4072,7 +3886,6 @@ dependencies = [ "http-body-util", "hyper 1.4.1", "hyper-util", - "idna 1.0.2", "itoa", "libc", "matchit 0.8.4", @@ -4561,42 +4374,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.35" @@ -4618,51 +4395,8 @@ dependencies = [ "syn 2.0.77", ] -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] From 8063073fc7df661ce2a2447ce3459003d006101b Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 17:53:42 +0800 Subject: [PATCH 12/34] feat(http): add cookie for client --- volo-http/src/utils/cookie/store.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index 3c239587..cdd93781 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -30,9 +30,8 @@ impl CookieStore { } } - /// Get [`HeaderValue`] from the cookie jar - #[cfg(feature = "client")] - pub(crate) fn cookies(&self, request_url: &url::Url) -> Option { + /// Get [`HeaderValue`] from the cookie store + pub fn cookies(&self, request_url: &url::Url) -> Option { let s = self .inner .get_request_values(request_url) From 5c6f746e3ba0d6aea3a0b5772568b51385d12c8c Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 29 Oct 2024 17:57:26 +0800 Subject: [PATCH 13/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index a5f2a5a6..e7e8710c 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -686,9 +686,6 @@ impl ClientBuilder { .make() .layer(self.inner_layer.layer(meta_service)), ); - // TODO: add cookie layer - - // TODO: how to implement redirect layer let caller_name = if self.caller_name.is_empty() { FastStr::from_static_str(PKG_NAME_WITH_VER) From 3522e3ed91e6e6395f843905afba5f03e9e6390b Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 30 Oct 2024 17:46:14 +0800 Subject: [PATCH 14/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 90 +++++++++++++++++++++++++++++ volo-http/src/client/mod.rs | 61 +++++++++---------- volo-http/src/client/transport.rs | 46 +-------------- volo-http/src/utils/cookie/store.rs | 2 +- 4 files changed, 125 insertions(+), 74 deletions(-) create mode 100644 volo-http/src/client/cookie.rs diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs new file mode 100644 index 00000000..d1818f0b --- /dev/null +++ b/volo-http/src/client/cookie.rs @@ -0,0 +1,90 @@ +use std::sync::RwLock; + +use motore::{layer::Layer, Service}; + +use crate::{ + context::ClientContext, + error::ClientError, + request::{ClientRequest, RequestPartsExt}, + response::ClientResponse, + utils::cookie::CookieStore, +}; + +#[cfg(feature = "cookie")] +pub struct CookieService { + inner: S, + cookie_store: RwLock, +} + +impl CookieService { + fn new(inner: S, cookie_store: RwLock) -> Self { + Self { + inner, + cookie_store, + } + } +} + +impl Service> for CookieService +where + S: Service, Response = ClientResponse, Error = ClientError> + + Send + + Sync + + 'static, + B: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + + async fn call( + &self, + cx: &mut ClientContext, + mut req: ClientRequest, + ) -> Result { + let url = req.url(); + + let (mut parts, body) = req.into_parts(); + if let Some(url) = &url { + if parts.headers.get(http::header::COOKIE).is_none() { + self.cookie_store + .read() + .unwrap() + .add_cookie_header(&mut parts.headers, url); + } + } + + req = ClientRequest::from_parts(parts, body); + + let resp = self.inner.call(cx, req).await?; + + if let Some(url) = &url { + self.cookie_store + .write() + .unwrap() + .with_response_headers(resp.headers(), url); + } + + Ok(resp) + } +} + +#[cfg(feature = "cookie")] +pub struct CookieLayer { + cookie_store: RwLock, +} + +impl CookieLayer { + pub fn new(cookie_store: CookieStore) -> Self { + Self { + cookie_store: RwLock::new(cookie_store), + } + } +} + +impl Layer for CookieLayer { + type Service = CookieService; + + fn layer(self, inner: S) -> Self::Service { + CookieService::new(inner, self.cookie_store) + } +} diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index e7e8710c..8d20022d 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -38,6 +38,7 @@ use self::{ transport::{ClientConfig, ClientTransport, ClientTransportConfig}, }; use crate::{ + client::cookie::CookieLayer, context::{client::Config, ClientContext}, error::{ client::{builder_error, no_address, ClientError, Result}, @@ -45,10 +46,11 @@ use crate::{ }, request::ClientRequest, response::ClientResponse, - utils::cookie::CookieStore, }; pub mod callopt; +#[cfg(feature = "cookie")] +mod cookie; pub mod dns; pub mod loadbalance; mod meta; @@ -84,8 +86,6 @@ pub struct ClientBuilder { call_opt: Option, target_parser: TargetParser, headers: HeaderMap, - #[cfg(feature = "cookie")] - cookie_store: Option, inner_layer: IL, outer_layer: OL, mk_client: C, @@ -131,8 +131,6 @@ impl ClientBuilder { call_opt: Default::default(), target_parser: parse_target, headers: Default::default(), - #[cfg(feature = "cookie")] - cookie_store: Some(Default::default()), inner_layer: Identity::new(), outer_layer: Identity::new(), mk_client: DefaultMkClient, @@ -165,8 +163,6 @@ impl ClientBuilder> { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -188,8 +184,6 @@ impl ClientBuilder> { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -214,8 +208,6 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: new_mk_client, @@ -249,8 +241,6 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: Stack::new(layer, self.inner_layer), outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -287,8 +277,6 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: Stack::new(self.inner_layer, layer), outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -322,8 +310,6 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: Stack::new(layer, self.outer_layer), mk_client: self.mk_client, @@ -360,8 +346,6 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: Stack::new(self.outer_layer, layer), mk_client: self.mk_client, @@ -383,8 +367,6 @@ impl ClientBuilder { call_opt: self.call_opt, target_parser: self.target_parser, headers: self.headers, - #[cfg(feature = "cookie")] - cookie_store: self.cookie_store, inner_layer: self.inner_layer, outer_layer: self.outer_layer, mk_client: self.mk_client, @@ -654,6 +636,28 @@ impl ClientBuilder { self } + /// Enable cookie for the client. + #[cfg(feature = "cookie")] + pub fn set_cookie_layer(self) -> ClientBuilder, OL, C, LB> { + ClientBuilder { + http_config: self.http_config, + builder_config: self.builder_config, + connector: self.connector, + callee_name: self.callee_name, + caller_name: self.caller_name, + target: self.target, + call_opt: self.call_opt, + target_parser: self.target_parser, + headers: self.headers, + inner_layer: Stack::new(CookieLayer::new(Default::default()), self.inner_layer), + outer_layer: self.outer_layer, + mk_client: self.mk_client, + mk_lb: self.mk_lb, + #[cfg(feature = "__tls")] + tls_config: self.tls_config, + } + } + /// Build the HTTP client. pub fn build(mut self) -> C::Target where @@ -675,8 +679,6 @@ impl ClientBuilder { self.http_config, transport_config, self.connector, - #[cfg(feature = "cookie")] - self.cookie_store, #[cfg(feature = "__tls")] self.tls_config.unwrap_or_default(), ); @@ -698,7 +700,6 @@ impl ClientBuilder { HeaderValue::from_str(caller_name.as_str()).expect("Invalid caller name"), ); } - let config = Config { timeout: self.builder_config.timeout, fail_on_error_status: self.builder_config.fail_on_error_status, @@ -1365,7 +1366,7 @@ mod client_tests { let url = format!("http://127.0.0.1:{}/", port); let builder = Client::builder(); - let client = builder.build(); + let client = builder.set_cookie_layer().build(); let resp = client.get(url).send().await.unwrap(); @@ -1437,7 +1438,7 @@ mod client_tests { run_handler(test_helpers::to_service(handler), port).await; let builder = Client::builder(); - let client = builder.build(); + let client = builder.set_cookie_layer().build(); let url = format!("http://127.0.0.1:{}/", port); client.get(&url).send().await.unwrap(); @@ -1472,7 +1473,7 @@ mod client_tests { run_handler(test_helpers::to_service(handler), port).await; let builder = Client::builder(); - let client = builder.build(); + let client = builder.set_cookie_layer().build(); let url = format!("http://127.0.0.1:{}/", port); client.get(&url).send().await.unwrap(); @@ -1499,7 +1500,7 @@ mod client_tests { run_handler(test_helpers::to_service(handler), port).await; let builder = Client::builder(); - let client = builder.build(); + let client = builder.set_cookie_layer().build(); let url = format!("http://127.0.0.1:{}/", port); client.get(&url).send().await.unwrap(); @@ -1524,7 +1525,7 @@ mod client_tests { run_handler(test_helpers::to_service(handler), port).await; let builder = Client::builder(); - let client = builder.build(); + let client = builder.set_cookie_layer().build(); let url = format!("http://127.0.0.1:{}/", port); client.get(&url).send().await.unwrap(); @@ -1552,7 +1553,7 @@ mod client_tests { run_handler(test_helpers::to_service(handler), port).await; let builder = Client::builder(); - let client = builder.build(); + let client = builder.set_cookie_layer().build(); let url = format!("http://127.0.0.1:{}/", port); client.get(&url).send().await.unwrap(); diff --git a/volo-http/src/client/transport.rs b/volo-http/src/client/transport.rs index d5bc41c1..22a464b2 100644 --- a/volo-http/src/client/transport.rs +++ b/volo-http/src/client/transport.rs @@ -1,4 +1,4 @@ -use std::{error::Error, sync::RwLock}; +use std::error::Error; use hyper::client::conn::http1; use hyper_util::rt::TokioIo; @@ -10,12 +10,10 @@ use volo::{ net::{conn::Conn, dial::DefaultMakeTransport, Address}, }; -#[cfg(feature = "cookie")] -use crate::utils::cookie::CookieStore; use crate::{ context::ClientContext, error::client::{no_address, request_error, ClientError}, - request::{ClientRequest, RequestPartsExt}, + request::ClientRequest, response::ClientResponse, }; @@ -32,8 +30,6 @@ pub struct ClientTransport { client: http1::Builder, mk_conn: DefaultMakeTransport, config: ClientTransportConfig, - #[cfg(feature = "cookie")] - cookie_store: Option>, #[cfg(feature = "__tls")] tls_connector: volo::net::tls::TlsConnector, } @@ -43,7 +39,6 @@ impl ClientTransport { http_config: ClientConfig, transport_config: ClientTransportConfig, mk_conn: DefaultMakeTransport, - #[cfg(feature = "cookie")] cookie_store: Option, #[cfg(feature = "__tls")] tls_connector: volo::net::tls::TlsConnector, ) -> Self { let mut builder = http1::Builder::new(); @@ -58,8 +53,6 @@ impl ClientTransport { client: builder, mk_conn, config: transport_config, - #[cfg(feature = "cookie")] - cookie_store: cookie_store.map(RwLock::new), #[cfg(feature = "__tls")] tls_connector, } @@ -118,33 +111,13 @@ impl ClientTransport { async fn request( &self, cx: &ClientContext, - mut req: ClientRequest, + req: ClientRequest, ) -> Result where B: http_body::Body + Send + 'static, B::Data: Send, B::Error: Into> + 'static, { - #[cfg(feature = "cookie")] - let url = req.url(); - - #[cfg(feature = "cookie")] - { - let (mut parts, body) = req.into_parts(); - if let Some(cookie_store) = self.cookie_store.as_ref() { - if let Some(url) = &url { - if parts.headers.get(http::header::COOKIE).is_none() { - cookie_store - .read() - .unwrap() - .add_cookie_header(&mut parts.headers, url); - } - } - } - - req = ClientRequest::from_parts(parts, body); - } - tracing::trace!("[Volo-HTTP] requesting {}", req.uri()); let conn = self.make_connection(cx).await?; let io = TokioIo::new(conn); @@ -157,19 +130,6 @@ impl ClientTransport { tracing::error!("[Volo-HTTP] failed to send request, error: {err}"); request_error(err) })?; - - #[cfg(feature = "cookie")] - { - if let Some(ref cookie_store) = self.cookie_store { - if let Some(url) = &url { - cookie_store - .write() - .unwrap() - .with_response_headers(resp.headers(), url); - } - } - } - Ok(resp.map(crate::body::Body::from_incoming)) } } diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index cdd93781..7008e0ea 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -6,7 +6,7 @@ use http::{header, HeaderMap, HeaderValue}; /// A cooke jar that can be extracted from a handler. #[derive(Default)] -pub(crate) struct CookieStore { +pub struct CookieStore { inner: cookie_store::CookieStore, } From 93a97a4f6f760c0037736186f4f53c83e0aca2db Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 30 Oct 2024 17:48:40 +0800 Subject: [PATCH 15/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index d1818f0b..c3de8b43 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -10,7 +10,6 @@ use crate::{ utils::cookie::CookieStore, }; -#[cfg(feature = "cookie")] pub struct CookieService { inner: S, cookie_store: RwLock, @@ -68,7 +67,6 @@ where } } -#[cfg(feature = "cookie")] pub struct CookieLayer { cookie_store: RwLock, } From 5c71f99fc770a0c17c5637c6bf2e38e8012c7941 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 30 Oct 2024 17:49:20 +0800 Subject: [PATCH 16/34] feat(http): add cookie for client --- volo-http/src/client/transport.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/volo-http/src/client/transport.rs b/volo-http/src/client/transport.rs index 22a464b2..a111ba58 100644 --- a/volo-http/src/client/transport.rs +++ b/volo-http/src/client/transport.rs @@ -26,6 +26,7 @@ use crate::{ #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "native-tls"))))] pub struct TlsTransport; +#[derive(Clone)] pub struct ClientTransport { client: http1::Builder, mk_conn: DefaultMakeTransport, From 46e52af2a672b70b3713a7d978069bfb8ec6b7f3 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 14:37:51 +0800 Subject: [PATCH 17/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 41 ++++- volo-http/src/client/mod.rs | 237 ++-------------------------- volo-http/src/utils/cookie/store.rs | 11 +- 3 files changed, 52 insertions(+), 237 deletions(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index c3de8b43..03765110 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -1,6 +1,9 @@ -use std::sync::RwLock; +//! Cookie implementation +//! +//! This module provides [`CookieLayer`] for extracting and setting cookies. use motore::{layer::Layer, Service}; +use parking_lot::RwLock; use crate::{ context::ClientContext, @@ -10,6 +13,9 @@ use crate::{ utils::cookie::CookieStore, }; +/// [`CookieLayer`] generated [`Service`] +/// +/// See [`CookieLayer`] for more details. pub struct CookieService { inner: S, cookie_store: RwLock, @@ -42,36 +48,57 @@ where ) -> Result { let url = req.url(); - let (mut parts, body) = req.into_parts(); if let Some(url) = &url { + let (mut parts, body) = req.into_parts(); if parts.headers.get(http::header::COOKIE).is_none() { self.cookie_store .read() - .unwrap() .add_cookie_header(&mut parts.headers, url); } + req = ClientRequest::from_parts(parts, body); } - req = ClientRequest::from_parts(parts, body); - let resp = self.inner.call(cx, req).await?; if let Some(url) = &url { self.cookie_store .write() - .unwrap() - .with_response_headers(resp.headers(), url); + .store_response_headers(resp.headers(), url); } Ok(resp) } } +/// [`Layer`] for extracting and setting cookies. +/// +/// See [`CookieLayer::new`] for more details. pub struct CookieLayer { cookie_store: RwLock, } impl CookieLayer { + /// Create a new [`CookieLayer`] with the given [`CookieStore`]. + /// + /// It will set cookies from the [`CookieStore`] into the request header before sending the + /// request, + /// + /// and store cookies after receiving the response. + /// + /// It is recommended to use [`CookieLayer`] as the innermost layer in the client stack, + /// + /// since it will extract cookies from the request header and store them in the [`CookieStore`]. + /// + /// # Example + /// + /// ```rust + /// use volo_http::{client::cookie::CookieLayer, Client}; + /// + /// let builder = Client::builder(); + /// let client = builder + /// .layer_inner(CookieLayer::new(Default::default())) + /// .build(); + /// ``` pub fn new(cookie_store: CookieStore) -> Self { Self { cookie_store: RwLock::new(cookie_store), diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 8d20022d..3cdba21b 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -38,7 +38,6 @@ use self::{ transport::{ClientConfig, ClientTransport, ClientTransportConfig}, }; use crate::{ - client::cookie::CookieLayer, context::{client::Config, ClientContext}, error::{ client::{builder_error, no_address, ClientError, Result}, @@ -50,7 +49,7 @@ use crate::{ pub mod callopt; #[cfg(feature = "cookie")] -mod cookie; +pub mod cookie; pub mod dns; pub mod loadbalance; mod meta; @@ -636,28 +635,6 @@ impl ClientBuilder { self } - /// Enable cookie for the client. - #[cfg(feature = "cookie")] - pub fn set_cookie_layer(self) -> ClientBuilder, OL, C, LB> { - ClientBuilder { - http_config: self.http_config, - builder_config: self.builder_config, - connector: self.connector, - callee_name: self.callee_name, - caller_name: self.caller_name, - target: self.target, - call_opt: self.call_opt, - target_parser: self.target_parser, - headers: self.headers, - inner_layer: Stack::new(CookieLayer::new(Default::default()), self.inner_layer), - outer_layer: self.outer_layer, - mk_client: self.mk_client, - mk_lb: self.mk_lb, - #[cfg(feature = "__tls")] - tls_config: self.tls_config, - } - } - /// Build the HTTP client. pub fn build(mut self) -> C::Target where @@ -977,10 +954,8 @@ mod client_tests { convert::Infallible, future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, - time::SystemTime, }; - use cookie::Expiration; use http::{header, StatusCode}; use motore::{ layer::{Layer, Stack}, @@ -996,10 +971,11 @@ mod client_tests { }; use crate::{ body::BodyConversion, + client::cookie::CookieLayer, context::ServerContext, error::client::status_error, request::ServerRequest, - response::{ResponseExt, ServerResponse}, + response::ServerResponse, server::{test_helpers, IntoResponse}, utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, Server, @@ -1340,95 +1316,16 @@ mod client_tests { } #[tokio::test] - async fn extract_cookie_from_server() { - async fn handler() -> impl IntoResponse { - http::Response::builder() - .header("Set-Cookie", "key=val") - .header( - "Set-Cookie", - "expires=1; Expires=Tue, 29 Oct 2024 09:49:37 GMT", - ) - .header("Set-Cookie", "path=1; Path=/the-path") - .header("Set-Cookie", "maxage=1; Max-Age=100") - .header("Set-Cookie", "domain=1; Domain=mydomain") - .header("Set-Cookie", "secure=1; Secure") - .header("Set-Cookie", "httponly=1; HttpOnly") - .header("Set-Cookie", "samesitelax=1; SameSite=Lax") - .header("Set-Cookie", "samesitestrict=1; SameSite=Strict") - .body("") - .unwrap() - } - - let port = 11000; - - run_handler(test_helpers::to_service(handler), port).await; - - let url = format!("http://127.0.0.1:{}/", port); - - let builder = Client::builder(); - let client = builder.set_cookie_layer().build(); - - let resp = client.get(url).send().await.unwrap(); - - let cookies = resp.cookies().collect::>(); - - // key=val - assert_eq!(cookies[0].name(), "key"); - assert_eq!(cookies[0].value(), "val"); - - // expires - assert_eq!(cookies[1].name(), "expires"); - match cookies[1].expires() { - Some(Expiration::DateTime(offset)) => { - assert_eq!( - SystemTime::from(offset), - SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_730_195_377) - ); - } - _ => {} - } - - // path - assert_eq!(cookies[2].name(), "path"); - assert_eq!(cookies[2].path().unwrap(), "/the-path"); - - // max-age - assert_eq!(cookies[3].name(), "maxage"); - assert_eq!( - cookies[3].max_age().unwrap(), - std::time::Duration::from_secs(100) - ); - - // domain - assert_eq!(cookies[4].name(), "domain"); - assert_eq!(cookies[4].domain().unwrap(), "mydomain"); - - // secure - assert_eq!(cookies[5].name(), "secure"); - assert_eq!(cookies[5].secure().unwrap_or(false), true); - - // httponly - assert_eq!(cookies[6].name(), "httponly"); - assert_eq!(cookies[6].http_only().unwrap_or(false), true); - - // samesitelax - assert_eq!(cookies[7].name(), "samesitelax"); - assert_eq!(cookies[7].same_site(), Some(cookie::SameSite::Lax)); - - // samesitestrict - assert_eq!(cookies[8].name(), "samesitestrict"); - assert_eq!(cookies[8].same_site(), Some(cookie::SameSite::Strict)); - } - - #[tokio::test] - async fn cookie_store_simple() { + async fn cookie_store() { async fn handler(req: ServerRequest) -> impl IntoResponse { + // test cookie header after server set cookie if req.uri() == "/2" { assert_eq!(req.headers()["cookie"], "key=val"); } + // test server set cookie http::Response::builder() - .header("Set-Cookie", "key=val; HttpOnly") + .header("Set-Cookie", "key=val") .body("") .unwrap() } @@ -1438,128 +1335,14 @@ mod client_tests { run_handler(test_helpers::to_service(handler), port).await; let builder = Client::builder(); - let client = builder.set_cookie_layer().build(); - - let url = format!("http://127.0.0.1:{}/", port); - client.get(&url).send().await.unwrap(); - - let url = format!("http://127.0.0.1:{}/2", port); - client.get(&url).send().await.unwrap(); - } - - #[tokio::test] - async fn cookie_store_overwrite_existing() { - async fn handler(req: ServerRequest) -> impl IntoResponse { - if req.uri() == "/" { - http::Response::builder() - .header("Set-Cookie", "key=val") - .body("") - .unwrap() - } else if req.uri() == "/2" { - assert_eq!(req.headers()["cookie"], "key=val"); - http::Response::builder() - .header("Set-Cookie", "key=val2") - .body("") - .unwrap() - } else { - assert_eq!(req.uri(), "/3"); - assert_eq!(req.headers()["cookie"], "key=val2"); - http::Response::default() - } - } - - let port = 11002; - - run_handler(test_helpers::to_service(handler), port).await; - - let builder = Client::builder(); - let client = builder.set_cookie_layer().build(); + let client = builder + .layer_inner(CookieLayer::new(Default::default())) + .build(); let url = format!("http://127.0.0.1:{}/", port); client.get(&url).send().await.unwrap(); let url = format!("http://127.0.0.1:{}/2", port); client.get(&url).send().await.unwrap(); - - let url = format!("http://127.0.0.1:{}/3", port); - client.get(&url).send().await.unwrap(); - } - - #[tokio::test] - async fn cookie_store_max_age() { - async fn handler(req: ServerRequest) -> impl IntoResponse { - assert_eq!(req.headers().get("cookie"), None); - http::Response::builder() - .header("Set-Cookie", "key=val; Max-Age=0") - .body("") - .unwrap() - } - - let port = 11003; - - run_handler(test_helpers::to_service(handler), port).await; - - let builder = Client::builder(); - let client = builder.set_cookie_layer().build(); - - let url = format!("http://127.0.0.1:{}/", port); - client.get(&url).send().await.unwrap(); - client.get(&url).send().await.unwrap(); - } - - #[tokio::test] - async fn cookie_store_expires() { - async fn handler(req: ServerRequest) -> impl IntoResponse { - assert_eq!(req.headers().get("cookie"), None); - http::Response::builder() - .header( - "Set-Cookie", - "key=val; Expires=Wed, 21 Oct 2015 07:28:00 GMT", - ) - .body("") - .unwrap() - } - - let port = 11004; - - run_handler(test_helpers::to_service(handler), port).await; - - let builder = Client::builder(); - let client = builder.set_cookie_layer().build(); - - let url = format!("http://127.0.0.1:{}/", port); - client.get(&url).send().await.unwrap(); - client.get(&url).send().await.unwrap(); - } - - #[tokio::test] - async fn cookie_store_path() { - async fn handler(req: ServerRequest) -> impl IntoResponse { - if req.uri() == "/" { - assert_eq!(req.headers().get("cookie"), None); - http::Response::builder() - .header("Set-Cookie", "key=val; Path=/subpath") - .body("") - .unwrap() - } else { - assert_eq!(req.uri(), "/subpath"); - assert_eq!(req.headers()["cookie"], "key=val"); - http::Response::default() - } - } - - let port = 11005; - - run_handler(test_helpers::to_service(handler), port).await; - - let builder = Client::builder(); - let client = builder.set_cookie_layer().build(); - - let url = format!("http://127.0.0.1:{}/", port); - client.get(&url).send().await.unwrap(); - client.get(&url).send().await.unwrap(); - - let url = format!("http://127.0.0.1:{}/subpath", port); - client.get(&url).send().await.unwrap(); } } diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index 7008e0ea..142ab30c 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -4,7 +4,6 @@ use bytes::Bytes; use cookie::Cookie; use http::{header, HeaderMap, HeaderValue}; -/// A cooke jar that can be extracted from a handler. #[derive(Default)] pub struct CookieStore { inner: cookie_store::CookieStore, @@ -17,7 +16,7 @@ impl CookieStore { } } - pub fn with_response_headers(&mut self, headers: &HeaderMap, request_url: &url::Url) { + pub fn store_response_headers(&mut self, headers: &HeaderMap, request_url: &url::Url) { let mut set_cookie_headers = headers.get_all(header::SET_COOKIE).iter().peekable(); if set_cookie_headers.peek().is_some() { @@ -35,7 +34,13 @@ impl CookieStore { let s = self .inner .get_request_values(request_url) - .map(|(name, value)| format!("{name}={value}")) + .map(|(name, value)| { + let mut s = String::with_capacity(name.len() + value.len() + 1); + s.push_str(name); + s.push('='); + s.push_str(value); + s + }) .collect::>() .join("; "); From 6c9b8ad3c9f69b4b2223a06d71e773670c8db90c Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 14:40:43 +0800 Subject: [PATCH 18/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index 03765110..d07a2627 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -1,6 +1,8 @@ //! Cookie implementation //! //! This module provides [`CookieLayer`] for extracting and setting cookies. +//! +//! See [`CookieLayer`] for more details. use motore::{layer::Layer, Service}; use parking_lot::RwLock; From 1f2e378376eb174695fb83b49e534c61b807e79a Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 14:43:21 +0800 Subject: [PATCH 19/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index d07a2627..36afed60 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -89,7 +89,8 @@ impl CookieLayer { /// /// It is recommended to use [`CookieLayer`] as the innermost layer in the client stack, /// - /// since it will extract cookies from the request header and store them in the [`CookieStore`]. + /// since it will extract cookies from the request header and store them before and after call + /// the transport layer. /// /// # Example /// From 4c4c5c9b78289ab7ef741b67216a4f44dadfd378 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 14:43:43 +0800 Subject: [PATCH 20/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index 36afed60..946fe317 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -87,8 +87,7 @@ impl CookieLayer { /// /// and store cookies after receiving the response. /// - /// It is recommended to use [`CookieLayer`] as the innermost layer in the client stack, - /// + /// It is recommended to use [`CookieLayer`] as the innermost layer in the client stack /// since it will extract cookies from the request header and store them before and after call /// the transport layer. /// From fc10524e4f0b37415344f04c416951db82c9f5b7 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:16:15 +0800 Subject: [PATCH 21/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 87 ++++++++++++------------------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 3cdba21b..3ddd1415 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -72,7 +72,7 @@ const PKG_NAME_WITH_VER: &str = concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO pub type ClientMetaService = MetaService; /// Default [`Client`] without any extra [`Layer`]s pub type DefaultClient = - Client<
    >::Service>>>::Service>; +Client<
      >::Service>>>::Service>; /// A builder for configuring an HTTP [`Client`]. pub struct ClientBuilder { @@ -844,10 +844,10 @@ impl Client { timeout: Option, ) -> Result where - S: Service, Response = ClientResponse, Error = ClientError> - + Send - + Sync - + 'static, + S: Service, Response=ClientResponse, Error=ClientError> + + Send + + Sync + + 'static, B: Send + 'static, { let caller_name = self.inner.caller_name.clone(); @@ -897,10 +897,10 @@ impl Client { impl Service> for Client where - S: Service, Response = ClientResponse, Error = ClientError> - + Send - + Sync - + 'static, + S: Service, Response=ClientResponse, Error=ClientError> + + Send + + Sync + + 'static, B: Send + 'static, { type Response = S::Response; @@ -951,9 +951,7 @@ where mod client_tests { use std::{ collections::HashMap, - convert::Infallible, future::Future, - net::{IpAddr, Ipv4Addr, SocketAddr}, }; use http::{header, StatusCode}; @@ -962,7 +960,7 @@ mod client_tests { service::Service, }; use serde::Deserialize; - use volo::{context::Endpoint, layer::Identity, net::Address}; + use volo::{context::Endpoint, layer::Identity}; use super::{ callopt::CallOpt, @@ -972,14 +970,11 @@ mod client_tests { use crate::{ body::BodyConversion, client::cookie::CookieLayer, - context::ServerContext, error::client::status_error, - request::ServerRequest, - response::ServerResponse, - server::{test_helpers, IntoResponse}, utils::consts::HTTP_DEFAULT_PORT, - ClientBuilder, Server, + ClientBuilder, }; + use crate::response::ResponseExt; #[derive(Deserialize)] struct HttpBinResponse { @@ -1022,7 +1017,7 @@ mod client_tests { &self, cx: &mut Cx, req: Req, - ) -> impl Future> + Send { + ) -> impl Future> + Send { self.inner.call(cx, req) } } @@ -1298,51 +1293,25 @@ mod client_tests { assert!(resp.is_ok()); } - async fn run_handler(service: S, port: u16) - where - S: Service - + Send - + Sync - + 'static, - { - let addr = Address::Ip(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - port, - )); - - tokio::spawn(Server::new(service).run(addr)); - - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - #[tokio::test] async fn cookie_store() { - async fn handler(req: ServerRequest) -> impl IntoResponse { - // test cookie header after server set cookie - if req.uri() == "/2" { - assert_eq!(req.headers()["cookie"], "key=val"); - } - - // test server set cookie - http::Response::builder() - .header("Set-Cookie", "key=val") - .body("") - .unwrap() - } + let mut builder = Client::builder() + .layer_inner(CookieLayer::new(Default::default())); - let port = 11001; - - run_handler(test_helpers::to_service(handler), port).await; - - let builder = Client::builder(); - let client = builder - .layer_inner(CookieLayer::new(Default::default())) - .build(); + builder.host("httpbin.org"); - let url = format!("http://127.0.0.1:{}/", port); - client.get(&url).send().await.unwrap(); + let client = builder.build(); - let url = format!("http://127.0.0.1:{}/2", port); - client.get(&url).send().await.unwrap(); + // test server add cookie + let resp = client.get("http://httpbin.org/cookies/set?key=value").send().await.unwrap(); + let cookies = resp.cookies().collect::>(); + assert_eq!(cookies[0].name(), "key"); + assert_eq!(cookies[0].value(), "value"); + + // test server delete cookie + _ = client.get("http://httpbin.org/cookies/delete?key").send().await.unwrap(); + let resp = client.get(HTTPBIN_GET).send().await.unwrap(); + let cookies = resp.cookies().collect::>(); + assert_eq!(cookies.len(), 0) } } From 58dece8f0633ab744daa62c6b58a4804886a0798 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:22:55 +0800 Subject: [PATCH 22/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 3ddd1415..4786c842 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -72,7 +72,7 @@ const PKG_NAME_WITH_VER: &str = concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO pub type ClientMetaService = MetaService; /// Default [`Client`] without any extra [`Layer`]s pub type DefaultClient = -Client<
        >::Service>>>::Service>; + Client<
          >::Service>>>::Service>; /// A builder for configuring an HTTP [`Client`]. pub struct ClientBuilder { @@ -844,10 +844,10 @@ impl Client { timeout: Option, ) -> Result where - S: Service, Response=ClientResponse, Error=ClientError> - + Send - + Sync - + 'static, + S: Service, Response = ClientResponse, Error = ClientError> + + Send + + Sync + + 'static, B: Send + 'static, { let caller_name = self.inner.caller_name.clone(); @@ -897,10 +897,10 @@ impl Client { impl Service> for Client where - S: Service, Response=ClientResponse, Error=ClientError> - + Send - + Sync - + 'static, + S: Service, Response = ClientResponse, Error = ClientError> + + Send + + Sync + + 'static, B: Send + 'static, { type Response = S::Response; @@ -949,10 +949,7 @@ where #[cfg(feature = "json")] #[cfg(test)] mod client_tests { - use std::{ - collections::HashMap, - future::Future, - }; + use std::{collections::HashMap, future::Future}; use http::{header, StatusCode}; use motore::{ @@ -968,13 +965,9 @@ mod client_tests { get, Client, DefaultClient, Target, }; use crate::{ - body::BodyConversion, - client::cookie::CookieLayer, - error::client::status_error, - utils::consts::HTTP_DEFAULT_PORT, - ClientBuilder, + body::BodyConversion, client::cookie::CookieLayer, error::client::status_error, + response::ResponseExt, utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, }; - use crate::response::ResponseExt; #[derive(Deserialize)] struct HttpBinResponse { @@ -1017,7 +1010,7 @@ mod client_tests { &self, cx: &mut Cx, req: Req, - ) -> impl Future> + Send { + ) -> impl Future> + Send { self.inner.call(cx, req) } } @@ -1295,21 +1288,28 @@ mod client_tests { #[tokio::test] async fn cookie_store() { - let mut builder = Client::builder() - .layer_inner(CookieLayer::new(Default::default())); + let mut builder = Client::builder().layer_inner(CookieLayer::new(Default::default())); builder.host("httpbin.org"); let client = builder.build(); // test server add cookie - let resp = client.get("http://httpbin.org/cookies/set?key=value").send().await.unwrap(); + let resp = client + .get("http://httpbin.org/cookies/set?key=value") + .send() + .await + .unwrap(); let cookies = resp.cookies().collect::>(); assert_eq!(cookies[0].name(), "key"); assert_eq!(cookies[0].value(), "value"); // test server delete cookie - _ = client.get("http://httpbin.org/cookies/delete?key").send().await.unwrap(); + _ = client + .get("http://httpbin.org/cookies/delete?key") + .send() + .await + .unwrap(); let resp = client.get(HTTPBIN_GET).send().await.unwrap(); let cookies = resp.cookies().collect::>(); assert_eq!(cookies.len(), 0) From c7d9017fd5cd0a7429a75f51fc2c812b33e8c7b2 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:35:17 +0800 Subject: [PATCH 23/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 10 ++++----- volo-http/src/utils/cookie/store.rs | 34 +++++++++++++++++++---------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index 946fe317..861a16b2 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -80,10 +80,10 @@ pub struct CookieLayer { } impl CookieLayer { - /// Create a new [`CookieLayer`] with the given [`CookieStore`]. + /// Create a new [`CookieLayer`] with the given [` CookieStore`](cookie_store::CookieStore). /// - /// It will set cookies from the [`CookieStore`] into the request header before sending the - /// request, + /// It will set cookies from the [`CookieStore`](cookie_store::CookieStore) into the request + /// header before sending the request, /// /// and store cookies after receiving the response. /// @@ -101,9 +101,9 @@ impl CookieLayer { /// .layer_inner(CookieLayer::new(Default::default())) /// .build(); /// ``` - pub fn new(cookie_store: CookieStore) -> Self { + pub fn new(cookie_store: cookie_store::CookieStore) -> Self { Self { - cookie_store: RwLock::new(cookie_store), + cookie_store: RwLock::new(CookieStore::new(cookie_store)), } } } diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index 142ab30c..fe228299 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -10,6 +10,12 @@ pub struct CookieStore { } impl CookieStore { + pub fn new(cookie_store: cookie_store::CookieStore) -> Self { + Self { + inner: cookie_store, + } + } + pub fn add_cookie_header(&self, headers: &mut HeaderMap, request_url: &url::Url) { if let Some(header_value) = self.cookies(request_url) { headers.insert(header::COOKIE, header_value); @@ -31,18 +37,22 @@ impl CookieStore { /// Get [`HeaderValue`] from the cookie store pub fn cookies(&self, request_url: &url::Url) -> Option { - let s = self - .inner - .get_request_values(request_url) - .map(|(name, value)| { - let mut s = String::with_capacity(name.len() + value.len() + 1); - s.push_str(name); - s.push('='); - s.push_str(value); - s - }) - .collect::>() - .join("; "); + let mut cookie_iter = self.inner.get_request_values(request_url); + + let mut size = 0; + + for (key, value) in cookie_iter.by_ref() { + size += key.len() + value.len() + 3; + } + + let mut s = String::with_capacity(size); + + for (name, value) in cookie_iter { + s.push_str(name); + s.push('='); + s.push_str(value); + s.push_str("; "); + } if s.is_empty() { return None; From 4e19070811e6c44ff64575f85ee6ee61274fe0f0 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:39:29 +0800 Subject: [PATCH 24/34] feat(http): add cookie for client --- volo-http/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index f21a4927..2be1b485 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -58,6 +58,7 @@ tokio = { workspace = true, features = [ ] } tokio-util = { workspace = true, features = ["io"] } tracing.workspace = true +url.workspace = true # =====optional===== multer = { workspace = true, optional = true } @@ -76,7 +77,6 @@ tokio-native-tls = { workspace = true, optional = true } # cookie support cookie = { workspace = true, optional = true, features = ["percent-encode"] } cookie_store = { workspace = true, optional = true } -url = { workspace = true, optional = true } # serde and form, query, json serde = { workspace = true, optional = true } @@ -89,7 +89,6 @@ libc.workspace = true serde = { workspace = true, features = ["derive"] } reqwest = { workspace = true, features = ["multipart"] } tokio-test.workspace = true -url.workspace = true [features] default = [] @@ -111,7 +110,7 @@ rustls = ["__tls", "dep:tokio-rustls", "volo/rustls"] native-tls = ["__tls", "dep:tokio-native-tls", "volo/native-tls"] native-tls-vendored = ["native-tls", "volo/native-tls-vendored"] -cookie = ["dep:cookie", "dep:cookie_store", "dep:url"] +cookie = ["dep:cookie", "dep:cookie_store"] __serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx` query = ["__serde", "dep:serde_urlencoded"] From c73efcc95bfe856cb60e30e0c37141f41c09062d Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:46:24 +0800 Subject: [PATCH 25/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 4786c842..896224ad 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -964,9 +964,11 @@ mod client_tests { dns::{parse_target, DnsResolver}, get, Client, DefaultClient, Target, }; + #[cfg(feature = "cookie")] + use crate::client::cookie::CookieLayer; use crate::{ - body::BodyConversion, client::cookie::CookieLayer, error::client::status_error, - response::ResponseExt, utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, + body::BodyConversion, error::client::status_error, response::ResponseExt, + utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, }; #[derive(Deserialize)] @@ -1286,6 +1288,7 @@ mod client_tests { assert!(resp.is_ok()); } + #[cfg(feature = "cookie")] #[tokio::test] async fn cookie_store() { let mut builder = Client::builder().layer_inner(CookieLayer::new(Default::default())); From 9ba3062b28c60a3bf2dc110a15fa35b09daafddf Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:46:55 +0800 Subject: [PATCH 26/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 896224ad..94916a7b 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -964,12 +964,12 @@ mod client_tests { dns::{parse_target, DnsResolver}, get, Client, DefaultClient, Target, }; - #[cfg(feature = "cookie")] - use crate::client::cookie::CookieLayer; use crate::{ - body::BodyConversion, error::client::status_error, response::ResponseExt, - utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, + body::BodyConversion, error::client::status_error, utils::consts::HTTP_DEFAULT_PORT, + ClientBuilder, }; + #[cfg(feature = "cookie")] + use crate::{client::cookie::CookieLayer, response::ResponseExt}; #[derive(Deserialize)] struct HttpBinResponse { From 0b3d1e8626bac4f16ed64349d76e4f6d9c10eabf Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 31 Oct 2024 15:48:20 +0800 Subject: [PATCH 27/34] feat(http): add cookie for client --- volo-http/src/utils/cookie/store.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index fe228299..603edb83 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -45,6 +45,10 @@ impl CookieStore { size += key.len() + value.len() + 3; } + if size == 0 { + return None; + } + let mut s = String::with_capacity(size); for (name, value) in cookie_iter { @@ -54,10 +58,6 @@ impl CookieStore { s.push_str("; "); } - if s.is_empty() { - return None; - } - HeaderValue::from_maybe_shared(Bytes::from(s)).ok() } } From e49ab79049ed57e0440012f6a99e17125e1b3f51 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 1 Nov 2024 15:27:08 +0800 Subject: [PATCH 28/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 16 ++++++------ volo-http/src/client/mod.rs | 39 ++++++++++++++++++++++++++--- volo-http/src/request.rs | 10 +++++++- volo-http/src/utils/cookie/store.rs | 8 +++--- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index 861a16b2..5b82ae12 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -5,7 +5,7 @@ //! See [`CookieLayer`] for more details. use motore::{layer::Layer, Service}; -use parking_lot::RwLock; +use tokio::sync::Mutex; use crate::{ context::ClientContext, @@ -20,11 +20,11 @@ use crate::{ /// See [`CookieLayer`] for more details. pub struct CookieService { inner: S, - cookie_store: RwLock, + cookie_store: Mutex, } impl CookieService { - fn new(inner: S, cookie_store: RwLock) -> Self { + fn new(inner: S, cookie_store: Mutex) -> Self { Self { inner, cookie_store, @@ -54,7 +54,8 @@ where let (mut parts, body) = req.into_parts(); if parts.headers.get(http::header::COOKIE).is_none() { self.cookie_store - .read() + .lock() + .await .add_cookie_header(&mut parts.headers, url); } req = ClientRequest::from_parts(parts, body); @@ -64,7 +65,8 @@ where if let Some(url) = &url { self.cookie_store - .write() + .lock() + .await .store_response_headers(resp.headers(), url); } @@ -76,7 +78,7 @@ where /// /// See [`CookieLayer::new`] for more details. pub struct CookieLayer { - cookie_store: RwLock, + cookie_store: Mutex, } impl CookieLayer { @@ -103,7 +105,7 @@ impl CookieLayer { /// ``` pub fn new(cookie_store: cookie_store::CookieStore) -> Self { Self { - cookie_store: RwLock::new(CookieStore::new(cookie_store)), + cookie_store: Mutex::new(CookieStore::new(cookie_store)), } } } diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 94916a7b..118e5061 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -7,7 +7,7 @@ use std::{cell::RefCell, error::Error, sync::Arc, time::Duration}; use faststr::FastStr; use http::{ header::{self, HeaderMap, HeaderName, HeaderValue}, - uri::Uri, + uri::{Scheme, Uri}, Method, }; use metainfo::{MetaInfo, METAINFO}; @@ -882,6 +882,15 @@ impl Client { request.headers_mut().insert(header::HOST, host); } + #[cfg(feature = "cookie")] + { + let scheme = match target.is_https() { + true => Scheme::HTTPS, + false => Scheme::HTTP, + }; + request.extensions_mut().insert(scheme); + } + let mut cx = ClientContext::new(); cx.rpc_info_mut().caller_mut().set_service_name(caller_name); cx.rpc_info_mut().callee_mut().set_service_name(callee_name); @@ -1307,14 +1316,36 @@ mod client_tests { assert_eq!(cookies[0].name(), "key"); assert_eq!(cookies[0].value(), "value"); + #[derive(serde::Deserialize)] + struct CookieResponse { + #[serde(default)] + cookies: HashMap, + } + let resp = client + .get("http://httpbin.org/cookies") + .send() + .await + .unwrap(); + let json = resp.into_json::().await.unwrap(); + assert_eq!(json.cookies["key"], "value"); + // test server delete cookie _ = client .get("http://httpbin.org/cookies/delete?key") .send() .await .unwrap(); - let resp = client.get(HTTPBIN_GET).send().await.unwrap(); - let cookies = resp.cookies().collect::>(); - assert_eq!(cookies.len(), 0) + _ = client + .get("http://httpbin.org/delete?key") + .send() + .await + .unwrap(); + let resp = client + .get("http://httpbin.org/cookies") + .send() + .await + .unwrap(); + let json = resp.into_json::().await.unwrap(); + assert_eq!(json.cookies.len(), 0); } } diff --git a/volo-http/src/request.rs b/volo-http/src/request.rs index 14090a63..375f9b8b 100644 --- a/volo-http/src/request.rs +++ b/volo-http/src/request.rs @@ -3,6 +3,7 @@ use http::{ header::{self, HeaderMap, HeaderName}, request::{Parts, Request}, + uri::Scheme, Uri, }; @@ -38,6 +39,7 @@ mod sealed { pub trait SealedRequestPartsExt { fn headers(&self) -> &http::header::HeaderMap; fn uri(&self) -> &http::Uri; + fn extensions(&self) -> &http::Extensions; } } @@ -48,6 +50,9 @@ impl sealed::SealedRequestPartsExt for Parts { fn uri(&self) -> &Uri { &self.uri } + fn extensions(&self) -> &http::Extensions { + &self.extensions + } } impl sealed::SealedRequestPartsExt for Request { fn headers(&self) -> &HeaderMap { @@ -56,6 +61,9 @@ impl sealed::SealedRequestPartsExt for Request { fn uri(&self) -> &Uri { self.uri() } + fn extensions(&self) -> &http::Extensions { + self.extensions() + } } impl RequestPartsExt for T @@ -72,7 +80,7 @@ where let mut url_str = String::new(); - if let Some(scheme) = uri.scheme() { + if let Some(scheme) = self.extensions().get::() { url_str.push_str(scheme.as_str()); url_str.push_str("://"); } else { diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index 603edb83..674d323f 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -37,11 +37,13 @@ impl CookieStore { /// Get [`HeaderValue`] from the cookie store pub fn cookies(&self, request_url: &url::Url) -> Option { - let mut cookie_iter = self.inner.get_request_values(request_url); + let cookie_iter = self.inner.get_request_values(request_url); let mut size = 0; - for (key, value) in cookie_iter.by_ref() { + let cookies: Vec<(&str, &str)> = cookie_iter.collect::>(); + + for (key, value) in &cookies { size += key.len() + value.len() + 3; } @@ -51,7 +53,7 @@ impl CookieStore { let mut s = String::with_capacity(size); - for (name, value) in cookie_iter { + for (name, value) in cookies { s.push_str(name); s.push('='); s.push_str(value); From 2c8d22244e1ca8c03e606181d556c137cb6f9861 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 1 Nov 2024 15:46:12 +0800 Subject: [PATCH 29/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index 118e5061..f42f763d 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -1335,11 +1335,6 @@ mod client_tests { .send() .await .unwrap(); - _ = client - .get("http://httpbin.org/delete?key") - .send() - .await - .unwrap(); let resp = client .get("http://httpbin.org/cookies") .send() From 91e936cedbd7775c210d58b65240dc2f9da284fc Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 1 Nov 2024 16:19:20 +0800 Subject: [PATCH 30/34] feat(http): add cookie for client --- volo-http/src/client/cookie.rs | 14 ++++++------ volo-http/src/client/mod.rs | 29 ++++++++++++++---------- volo-http/src/response.rs | 40 ---------------------------------- 3 files changed, 25 insertions(+), 58 deletions(-) diff --git a/volo-http/src/client/cookie.rs b/volo-http/src/client/cookie.rs index 5b82ae12..3eb7a956 100644 --- a/volo-http/src/client/cookie.rs +++ b/volo-http/src/client/cookie.rs @@ -5,7 +5,7 @@ //! See [`CookieLayer`] for more details. use motore::{layer::Layer, Service}; -use tokio::sync::Mutex; +use tokio::sync::RwLock; use crate::{ context::ClientContext, @@ -20,11 +20,11 @@ use crate::{ /// See [`CookieLayer`] for more details. pub struct CookieService { inner: S, - cookie_store: Mutex, + cookie_store: RwLock, } impl CookieService { - fn new(inner: S, cookie_store: Mutex) -> Self { + fn new(inner: S, cookie_store: RwLock) -> Self { Self { inner, cookie_store, @@ -54,7 +54,7 @@ where let (mut parts, body) = req.into_parts(); if parts.headers.get(http::header::COOKIE).is_none() { self.cookie_store - .lock() + .read() .await .add_cookie_header(&mut parts.headers, url); } @@ -65,7 +65,7 @@ where if let Some(url) = &url { self.cookie_store - .lock() + .write() .await .store_response_headers(resp.headers(), url); } @@ -78,7 +78,7 @@ where /// /// See [`CookieLayer::new`] for more details. pub struct CookieLayer { - cookie_store: Mutex, + cookie_store: RwLock, } impl CookieLayer { @@ -105,7 +105,7 @@ impl CookieLayer { /// ``` pub fn new(cookie_store: cookie_store::CookieStore) -> Self { Self { - cookie_store: Mutex::new(CookieStore::new(cookie_store)), + cookie_store: RwLock::new(CookieStore::new(cookie_store)), } } } diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index f42f763d..f8285779 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -882,14 +882,11 @@ impl Client { request.headers_mut().insert(header::HOST, host); } - #[cfg(feature = "cookie")] - { - let scheme = match target.is_https() { - true => Scheme::HTTPS, - false => Scheme::HTTP, - }; - request.extensions_mut().insert(scheme); - } + let scheme = match target.is_https() { + true => Scheme::HTTPS, + false => Scheme::HTTP, + }; + request.extensions_mut().insert(scheme); let mut cx = ClientContext::new(); cx.rpc_info_mut().caller_mut().set_service_name(caller_name); @@ -960,6 +957,7 @@ where mod client_tests { use std::{collections::HashMap, future::Future}; + use cookie::Cookie; use http::{header, StatusCode}; use motore::{ layer::{Layer, Stack}, @@ -973,12 +971,12 @@ mod client_tests { dns::{parse_target, DnsResolver}, get, Client, DefaultClient, Target, }; + #[cfg(feature = "cookie")] + use crate::client::cookie::CookieLayer; use crate::{ body::BodyConversion, error::client::status_error, utils::consts::HTTP_DEFAULT_PORT, ClientBuilder, }; - #[cfg(feature = "cookie")] - use crate::{client::cookie::CookieLayer, response::ResponseExt}; #[derive(Deserialize)] struct HttpBinResponse { @@ -1312,7 +1310,16 @@ mod client_tests { .send() .await .unwrap(); - let cookies = resp.cookies().collect::>(); + let cookies = resp + .headers() + .get_all(http::header::SET_COOKIE) + .iter() + .filter_map(|value| { + std::str::from_utf8(value.as_bytes()) + .ok() + .and_then(|val| Cookie::parse(val).map(|c| c.into_owned()).ok()) + }) + .collect::>(); assert_eq!(cookies[0].name(), "key"); assert_eq!(cookies[0].value(), "value"); diff --git a/volo-http/src/response.rs b/volo-http/src/response.rs index 68e6c81b..260bd1d7 100644 --- a/volo-http/src/response.rs +++ b/volo-http/src/response.rs @@ -1,9 +1,5 @@ //! Response types for client and server. -#[cfg(feature = "cookie")] -use cookie::Cookie; -use http::Response; - /// [`Response`] with [`Body`] as default body /// /// [`Response`]: http::response::Response @@ -17,39 +13,3 @@ pub type ServerResponse = http::response::Response; /// [`Body`]: crate::body::Body #[cfg(feature = "client")] pub type ClientResponse = http::response::Response; - -/// Utilities of [`http::response::Response`]. -pub trait ResponseExt: sealed::SealedResponseExt { - /// Get all cookies from `Set-Cookie` header. - #[cfg(feature = "cookie")] - fn cookies(&self) -> impl Iterator; -} - -mod sealed { - pub trait SealedResponseExt { - fn headers(&self) -> &http::HeaderMap; - } -} - -impl sealed::SealedResponseExt for Response { - fn headers(&self) -> &http::HeaderMap { - self.headers() - } -} - -impl ResponseExt for T -where - T: sealed::SealedResponseExt, -{ - #[cfg(feature = "cookie")] - fn cookies(&self) -> impl Iterator { - self.headers() - .get_all(http::header::SET_COOKIE) - .iter() - .filter_map(|value| { - std::str::from_utf8(value.as_bytes()) - .ok() - .and_then(|val| Cookie::parse(val).map(|c| c.into_owned()).ok()) - }) - } -} From 2ac63d80fc8db9fd7b7f778d16ccd044134c4fbf Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 1 Nov 2024 16:39:48 +0800 Subject: [PATCH 31/34] feat(http): add cookie for client --- volo-http/src/utils/cookie/store.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index 674d323f..1e6ea499 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -43,21 +43,33 @@ impl CookieStore { let cookies: Vec<(&str, &str)> = cookie_iter.collect::>(); + if cookies.is_empty() { + return None; + } + for (key, value) in &cookies { - size += key.len() + value.len() + 3; + size += key.len() + value.len(); } if size == 0 { return None; } + size += 3 * cookies.len() - 2; + let mut s = String::with_capacity(size); - for (name, value) in cookies { - s.push_str(name); + let mut cookie_iter = cookies.iter(); + let first = cookie_iter.next().unwrap(); + + s.push_str(first.0); + s.push('='); + s.push_str(first.1); + + for (key, value) in cookie_iter { + s.push_str(key); s.push('='); s.push_str(value); - s.push_str("; "); } HeaderValue::from_maybe_shared(Bytes::from(s)).ok() From 3adaace2c4d1ca071afb57f07b313d382ceb8c39 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 1 Nov 2024 16:41:11 +0800 Subject: [PATCH 32/34] feat(http): add cookie for client --- volo-http/src/utils/cookie/store.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/volo-http/src/utils/cookie/store.rs b/volo-http/src/utils/cookie/store.rs index 1e6ea499..12375547 100644 --- a/volo-http/src/utils/cookie/store.rs +++ b/volo-http/src/utils/cookie/store.rs @@ -67,6 +67,7 @@ impl CookieStore { s.push_str(first.1); for (key, value) in cookie_iter { + s.push_str("; "); s.push_str(key); s.push('='); s.push_str(value); From 45630274ce73c3b0e15c3be02d65d4b6629ca07b Mon Sep 17 00:00:00 2001 From: StellarisW Date: Mon, 4 Nov 2024 14:50:08 +0800 Subject: [PATCH 33/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index f8285779..be72239a 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -957,7 +957,6 @@ where mod client_tests { use std::{collections::HashMap, future::Future}; - use cookie::Cookie; use http::{header, StatusCode}; use motore::{ layer::{Layer, Stack}, From ecc906c30d185b449bb91b11570fb50066f114ae Mon Sep 17 00:00:00 2001 From: StellarisW Date: Mon, 4 Nov 2024 14:55:11 +0800 Subject: [PATCH 34/34] feat(http): add cookie for client --- volo-http/src/client/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volo-http/src/client/mod.rs b/volo-http/src/client/mod.rs index be72239a..8e8f2c05 100644 --- a/volo-http/src/client/mod.rs +++ b/volo-http/src/client/mod.rs @@ -957,6 +957,8 @@ where mod client_tests { use std::{collections::HashMap, future::Future}; + #[cfg(feature = "cookie")] + use cookie::Cookie; use http::{header, StatusCode}; use motore::{ layer::{Layer, Stack},