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