Skip to content

Commit

Permalink
fix(volo-http): add mime parsing in server, do not always prefer ipv4…
Browse files Browse the repository at this point in the history
… 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 <[email protected]>

---------

Signed-off-by: Yu Li <[email protected]>
  • Loading branch information
yukiiiteru authored Nov 28, 2024
1 parent 8b0f0cc commit 661346b
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 54 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

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

30 changes: 19 additions & 11 deletions volo-http/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = [
Expand All @@ -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 }
Expand Down Expand Up @@ -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"]
Expand Down
24 changes: 21 additions & 3 deletions volo-http/src/client/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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)
}
}

Expand Down
56 changes: 17 additions & 39 deletions volo-http/src/server/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ where
parts: Parts,
body: B,
) -> Result<Self, Self::Rejection> {
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());
}

Expand All @@ -555,7 +555,7 @@ where
parts: Parts,
body: B,
) -> Result<Self, Self::Rejection> {
if !json_content_type(&parts.headers) {
if !content_type_matches(&parts.headers, mime::APPLICATION, mime::JSON) {
return Err(crate::error::server::invalid_content_type());
}

Expand All @@ -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::<mime::Mime>() {
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)]
Expand Down

0 comments on commit 661346b

Please sign in to comment.