Skip to main content

pipa/http/
url.rs

1#[derive(Debug, Clone)]
2pub struct Url {
3    pub scheme: String,
4    pub host: String,
5    pub port: u16,
6    pub path: String,
7    pub query: String,
8    pub full: String,
9}
10
11impl Url {
12    pub fn parse(input: &str) -> Result<Self, String> {
13        let input = input.trim();
14
15        let (scheme, rest) = match input.find("://") {
16            Some(pos) => {
17                let s = &input[..pos].to_lowercase();
18                let rest = &input[pos + 3..];
19                (s.to_string(), rest)
20            }
21            None => return Err(format!("missing scheme in url: {input}")),
22        };
23
24        if scheme != "http" && scheme != "https" && scheme != "ws" && scheme != "wss" {
25            return Err(format!("unsupported scheme: {scheme}"));
26        }
27
28        let default_port: u16 = match scheme.as_str() {
29            "https" | "wss" => 443,
30            _ => 80,
31        };
32        let is_tls = scheme == "https" || scheme == "wss";
33
34        let (host_str, path_and_query) = match rest.find('/') {
35            Some(pos) => (&rest[..pos], &rest[pos..]),
36            None => (rest, "/"),
37        };
38
39        let (host, port) = if let Some(colon_pos) = host_str.rfind(':') {
40            let h = &host_str[..colon_pos];
41            let p_str = &host_str[colon_pos + 1..];
42            let p: u16 = p_str
43                .parse()
44                .map_err(|_| format!("invalid port in url: {input}"))?;
45            (h.to_string(), p)
46        } else {
47            (host_str.to_string(), default_port)
48        };
49
50        if host.is_empty() {
51            return Err(format!("empty host in url: {input}"));
52        }
53
54        let (path, query) = if let Some(qmark) = path_and_query.find('?') {
55            let p = if qmark == 0 {
56                ""
57            } else {
58                &path_and_query[..qmark]
59            };
60            let p = if p.is_empty() { "/" } else { p };
61            (p.to_string(), path_and_query[qmark..].to_string())
62        } else {
63            let p = if path_and_query.is_empty() {
64                "/"
65            } else {
66                path_and_query
67            };
68            (p.to_string(), String::new())
69        };
70
71        Ok(Url {
72            scheme: if is_tls {
73                "https".into()
74            } else {
75                "http".into()
76            },
77            host,
78            port,
79            path,
80            query,
81            full: input.to_string(),
82        })
83    }
84
85    pub fn origin(&self) -> String {
86        if self.port == 80 || self.port == 443 {
87            format!("{}://{}", self.scheme, self.host)
88        } else {
89            format!("{}://{}:{}", self.scheme, self.host, self.port)
90        }
91    }
92
93    pub fn is_tls(&self) -> bool {
94        self.port == 443 || self.scheme == "https"
95    }
96
97    pub fn request_target(&self) -> String {
98        if self.query.is_empty() {
99            self.path.clone()
100        } else {
101            format!("{}?{}", self.path, self.query)
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_parse_https() {
112        let u = Url::parse("https://example.com/path?a=1").unwrap();
113        assert_eq!(u.scheme, "https");
114        assert_eq!(u.host, "example.com");
115        assert_eq!(u.port, 443);
116        assert_eq!(u.path, "/path");
117        assert_eq!(u.query, "?a=1");
118        assert!(u.is_tls());
119    }
120
121    #[test]
122    fn test_parse_http_with_port() {
123        let u = Url::parse("http://localhost:8080/api").unwrap();
124        assert_eq!(u.scheme, "http");
125        assert_eq!(u.host, "localhost");
126        assert_eq!(u.port, 8080);
127        assert_eq!(u.path, "/api");
128        assert!(!u.is_tls());
129    }
130
131    #[test]
132    fn test_parse_ws() {
133        let u = Url::parse("ws://echo.example.com/chat").unwrap();
134        assert_eq!(u.host, "echo.example.com");
135        assert_eq!(u.port, 80);
136    }
137
138    #[test]
139    fn test_parse_root() {
140        let u = Url::parse("http://example.com").unwrap();
141        assert_eq!(u.path, "/");
142        assert_eq!(u.query, "");
143    }
144
145    #[test]
146    fn test_parse_no_scheme() {
147        assert!(Url::parse("example.com/path").is_err());
148    }
149
150    #[test]
151    fn test_parse_bad_port() {
152        assert!(Url::parse("http://example.com:abc/path").is_err());
153    }
154
155    #[test]
156    fn test_parse_empty_host() {
157        assert!(Url::parse("http:///path").is_err());
158    }
159}