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