Skip to content

Commit 661346b

Browse files
authored
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 <[email protected]> --------- Signed-off-by: Yu Li <[email protected]>
1 parent 8b0f0cc commit 661346b

File tree

4 files changed

+58
-54
lines changed

4 files changed

+58
-54
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

volo-http/Cargo.toml

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "volo-http"
3-
version = "0.3.0-rc.3"
3+
version = "0.3.0"
44
edition.workspace = true
55
homepage.workspace = true
66
repository.workspace = true
@@ -22,29 +22,22 @@ maintenance = { status = "actively-developed" }
2222
volo = { version = "0.10", path = "../volo" }
2323

2424
ahash.workspace = true
25-
async-broadcast.workspace = true
2625
bytes.workspace = true
27-
chrono.workspace = true
2826
faststr.workspace = true
2927
futures.workspace = true
3028
futures-util.workspace = true
31-
hickory-resolver.workspace = true
3229
http.workspace = true
3330
http-body.workspace = true
3431
http-body-util.workspace = true
3532
hyper.workspace = true
3633
hyper-util = { workspace = true, features = ["tokio"] }
37-
ipnet.workspace = true
3834
itoa.workspace = true
39-
memchr.workspace = true
4035
metainfo.workspace = true
4136
mime.workspace = true
42-
mime_guess.workspace = true
4337
motore.workspace = true
4438
parking_lot.workspace = true
4539
paste.workspace = true
4640
pin-project.workspace = true
47-
scopeguard.workspace = true
4841
simdutf8.workspace = true
4942
thiserror.workspace = true
5043
tokio = { workspace = true, features = [
@@ -62,7 +55,16 @@ url.workspace = true
6255
# =====optional=====
6356

6457
# server optional
65-
matchit = { workspace = true, optional = true }
58+
ipnet = { workspace = true, optional = true } # client ip
59+
matchit = { workspace = true, optional = true } # route matching
60+
memchr = { workspace = true, optional = true } # sse
61+
scopeguard = { workspace = true, optional = true } # defer
62+
63+
# client optional
64+
async-broadcast = { workspace = true, optional = true } # service discover
65+
chrono = { workspace = true, optional = true } # stat
66+
hickory-resolver = { workspace = true, optional = true } # dns resolver
67+
mime_guess = { workspace = true, optional = true }
6668

6769
# serde and form, query, json
6870
serde = { workspace = true, optional = true }
@@ -106,8 +108,14 @@ full = [
106108

107109
http1 = ["hyper/http1", "hyper-util/http1"]
108110

109-
client = ["http1", "hyper/client"] # client core
110-
server = ["http1", "hyper-util/server", "dep:matchit"] # server core
111+
client = [
112+
"http1", "hyper/client",
113+
"dep:async-broadcast", "dep:chrono", "dep:hickory-resolver",
114+
] # client core
115+
server = [
116+
"http1", "hyper-util/server",
117+
"dep:ipnet", "dep:matchit", "dep:memchr", "dep:scopeguard", "dep:mime_guess",
118+
] # server core
111119

112120
__serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx`
113121
query = ["__serde", "dep:serde_urlencoded"]

volo-http/src/client/dns.rs

+21-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{net::SocketAddr, ops::Deref, sync::Arc};
77
use async_broadcast::Receiver;
88
use faststr::FastStr;
99
use hickory_resolver::{
10-
config::{ResolverConfig, ResolverOpts},
10+
config::{LookupIpStrategy, ResolverConfig, ResolverOpts},
1111
AsyncResolver, TokioAsyncResolver,
1212
};
1313
use volo::{
@@ -64,9 +64,27 @@ impl DnsResolver {
6464

6565
impl Default for DnsResolver {
6666
fn default() -> Self {
67-
Self {
68-
resolver: AsyncResolver::tokio_from_system_conf().expect("failed to init dns resolver"),
67+
let (conf, mut opts) = hickory_resolver::system_conf::read_system_conf()
68+
.expect("[Volo-HTTP] DnsResolver: failed to parse dns config");
69+
if conf
70+
.name_servers()
71+
.first()
72+
.expect("[Volo-HTTP] DnsResolver: no nameserver found")
73+
.socket_addr
74+
.is_ipv6()
75+
{
76+
// The default `LookupIpStrategy` is always `Ipv4thenIpv6`, it may not work in an IPv6
77+
// only environment.
78+
//
79+
// Here we trust the system configuration and check its first name server.
80+
//
81+
// If the first nameserver is an IPv4 address, we keep the default configuration.
82+
//
83+
// If the first nameserver is an IPv6 address, we need to update the policy to prefer
84+
// IPv6 addresses.
85+
opts.ip_strategy = LookupIpStrategy::Ipv6thenIpv4;
6986
}
87+
Self::new(conf, opts)
7088
}
7189
}
7290

volo-http/src/server/extract.rs

+17-39
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ where
528528
parts: Parts,
529529
body: B,
530530
) -> Result<Self, Self::Rejection> {
531-
if !content_type_eq(&parts.headers, mime::APPLICATION_WWW_FORM_URLENCODED) {
531+
if !content_type_matches(&parts.headers, mime::APPLICATION, mime::WWW_FORM_URLENCODED) {
532532
return Err(crate::error::server::invalid_content_type());
533533
}
534534

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

@@ -580,48 +580,26 @@ fn get_header_value(map: &HeaderMap, key: HeaderName) -> Option<&str> {
580580
map.get(key)?.to_str().ok()
581581
}
582582

583-
#[cfg(feature = "form")]
584-
fn content_type_eq(map: &HeaderMap, val: mime::Mime) -> bool {
585-
let Some(ty) = get_header_value(map, header::CONTENT_TYPE) else {
586-
return false;
587-
};
588-
ty == val.essence_str()
589-
}
583+
#[cfg(any(feature = "form", feature = "json"))]
584+
fn content_type_matches(
585+
headers: &HeaderMap,
586+
ty: mime::Name<'static>,
587+
subtype: mime::Name<'static>,
588+
) -> bool {
589+
use std::str::FromStr;
590590

591-
#[cfg(feature = "json")]
592-
fn json_content_type(headers: &HeaderMap) -> bool {
593-
let content_type = match headers.get(header::CONTENT_TYPE) {
594-
Some(content_type) => content_type,
595-
None => {
596-
return false;
597-
}
591+
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
592+
return false;
598593
};
599-
600-
let content_type = match content_type.to_str() {
601-
Ok(s) => s,
602-
Err(_) => {
603-
return false;
604-
}
594+
let Ok(content_type) = content_type.to_str() else {
595+
return false;
605596
};
606-
607-
let mime_type = match content_type.parse::<mime::Mime>() {
608-
Ok(mime_type) => mime_type,
609-
Err(_) => {
610-
return false;
611-
}
597+
let Ok(mime) = mime::Mime::from_str(content_type) else {
598+
return false;
612599
};
613600

614-
// `application/json` or `application/json+foo`
615-
if mime_type.type_() == mime::APPLICATION && mime_type.subtype() == mime::JSON {
616-
return true;
617-
}
618-
619-
// `application/foo+json`
620-
if mime_type.suffix() == Some(mime::JSON) {
621-
return true;
622-
}
623-
624-
false
601+
// `text/xml` or `image/svg+xml`
602+
(mime.type_() == ty && mime.subtype() == subtype) || mime.suffix() == Some(subtype)
625603
}
626604

627605
#[cfg(test)]

0 commit comments

Comments
 (0)