Skip to main content

webfinger_rs/
http.rs

1use std::str::FromStr;
2
3use http::Uri;
4use http::uri::{InvalidUri, PathAndQuery, Scheme};
5use percent_encoding::{AsciiSet, utf8_percent_encode};
6
7use crate::{WELL_KNOWN_PATH, WebFingerRequest, WebFingerResponse};
8
9pub(crate) const CORS_ALLOW_ORIGIN: &str = "*";
10
11/// The set of bytes to percent-encode in WebFinger query parameter values.
12///
13/// RFC 7033 section 4.1 percent-encodes the `resource` and `rel` parameter values before placing
14/// them in the query component. Only RFC 3986 unreserved characters are left literal here, which
15/// matches the RFC examples and keeps URI delimiters inside parameter values unambiguous. The set
16/// includes `%` so already-percent-encoded resource or relation URIs survive WebFinger query
17/// parsing as literal percent escapes in the target value.
18///
19/// See the following RFCs for more information:
20/// - <https://www.rfc-editor.org/rfc/rfc7033#section-4.1>
21/// - <https://www.rfc-editor.org/rfc/rfc3986#section-2.1>
22/// - <https://www.rfc-editor.org/rfc/rfc3986#section-3.4>
23/// - <https://www.rfc-editor.org/rfc/rfc3986#appendix-A>
24///
25/// Note: this may be implemented in the `percent-encoding` crate soon in
26/// <https://github.com/servo/rust-url/pull/971>
27const QUERY: AsciiSet = percent_encoding::CONTROLS
28    .add(b' ')
29    .add(b'!')
30    .add(b'"')
31    .add(b'#')
32    .add(b'$')
33    .add(b'%')
34    .add(b'&')
35    .add(b'\'')
36    .add(b'(')
37    .add(b')')
38    .add(b'*')
39    .add(b'+')
40    .add(b',')
41    .add(b'/')
42    .add(b':')
43    .add(b';')
44    .add(b'<')
45    .add(b'=')
46    .add(b'>')
47    .add(b'?')
48    .add(b'@')
49    .add(b'[')
50    .add(b'\\')
51    .add(b']')
52    .add(b'^')
53    .add(b'`')
54    .add(b'{')
55    .add(b'|')
56    .add(b'}');
57
58impl TryFrom<&WebFingerRequest> for PathAndQuery {
59    type Error = InvalidUri;
60
61    fn try_from(query: &WebFingerRequest) -> Result<PathAndQuery, InvalidUri> {
62        let resource = query.resource.to_string();
63        let resource = utf8_percent_encode(&resource, &QUERY).to_string();
64        let mut path = WELL_KNOWN_PATH.to_owned();
65        path.push_str("?resource=");
66        path.push_str(&resource);
67        for rel in &query.rels {
68            let rel = utf8_percent_encode(rel.as_ref(), &QUERY).to_string();
69            path.push_str("&rel=");
70            path.push_str(&rel);
71        }
72        PathAndQuery::from_str(&path)
73    }
74}
75
76impl TryFrom<&WebFingerRequest> for Uri {
77    type Error = http::Error;
78
79    fn try_from(query: &WebFingerRequest) -> Result<Uri, http::Error> {
80        let path_and_query = PathAndQuery::try_from(query)?;
81
82        // HTTPS is mandatory
83        // <https://www.rfc-editor.org/rfc/rfc7033.html#section-4>
84        // <https://www.rfc-editor.org/rfc/rfc7033.html#section-9.1>
85        const SCHEME: Scheme = Scheme::HTTPS;
86
87        Uri::builder()
88            .scheme(SCHEME)
89            .authority(query.host.clone())
90            .path_and_query(path_and_query)
91            .build()
92    }
93}
94
95impl TryFrom<&WebFingerResponse> for http::Response<()> {
96    type Error = http::Error;
97    fn try_from(_: &WebFingerResponse) -> Result<http::Response<()>, http::Error> {
98        http::Response::builder()
99            .header("Content-Type", "application/jrd+json")
100            .header("Access-Control-Allow-Origin", CORS_ALLOW_ORIGIN)
101            .body(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::Rel;
109
110    /// Percent-encodes literal percent signs in the outgoing `resource` value.
111    ///
112    /// RFC 7033 section 4.1 puts the resource URI inside a WebFinger query parameter. If the target
113    /// URI already contains percent escapes, the `%` signs must become `%25` in the outer query or a
114    /// server will decode them as part of the WebFinger parameter and change the target resource.
115    ///
116    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
117    /// <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
118    #[test]
119    fn outgoing_resource_preserves_inner_percent_escapes() {
120        let request = WebFingerRequest {
121            resource: "https://example.org/profile/a%20b".parse().unwrap(),
122            host: "example.org".to_string(),
123            rels: Vec::new(),
124        };
125
126        let uri = Uri::try_from(&request).unwrap();
127
128        assert_eq!(
129            uri.to_string(),
130            "https://example.org/.well-known/webfinger?resource=https%3A%2F%2Fexample.org%2Fprofile%2Fa%2520b",
131        );
132    }
133
134    /// Percent-encodes literal percent signs in outgoing `rel` values.
135    ///
136    /// Relation filters are also WebFinger query parameter values. Encoding `%` prevents an already
137    /// escaped relation URI from being decoded one level too far by the receiving WebFinger server.
138    ///
139    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
140    /// <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
141    #[test]
142    fn outgoing_rel_preserves_inner_percent_escapes() {
143        let request = WebFingerRequest {
144            resource: "acct:carol@example.org".parse().unwrap(),
145            host: "example.org".to_string(),
146            rels: vec![Rel::new("https://example.org/rel/a%2Fb")],
147        };
148
149        let uri = Uri::try_from(&request).unwrap();
150
151        assert_eq!(
152            uri.to_string(),
153            "https://example.org/.well-known/webfinger?resource=acct%3Acarol%40example.org&rel=https%3A%2F%2Fexample.org%2Frel%2Fa%252Fb",
154        );
155    }
156}