hickory_proto/http/
request.rs1use core::str::FromStr;
11
12use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
13use http::{Request, Uri, header, uri};
14use tracing::debug;
15
16use crate::error::ProtoError;
17use crate::http::Version;
18use crate::http::error::Result;
19
20#[allow(clippy::field_reassign_with_default)] pub fn new(
33 version: Version,
34 name_server_name: &str,
35 query_path: &str,
36 message_len: usize,
37) -> Result<Request<()>> {
38 let mut parts = uri::Parts::default();
51 parts.path_and_query = Some(
52 uri::PathAndQuery::try_from(query_path)
53 .map_err(|e| ProtoError::from(format!("invalid DoH path: {e}")))?,
54 );
55 parts.scheme = Some(uri::Scheme::HTTPS);
56 parts.authority = Some(
57 uri::Authority::from_str(name_server_name)
58 .map_err(|e| ProtoError::from(format!("invalid authority: {e}")))?,
59 );
60
61 let url =
62 Uri::from_parts(parts).map_err(|e| ProtoError::from(format!("uri parse error: {e}")))?;
63
64 let request = Request::builder()
66 .method("POST")
67 .uri(url)
68 .version(version.to_http())
69 .header(CONTENT_TYPE, crate::http::MIME_APPLICATION_DNS)
70 .header(ACCEPT, crate::http::MIME_APPLICATION_DNS)
71 .header(CONTENT_LENGTH, message_len)
72 .body(())
73 .map_err(|e| ProtoError::from(format!("http stream errored: {e}")))?;
74
75 Ok(request)
76}
77
78pub fn verify<T>(
80 version: Version,
81 name_server: Option<&str>,
82 query_path: &str,
83 request: &Request<T>,
84) -> Result<()> {
85 let uri = request.uri();
87
88 if uri.path() != query_path {
90 return Err(format!("bad path: {}, expected: {}", uri.path(), query_path,).into());
91 }
92
93 if Some(&uri::Scheme::HTTPS) != uri.scheme() {
95 return Err("must be HTTPS scheme".into());
96 }
97
98 if let Some(name_server) = name_server {
100 if let Some(authority) = uri.authority() {
101 if authority.host() != name_server {
102 return Err("incorrect authority".into());
103 }
104 } else {
105 return Err("no authority in HTTPS request".into());
106 }
107 }
108
109 match request.headers().get(CONTENT_TYPE).map(|v| v.to_str()) {
111 Some(Ok(ctype)) if ctype == crate::http::MIME_APPLICATION_DNS => {}
112 _ => return Err("unsupported content type".into()),
113 };
114
115 match request.headers().get(ACCEPT).map(|v| v.to_str()) {
117 Some(Ok(ctype)) => {
118 let mut found = false;
119 for mime_and_quality in ctype.split(',') {
120 let mut parts = mime_and_quality.splitn(2, ';');
121 match parts.next() {
122 Some(mime) if mime.trim() == crate::http::MIME_APPLICATION_DNS => {
123 found = true;
124 break;
125 }
126 Some(mime) if mime.trim() == "application/*" => {
127 found = true;
128 break;
129 }
130 _ => continue,
131 }
132 }
133
134 if !found {
135 return Err("does not accept content type".into());
136 }
137 }
138 Some(Err(e)) => return Err(e.into()),
139 None => return Err("Accept is unspecified".into()),
140 };
141
142 if request.version() != version.to_http() {
143 let message = match version {
144 #[cfg(feature = "__https")]
145 Version::Http2 => "only HTTP/2 supported",
146 #[cfg(feature = "__h3")]
147 Version::Http3 => "only HTTP/3 supported",
148 };
149 return Err(message.into());
150 }
151
152 debug!(
153 "verified request from: {}",
154 request
155 .headers()
156 .get(header::USER_AGENT)
157 .map(|h| h.to_str().unwrap_or("bad user agent"))
158 .unwrap_or("unknown user agent")
159 );
160
161 Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 #[cfg(feature = "__https")]
170 fn test_new_verify_h2() {
171 let request = new(Version::Http2, "ns.example.com", "/dns-query", 512)
172 .expect("error converting to http");
173 assert!(
174 verify(
175 Version::Http2,
176 Some("ns.example.com"),
177 "/dns-query",
178 &request
179 )
180 .is_ok()
181 );
182 }
183
184 #[test]
185 #[cfg(feature = "__h3")]
186 fn test_new_verify_h3() {
187 let request = new(Version::Http3, "ns.example.com", "/dns-query", 512)
188 .expect("error converting to http");
189 assert!(
190 verify(
191 Version::Http3,
192 Some("ns.example.com"),
193 "/dns-query",
194 &request
195 )
196 .is_ok()
197 );
198 }
199}