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/// 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(
32    version: Version,
33    name_server_name: &str,
34    query_path: &str,
35    message_len: usize,
36) -> Result<Request<()>> {
37    // TODO: this is basically the GET version, but it is more expensive than POST
38    //   perhaps add an option if people want better HTTP caching options.
39
40    // let query = BASE64URL_NOPAD.encode(&message);
41    // let url = format!("/dns-query?dns={}", query);
42    // let request = Request::get(&url)
43    //     .header(header::CONTENT_TYPE, ::MIME_DNS_BINARY)
44    //     .header(header::HOST, &self.name_server_name as &str)
45    //     .header("authority", &self.name_server_name as &str)
46    //     .header(header::USER_AGENT, USER_AGENT)
47    //     .body(());
48
49    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    // TODO: add user agent to TypedHeaders
64    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
77/// Verifies the request is something we know what to deal with
78pub fn verify<T>(
79    version: Version,
80    name_server: Option<&str>,
81    query_path: &str,
82    request: &Request<T>,
83) -> Result<()> {
84    // Verify all HTTP parameters
85    let uri = request.uri();
86
87    // validate path
88    if uri.path() != query_path {
89        return Err(format!("bad path: {}, expected: {}", uri.path(), query_path,).into());
90    }
91
92    // we only accept HTTPS
93    if Some(&uri::Scheme::HTTPS) != uri.scheme() {
94        return Err("must be HTTPS scheme".into());
95    }
96
97    // the authority must match our nameserver name
98    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    // TODO: switch to mime::APPLICATION_DNS when that stabilizes
109    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    // TODO: switch to mime::APPLICATION_DNS when that stabilizes
115    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}