Skip to main content

pingora_http/
authority.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Request-target authority classification.
16//!
17//! Locates the authority and path boundaries within a raw request-target, without
18//! interpreting them. Classification is kept separate from the policy that acts on it so
19//! that the URI stored on a [`RequestHeader`](crate::RequestHeader) and the authority a
20//! protocol implementation reconciles against `Host` derive from the same boundaries and
21//! cannot disagree about where the authority ends.
22//!
23//! These functions read raw bytes and never allocate.
24
25/// Raw request-target authority boundaries.
26///
27/// Used for H1 absolute-form and malformed H2 `:path`; authority bytes remain opaque.
28#[derive(Debug, PartialEq, Eq)]
29pub enum RawTargetAuthority<'a> {
30    /// The target does not carry an authority in absolute-form.
31    None,
32    /// The target carries an absolute-form authority and the following path/query bytes.
33    Absolute {
34        /// The raw scheme bytes, excluding `:`.
35        scheme: &'a [u8],
36        /// The raw authority bytes, excluding the leading `//`.
37        authority: &'a [u8],
38        /// The path and query after the authority, or an empty slice when absent.
39        path_and_query: &'a [u8],
40    },
41    /// Parser normalization could produce a different authority.
42    ///
43    /// Examples: `http:/\host` and `http://host\other/`
44    AmbiguousAuthority,
45}
46
47impl<'a> RawTargetAuthority<'a> {
48    /// Return the absolute-form authority, if this classification carries one.
49    pub fn authority(&self) -> Option<&'a [u8]> {
50        match self {
51            Self::None | Self::AmbiguousAuthority => None,
52            Self::Absolute { authority, .. } => Some(authority),
53        }
54    }
55}
56
57/// Classify authority and path/query boundaries in a raw request target.
58pub fn raw_target_authority(target: &[u8]) -> RawTargetAuthority<'_> {
59    // Origin form, the common case, cannot carry an authority.
60    if target.first() == Some(&b'/') {
61        return RawTargetAuthority::None;
62    }
63
64    // Phase 1: validate the scheme while locating its terminating colon.
65    // RFC 3986 section 3.1: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ).
66    // https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1
67    let mut valid_scheme = true;
68    let mut scheme_end = None;
69    for (index, &byte) in target.iter().enumerate() {
70        if byte == b':' {
71            scheme_end = Some(index);
72            break;
73        }
74        valid_scheme &= if index == 0 {
75            byte.is_ascii_alphabetic()
76        } else {
77            byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.')
78        };
79    }
80    let Some(scheme_end) = scheme_end else {
81        return RawTargetAuthority::None;
82    };
83    let scheme = &target[..scheme_end];
84    let remainder = &target[scheme_end + 1..];
85
86    let authority = remainder.strip_prefix(b"//");
87
88    if scheme.is_empty() || !valid_scheme {
89        // `://host` and `ht_tp://host` are ambiguous because `//` can make permissive parsers
90        // derive an authority; `:opaque` and `ht_tp:opaque` carry no authority marker.
91        return if authority.is_some() {
92            RawTargetAuthority::AmbiguousAuthority
93        } else {
94            RawTargetAuthority::None
95        };
96    }
97
98    let http_family_scheme = is_http_family_scheme(scheme);
99    let Some(authority) = authority else {
100        // HTTP/WebSocket schemes without `//` can gain authority during normalization
101        // (`http:host`); custom forms remain application-defined (`myproto:opaque`).
102        return if http_family_scheme {
103            RawTargetAuthority::AmbiguousAuthority
104        } else {
105            RawTargetAuthority::None
106        };
107    };
108
109    // Phase 2: locate the authority boundary and special-scheme backslashes in one scan.
110    let mut end = authority.len();
111    for (index, &byte) in authority.iter().enumerate() {
112        if matches!(byte, b'/' | b'?' | b'#') {
113            end = index;
114            break;
115        }
116        // HTTP/WebSocket URL parsers treat `\` as `/`, which can change the authority boundary.
117        if http_family_scheme && byte == b'\\' {
118            return RawTargetAuthority::AmbiguousAuthority;
119        }
120    }
121    if http_family_scheme && end == 0 {
122        return RawTargetAuthority::AmbiguousAuthority;
123    }
124    RawTargetAuthority::Absolute {
125        scheme,
126        authority: &authority[..end],
127        path_and_query: &authority[end..],
128    }
129}
130
131/// Check whether a scheme is HTTP, HTTPS, WebSocket, or secure WebSocket.
132///
133/// These schemes receive special parsing rules that differ from opaque schemes:
134/// normalization can introduce an authority where none appears in the request-target
135/// (e.g. `http:host` becoming `http://host`), and backslash is treated as a path
136/// separator.
137pub fn is_http_family_scheme(scheme: &[u8]) -> bool {
138    [b"http".as_slice(), b"https", b"ws", b"wss"]
139        .iter()
140        .any(|candidate| scheme.eq_ignore_ascii_case(candidate))
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn classify_raw_target_authority() {
149        assert_eq!(
150            raw_target_authority(b"http://authority.example/test"),
151            RawTargetAuthority::Absolute {
152                scheme: b"http",
153                authority: b"authority.example",
154                path_and_query: b"/test"
155            }
156        );
157        assert_eq!(
158            raw_target_authority(b"http://authority.example/test").authority(),
159            Some(b"authority.example".as_slice())
160        );
161        assert_eq!(
162            raw_target_authority(b"http://user@authority.example:8443/test?a=b"),
163            RawTargetAuthority::Absolute {
164                scheme: b"http",
165                authority: b"user@authority.example:8443",
166                path_and_query: b"/test?a=b"
167            }
168        );
169        assert_eq!(
170            raw_target_authority(b"http://authority.example"),
171            RawTargetAuthority::Absolute {
172                scheme: b"http",
173                authority: b"authority.example",
174                path_and_query: b""
175            }
176        );
177        assert_eq!(
178            raw_target_authority(b"http:/\\/\\authority.example/test"),
179            RawTargetAuthority::AmbiguousAuthority
180        );
181        assert_eq!(
182            raw_target_authority(b"https:authority.example/test"),
183            RawTargetAuthority::AmbiguousAuthority
184        );
185        assert_eq!(
186            raw_target_authority(b"http://good.example\\evil.example/"),
187            RawTargetAuthority::AmbiguousAuthority
188        );
189        for target in [
190            b"ht_tp://other.example/admin".as_slice(),
191            b"9x://other.example/admin",
192            b"http:///path",
193            b"https://?query",
194            b"ws://good.example\\evil.example/",
195            b"wss://good.example\\evil.example/",
196        ] {
197            assert_eq!(
198                raw_target_authority(target),
199                RawTargetAuthority::AmbiguousAuthority,
200                "{}",
201                String::from_utf8_lossy(target)
202            );
203        }
204        assert_eq!(
205            raw_target_authority(b"file:///path"),
206            RawTargetAuthority::Absolute {
207                scheme: b"file",
208                authority: b"",
209                path_and_query: b"/path"
210            }
211        );
212        assert_eq!(
213            raw_target_authority(b"ftp://good.example\\evil.example/"),
214            RawTargetAuthority::Absolute {
215                scheme: b"ftp",
216                authority: b"good.example\\evil.example",
217                path_and_query: b"/"
218            }
219        );
220        assert_eq!(raw_target_authority(b"/test"), RawTargetAuthority::None);
221        assert_eq!(raw_target_authority(b":opaque"), RawTargetAuthority::None);
222        assert_eq!(
223            raw_target_authority(b"/redirect?next=http://user@evil.example/"),
224            RawTargetAuthority::None
225        );
226        assert_eq!(
227            raw_target_authority(b"/test#http://user@evil.example/"),
228            RawTargetAuthority::None
229        );
230        assert_eq!(
231            raw_target_authority(b"foo:bar://user@example/path"),
232            RawTargetAuthority::None
233        );
234    }
235}