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 std::str::FromStr;
11
12use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
13use http::{header, uri, Request, Uri};
14use tracing::debug;
15
16use crate::error::ProtoError;
17use crate::http::error::Result;
18use crate::http::Version;
19
20/// Create a new Request for an http dns-message request
21///
22/// ```text
23/// https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-10#section-5.1
24/// The URI Template defined in this document is processed without any
25/// variables when the HTTP method is POST.  When the HTTP method is GET
26/// the single variable "dns" is defined as the content of the DNS
27/// request (as described in Section 7), encoded with base64url
28/// [RFC4648].
29/// ```
30#[allow(clippy::field_reassign_with_default)] // https://github.com/rust-lang/rust-clippy/issues/6527
31pub fn new(version: Version, name_server_name: &str, message_len: usize) -> Result<Request<()>> {
32    // TODO: this is basically the GET version, but it is more expensive than POST
33    //   perhaps add an option if people want better HTTP caching options.
34
35    // let query = BASE64URL_NOPAD.encode(&message);
36    // let url = format!("/dns-query?dns={}", query);
37    // let request = Request::get(&url)
38    //     .header(header::CONTENT_TYPE, ::MIME_DNS_BINARY)
39    //     .header(header::HOST, &self.name_server_name as &str)
40    //     .header("authority", &self.name_server_name as &str)
41    //     .header(header::USER_AGENT, USER_AGENT)
42    //     .body(());
43
44    let mut parts = uri::Parts::default();
45    parts.path_and_query = Some(uri::PathAndQuery::from_static(crate::http::DNS_QUERY_PATH));
46    parts.scheme = Some(uri::Scheme::HTTPS);
47    parts.authority = Some(
48        uri::Authority::from_str(name_server_name)
49            .map_err(|e| ProtoError::from(format!("invalid authority: {e}")))?,
50    );
51
52    let url =
53        Uri::from_parts(parts).map_err(|e| ProtoError::from(format!("uri parse error: {e}")))?;
54
55    // TODO: add user agent to TypedHeaders
56    let request = Request::builder()
57        .method("POST")
58        .uri(url)
59        .version(version.to_http())
60        .header(CONTENT_TYPE, crate::http::MIME_APPLICATION_DNS)
61        .header(ACCEPT, crate::http::MIME_APPLICATION_DNS)
62        .header(CONTENT_LENGTH, message_len)
63        .body(())
64        .map_err(|e| ProtoError::from(format!("http stream errored: {e}")))?;
65
66    Ok(request)
67}
68
69/// Verifies the request is something we know what to deal with
70pub fn verify<T>(version: Version, name_server: Option<&str>, request: &Request<T>) -> Result<()> {
71    // Verify all HTTP parameters
72    let uri = request.uri();
73
74    // validate path
75    if uri.path() != crate::http::DNS_QUERY_PATH {
76        return Err(format!(
77            "bad path: {}, expected: {}",
78            uri.path(),
79            crate::http::DNS_QUERY_PATH
80        )
81        .into());
82    }
83
84    // we only accept HTTPS
85    if Some(&uri::Scheme::HTTPS) != uri.scheme() {
86        return Err("must be HTTPS scheme".into());
87    }
88
89    // the authority must match our nameserver name
90    if let Some(name_server) = name_server {
91        if let Some(authority) = uri.authority() {
92            if authority.host() != name_server {
93                return Err("incorrect authority".into());
94            }
95        } else {
96            return Err("no authority in HTTPS request".into());
97        }
98    }
99
100    // TODO: switch to mime::APPLICATION_DNS when that stabilizes
101    match request.headers().get(CONTENT_TYPE).map(|v| v.to_str()) {
102        Some(Ok(ctype)) if ctype == crate::http::MIME_APPLICATION_DNS => {}
103        _ => return Err("unsupported content type".into()),
104    };
105
106    // TODO: switch to mime::APPLICATION_DNS when that stabilizes
107    match request.headers().get(ACCEPT).map(|v| v.to_str()) {
108        Some(Ok(ctype)) => {
109            let mut found = false;
110            for mime_and_quality in ctype.split(',') {
111                let mut parts = mime_and_quality.splitn(2, ';');
112                match parts.next() {
113                    Some(mime) if mime.trim() == crate::http::MIME_APPLICATION_DNS => {
114                        found = true;
115                        break;
116                    }
117                    Some(mime) if mime.trim() == "application/*" => {
118                        found = true;
119                        break;
120                    }
121                    _ => continue,
122                }
123            }
124
125            if !found {
126                return Err("does not accept content type".into());
127            }
128        }
129        Some(Err(e)) => return Err(e.into()),
130        None => return Err("Accept is unspecified".into()),
131    };
132
133    if request.version() != version.to_http() {
134        let message = match version {
135            #[cfg(feature = "dns-over-https")]
136            Version::Http2 => "only HTTP/2 supported",
137            #[cfg(feature = "dns-over-h3")]
138            Version::Http3 => "only HTTP/3 supported",
139        };
140        return Err(message.into());
141    }
142
143    debug!(
144        "verified request from: {}",
145        request
146            .headers()
147            .get(header::USER_AGENT)
148            .map(|h| h.to_str().unwrap_or("bad user agent"))
149            .unwrap_or("unknown user agent")
150    );
151
152    Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    #[cfg(feature = "dns-over-https")]
161    fn test_new_verify_h2() {
162        let request = new(Version::Http2, "ns.example.com", 512).expect("error converting to http");
163        assert!(verify(Version::Http2, Some("ns.example.com"), &request).is_ok());
164    }
165
166    #[test]
167    #[cfg(feature = "dns-over-h3")]
168    fn test_new_verify_h3() {
169        let request = new(Version::Http3, "ns.example.com", 512).expect("error converting to http");
170        assert!(verify(Version::Http3, Some("ns.example.com"), &request).is_ok());
171    }
172}