From 661346b28acb9ef7dca48429cce6ba5c39e61d0a Mon Sep 17 00:00:00 2001 From: Yukiteru Date: Thu, 28 Nov 2024 19:31:18 +0800 Subject: [PATCH] fix(volo-http): add mime parsing in server, do not always prefer ipv4 in client dns (#538) * chore(volo-http): mark some dependencies as optional * fix(volo-http): check content type through parsing content type In the previous implementation, the `Form` extractor directly compared `Content-Type` and rejected the form if `Content-Type` was not `application/x-www-form-urlencoded`. But sometimes `Content-Type` could be `application/x-www-form-urlencoded; charset=utf-8`, which is actually a valid mime for the form, but we incorrectly rejected it. This commit makes the current implementation check `Content-Type` by parsing instead of directly comparing the string. * fix(volo-http): prefer ipv6 addr in dns resolver when dns addr is ipv6 We use the `hickory_resolver` crate to resolve domain names, but we found that it always prefers IPv4 addresses, which doesn't work if the client is running in an IPv6 only environment. This commit fixes this by checking the first name server, if the address is an IPv4 address we keep preferring IPv4 addresses, if it is an IPv6 address we set the resolver to prefer IPv6 addresses. * chore(volo-http): bump Volo-HTTP to 0.3.0 Signed-off-by: Yu Li --------- Signed-off-by: Yu Li --- Cargo.lock | 2 +- volo-http/Cargo.toml | 30 +++++++++++------- volo-http/src/client/dns.rs | 24 ++++++++++++-- volo-http/src/server/extract.rs | 56 ++++++++++----------------------- 4 files changed, 58 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e0d6ff4..fa437e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4187,7 +4187,7 @@ dependencies = [ [[package]] name = "volo-http" -version = "0.3.0-rc.3" +version = "0.3.0" dependencies = [ "ahash", "async-broadcast", diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index eaac5baf..8e827d37 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "volo-http" -version = "0.3.0-rc.3" +version = "0.3.0" edition.workspace = true homepage.workspace = true repository.workspace = true @@ -22,29 +22,22 @@ maintenance = { status = "actively-developed" } volo = { version = "0.10", path = "../volo" } ahash.workspace = true -async-broadcast.workspace = true bytes.workspace = true -chrono.workspace = true faststr.workspace = true futures.workspace = true futures-util.workspace = true -hickory-resolver.workspace = true http.workspace = true http-body.workspace = true http-body-util.workspace = true hyper.workspace = true hyper-util = { workspace = true, features = ["tokio"] } -ipnet.workspace = true itoa.workspace = true -memchr.workspace = true metainfo.workspace = true mime.workspace = true -mime_guess.workspace = true motore.workspace = true parking_lot.workspace = true paste.workspace = true pin-project.workspace = true -scopeguard.workspace = true simdutf8.workspace = true thiserror.workspace = true tokio = { workspace = true, features = [ @@ -62,7 +55,16 @@ url.workspace = true # =====optional===== # server optional -matchit = { workspace = true, optional = true } +ipnet = { workspace = true, optional = true } # client ip +matchit = { workspace = true, optional = true } # route matching +memchr = { workspace = true, optional = true } # sse +scopeguard = { workspace = true, optional = true } # defer + +# client optional +async-broadcast = { workspace = true, optional = true } # service discover +chrono = { workspace = true, optional = true } # stat +hickory-resolver = { workspace = true, optional = true } # dns resolver +mime_guess = { workspace = true, optional = true } # serde and form, query, json serde = { workspace = true, optional = true } @@ -106,8 +108,14 @@ full = [ http1 = ["hyper/http1", "hyper-util/http1"] -client = ["http1", "hyper/client"] # client core -server = ["http1", "hyper-util/server", "dep:matchit"] # server core +client = [ + "http1", "hyper/client", + "dep:async-broadcast", "dep:chrono", "dep:hickory-resolver", +] # client core +server = [ + "http1", "hyper-util/server", + "dep:ipnet", "dep:matchit", "dep:memchr", "dep:scopeguard", "dep:mime_guess", +] # server core __serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx` query = ["__serde", "dep:serde_urlencoded"] diff --git a/volo-http/src/client/dns.rs b/volo-http/src/client/dns.rs index 7ca16bc6..9fd6c649 100644 --- a/volo-http/src/client/dns.rs +++ b/volo-http/src/client/dns.rs @@ -7,7 +7,7 @@ use std::{net::SocketAddr, ops::Deref, sync::Arc}; use async_broadcast::Receiver; use faststr::FastStr; use hickory_resolver::{ - config::{ResolverConfig, ResolverOpts}, + config::{LookupIpStrategy, ResolverConfig, ResolverOpts}, AsyncResolver, TokioAsyncResolver, }; use volo::{ @@ -64,9 +64,27 @@ impl DnsResolver { impl Default for DnsResolver { fn default() -> Self { - Self { - resolver: AsyncResolver::tokio_from_system_conf().expect("failed to init dns resolver"), + let (conf, mut opts) = hickory_resolver::system_conf::read_system_conf() + .expect("[Volo-HTTP] DnsResolver: failed to parse dns config"); + if conf + .name_servers() + .first() + .expect("[Volo-HTTP] DnsResolver: no nameserver found") + .socket_addr + .is_ipv6() + { + // The default `LookupIpStrategy` is always `Ipv4thenIpv6`, it may not work in an IPv6 + // only environment. + // + // Here we trust the system configuration and check its first name server. + // + // If the first nameserver is an IPv4 address, we keep the default configuration. + // + // If the first nameserver is an IPv6 address, we need to update the policy to prefer + // IPv6 addresses. + opts.ip_strategy = LookupIpStrategy::Ipv6thenIpv4; } + Self::new(conf, opts) } } diff --git a/volo-http/src/server/extract.rs b/volo-http/src/server/extract.rs index 315aaae3..3aa81c89 100644 --- a/volo-http/src/server/extract.rs +++ b/volo-http/src/server/extract.rs @@ -528,7 +528,7 @@ where parts: Parts, body: B, ) -> Result { - if !content_type_eq(&parts.headers, mime::APPLICATION_WWW_FORM_URLENCODED) { + if !content_type_matches(&parts.headers, mime::APPLICATION, mime::WWW_FORM_URLENCODED) { return Err(crate::error::server::invalid_content_type()); } @@ -555,7 +555,7 @@ where parts: Parts, body: B, ) -> Result { - if !json_content_type(&parts.headers) { + if !content_type_matches(&parts.headers, mime::APPLICATION, mime::JSON) { return Err(crate::error::server::invalid_content_type()); } @@ -580,48 +580,26 @@ fn get_header_value(map: &HeaderMap, key: HeaderName) -> Option<&str> { map.get(key)?.to_str().ok() } -#[cfg(feature = "form")] -fn content_type_eq(map: &HeaderMap, val: mime::Mime) -> bool { - let Some(ty) = get_header_value(map, header::CONTENT_TYPE) else { - return false; - }; - ty == val.essence_str() -} +#[cfg(any(feature = "form", feature = "json"))] +fn content_type_matches( + headers: &HeaderMap, + ty: mime::Name<'static>, + subtype: mime::Name<'static>, +) -> bool { + use std::str::FromStr; -#[cfg(feature = "json")] -fn json_content_type(headers: &HeaderMap) -> bool { - let content_type = match headers.get(header::CONTENT_TYPE) { - Some(content_type) => content_type, - None => { - return false; - } + let Some(content_type) = headers.get(header::CONTENT_TYPE) else { + return false; }; - - let content_type = match content_type.to_str() { - Ok(s) => s, - Err(_) => { - return false; - } + let Ok(content_type) = content_type.to_str() else { + return false; }; - - let mime_type = match content_type.parse::() { - Ok(mime_type) => mime_type, - Err(_) => { - return false; - } + let Ok(mime) = mime::Mime::from_str(content_type) else { + return false; }; - // `application/json` or `application/json+foo` - if mime_type.type_() == mime::APPLICATION && mime_type.subtype() == mime::JSON { - return true; - } - - // `application/foo+json` - if mime_type.suffix() == Some(mime::JSON) { - return true; - } - - false + // `text/xml` or `image/svg+xml` + (mime.type_() == ty && mime.subtype() == subtype) || mime.suffix() == Some(subtype) } #[cfg(test)]