use std::str::FromStr;
use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
use http::{header, uri, Request, Uri};
use tracing::debug;
use crate::error::ProtoError;
use crate::http::error::Result;
use crate::http::Version;
#[allow(clippy::field_reassign_with_default)] pub fn new(version: Version, name_server_name: &str, message_len: usize) -> Result<Request<()>> {
let mut parts = uri::Parts::default();
parts.path_and_query = Some(uri::PathAndQuery::from_static(crate::http::DNS_QUERY_PATH));
parts.scheme = Some(uri::Scheme::HTTPS);
parts.authority = Some(
uri::Authority::from_str(name_server_name)
.map_err(|e| ProtoError::from(format!("invalid authority: {e}")))?,
);
let url =
Uri::from_parts(parts).map_err(|e| ProtoError::from(format!("uri parse error: {e}")))?;
let request = Request::builder()
.method("POST")
.uri(url)
.version(version.to_http())
.header(CONTENT_TYPE, crate::http::MIME_APPLICATION_DNS)
.header(ACCEPT, crate::http::MIME_APPLICATION_DNS)
.header(CONTENT_LENGTH, message_len)
.body(())
.map_err(|e| ProtoError::from(format!("http stream errored: {e}")))?;
Ok(request)
}
pub fn verify<T>(version: Version, name_server: Option<&str>, request: &Request<T>) -> Result<()> {
let uri = request.uri();
if uri.path() != crate::http::DNS_QUERY_PATH {
return Err(format!(
"bad path: {}, expected: {}",
uri.path(),
crate::http::DNS_QUERY_PATH
)
.into());
}
if Some(&uri::Scheme::HTTPS) != uri.scheme() {
return Err("must be HTTPS scheme".into());
}
if let Some(name_server) = name_server {
if let Some(authority) = uri.authority() {
if authority.host() != name_server {
return Err("incorrect authority".into());
}
} else {
return Err("no authority in HTTPS request".into());
}
}
match request.headers().get(CONTENT_TYPE).map(|v| v.to_str()) {
Some(Ok(ctype)) if ctype == crate::http::MIME_APPLICATION_DNS => {}
_ => return Err("unsupported content type".into()),
};
match request.headers().get(ACCEPT).map(|v| v.to_str()) {
Some(Ok(ctype)) => {
let mut found = false;
for mime_and_quality in ctype.split(',') {
let mut parts = mime_and_quality.splitn(2, ';');
match parts.next() {
Some(mime) if mime.trim() == crate::http::MIME_APPLICATION_DNS => {
found = true;
break;
}
Some(mime) if mime.trim() == "application/*" => {
found = true;
break;
}
_ => continue,
}
}
if !found {
return Err("does not accept content type".into());
}
}
Some(Err(e)) => return Err(e.into()),
None => return Err("Accept is unspecified".into()),
};
if request.version() != version.to_http() {
let message = match version {
#[cfg(feature = "dns-over-https")]
Version::Http2 => "only HTTP/2 supported",
#[cfg(feature = "dns-over-h3")]
Version::Http3 => "only HTTP/3 supported",
};
return Err(message.into());
}
debug!(
"verified request from: {}",
request
.headers()
.get(header::USER_AGENT)
.map(|h| h.to_str().unwrap_or("bad user agent"))
.unwrap_or("unknown user agent")
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "dns-over-https")]
fn test_new_verify_h2() {
let request = new(Version::Http2, "ns.example.com", 512).expect("error converting to http");
assert!(verify(Version::Http2, Some("ns.example.com"), &request).is_ok());
}
#[test]
#[cfg(feature = "dns-over-h3")]
fn test_new_verify_h3() {
let request = new(Version::Http3, "ns.example.com", 512).expect("error converting to http");
assert!(verify(Version::Http3, Some("ns.example.com"), &request).is_ok());
}
}