Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http): add cookie feature for client #512

Merged
merged 36 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4bc8068
feat(http): add cookie feature for client
StellarisW Oct 24, 2024
c4ceb7e
feat(http): add cookie feature for client
StellarisW Oct 24, 2024
80d948b
feat(http): add multipart for server
StellarisW Oct 24, 2024
6ccaa68
feat(http): add multipart for server
StellarisW Oct 24, 2024
d75e64d
feat(http): add multipart for server
StellarisW Oct 24, 2024
709de2f
feat(http): add cookie for client
StellarisW Oct 29, 2024
66ff8d1
feat(http): add cookie for client
StellarisW Oct 29, 2024
d75b8a3
Merge branch 'main' into feat(http)/cookie_jar
StellarisW Oct 29, 2024
a05fdc3
feat(http): add cookie for client
StellarisW Oct 29, 2024
ab0cf6c
feat(http): add cookie for client
StellarisW Oct 29, 2024
c39b065
feat(http): add cookie for client
StellarisW Oct 29, 2024
8c7bc94
feat(http): add cookie for client
StellarisW Oct 29, 2024
8063073
feat(http): add cookie for client
StellarisW Oct 29, 2024
5c6f746
feat(http): add cookie for client
StellarisW Oct 29, 2024
3522e3e
feat(http): add cookie for client
StellarisW Oct 30, 2024
93a97a4
feat(http): add cookie for client
StellarisW Oct 30, 2024
5c71f99
feat(http): add cookie for client
StellarisW Oct 30, 2024
46e52af
feat(http): add cookie for client
StellarisW Oct 31, 2024
6c9b8ad
feat(http): add cookie for client
StellarisW Oct 31, 2024
1f2e378
feat(http): add cookie for client
StellarisW Oct 31, 2024
4c4c5c9
feat(http): add cookie for client
StellarisW Oct 31, 2024
fc10524
feat(http): add cookie for client
StellarisW Oct 31, 2024
58dece8
feat(http): add cookie for client
StellarisW Oct 31, 2024
c7d9017
feat(http): add cookie for client
StellarisW Oct 31, 2024
4e19070
feat(http): add cookie for client
StellarisW Oct 31, 2024
c73efcc
feat(http): add cookie for client
StellarisW Oct 31, 2024
9ba3062
feat(http): add cookie for client
StellarisW Oct 31, 2024
0b3d1e8
feat(http): add cookie for client
StellarisW Oct 31, 2024
e49ab79
feat(http): add cookie for client
StellarisW Nov 1, 2024
2c8d222
feat(http): add cookie for client
StellarisW Nov 1, 2024
91e936c
feat(http): add cookie for client
StellarisW Nov 1, 2024
2ac63d8
feat(http): add cookie for client
StellarisW Nov 1, 2024
3adaace
feat(http): add cookie for client
StellarisW Nov 1, 2024
5e173f0
Merge branch 'main' into feat(http)/cookie_jar
StellarisW Nov 4, 2024
4563027
feat(http): add cookie for client
StellarisW Nov 4, 2024
ecc906c
feat(http): add cookie for client
StellarisW Nov 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
5 changes: 3 additions & 2 deletions volo-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -75,6 +76,7 @@ tokio-native-tls = { workspace = true, optional = true }

# cookie support
cookie = { workspace = true, optional = true, features = ["percent-encode"] }
cookie_store = { workspace = true, optional = true }

# serde and form, query, json
serde = { workspace = true, optional = true }
Expand All @@ -87,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 = []
Expand All @@ -109,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"]
cookie = ["dep:cookie", "dep:cookie_store"]

__serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx`
query = ["__serde", "dep:serde_urlencoded"]
Expand Down
119 changes: 119 additions & 0 deletions volo-http/src/client/cookie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//! Cookie implementation
//!
//! This module provides [`CookieLayer`] for extracting and setting cookies.
//!
//! See [`CookieLayer`] for more details.

use motore::{layer::Layer, Service};
use tokio::sync::RwLock;

use crate::{
context::ClientContext,
error::ClientError,
request::{ClientRequest, RequestPartsExt},
response::ClientResponse,
utils::cookie::CookieStore,
};

/// [`CookieLayer`] generated [`Service`]
///
/// See [`CookieLayer`] for more details.
pub struct CookieService<S> {
inner: S,
cookie_store: RwLock<CookieStore>,
}

impl<S> CookieService<S> {
fn new(inner: S, cookie_store: RwLock<CookieStore>) -> Self {
Self {
inner,
cookie_store,
}
}
}

impl<S, B> Service<ClientContext, ClientRequest<B>> for CookieService<S>
where
S: Service<ClientContext, ClientRequest<B>, 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<B>,
) -> Result<Self::Response, Self::Error> {
let url = req.url();

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()
.await
.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()
.await
.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<CookieStore>,
}

impl CookieLayer {
/// Create a new [`CookieLayer`] with the given [` CookieStore`](cookie_store::CookieStore).
///
/// 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.
///
/// 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.
///
/// # 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: cookie_store::CookieStore) -> Self {
yukiiiteru marked this conversation as resolved.
Show resolved Hide resolved
Self {
cookie_store: RwLock::new(CookieStore::new(cookie_store)),
}
}
}

impl<S> Layer<S> for CookieLayer {
type Service = CookieService<S>;

fn layer(self, inner: S) -> Self::Service {
CookieService::new(inner, self.cookie_store)
}
}
70 changes: 68 additions & 2 deletions volo-http/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -48,6 +48,8 @@ use crate::{
};

pub mod callopt;
#[cfg(feature = "cookie")]
pub mod cookie;
pub mod dns;
pub mod loadbalance;
mod meta;
Expand Down Expand Up @@ -880,6 +882,12 @@ impl<S> Client<S> {
request.headers_mut().insert(header::HOST, host);
}

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);
Expand Down Expand Up @@ -947,9 +955,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},
Expand All @@ -963,6 +971,8 @@ 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,
Expand Down Expand Up @@ -1284,4 +1294,60 @@ mod client_tests {
let resp = client.get(HTTPBIN_GET).send().await;
assert!(resp.is_ok());
}

#[cfg(feature = "cookie")]
#[tokio::test]
async fn cookie_store() {
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 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::<Vec<_>>();
assert_eq!(cookies[0].name(), "key");
assert_eq!(cookies[0].value(), "value");

#[derive(serde::Deserialize)]
struct CookieResponse {
#[serde(default)]
cookies: HashMap<String, String>,
}
let resp = client
.get("http://httpbin.org/cookies")
.send()
.await
.unwrap();
let json = resp.into_json::<CookieResponse>().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("http://httpbin.org/cookies")
.send()
.await
.unwrap();
let json = resp.into_json::<CookieResponse>().await.unwrap();
assert_eq!(json.cookies.len(), 0);
}
}
Loading
Loading