hickory_proto/http/
request.rs

1// Copyright 2015-2018 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! HTTP request creation and validation
9
10use 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/// Create a new Request for an http dns-message request
21///
22/// ```text
23/// RFC 8484              DNS Queries over HTTPS (DoH)          October 2018
24///
25/// The URI Template defined in this document is processed without any
26/// variables when the HTTP method is POST.  When the HTTP method is GET,
27/// the single variable "dns" is defined as the content of the DNS
28/// request (as described in Section 6), encoded with base64url
29/// [RFC4648].
30/// ```
31#[allow(clippy::field_reassign_with_default)] // https://github.com/rust-lang/rust-clippy/issues/6527
32pub fn new(
33    version: Version,
34    name_server_name: &str,
35    query_path: &str,
36    message_len: usize,
37) -> Result<Request<()>> {
38    // TODO: this is basically the GET version, but it is more expensive than POST
39    //   perhaps add an option if people want better HTTP caching options.
40
41    // let query = BASE64URL_NOPAD.encode(&message);
42    // let url = format!("/dns-query?dns={}", query);
43    // let request = Request::get(&url)
44    //     .header(header::CONTENT_TYPE, ::MIME_DNS_BINARY)
45    //     .header(header::HOST, &self.name_server_name as &str)
46    //     .header("authority", &self.name_server_name as &str)
47    //     .header(header::USER_AGENT, USER_AGENT)
48    //     .body(());
49
50    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    // TODO: add user agent to TypedHeaders
65    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
78/// Verifies the request is something we know what to deal with
79pub fn verify<T>(
80    version: Version,
81    name_server: Option<&str>,
82    query_path: &str,
83    request: &Request<T>,
84) -> Result<()> {
85    // Verify all HTTP parameters
86    let uri = request.uri();
87
88    // validate path
89    if uri.path() != query_path {
90        return Err(format!("bad path: {}, expected: {}", uri.path(), query_path,).into());
91    }
92
93    // we only accept HTTPS
94    if Some(&uri::Scheme::HTTPS) != uri.scheme() {
95        return Err("must be HTTPS scheme".into());
96    }
97
98    // the authority must match our nameserver name
99    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    // TODO: switch to mime::APPLICATION_DNS when that stabilizes
110    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    // TODO: switch to mime::APPLICATION_DNS when that stabilizes
116    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}