warg_client/
registry_url.rs1use crate::storage::RegistryDomain;
2use anyhow::{anyhow, bail, Context, Result};
3use reqwest::IntoUrl;
4use url::{Host, Url};
5
6#[derive(Clone, Eq, PartialEq)]
9pub struct RegistryUrl(Url);
10
11impl RegistryUrl {
12 pub fn new(url: impl IntoUrl) -> Result<Self> {
14 let mut url: Url = if !url.as_str().contains("://") {
16 Url::parse(&format!("https://{url}", url = url.as_str()))
17 .context("failed to parse registry server URL")?
18 } else {
19 url.into_url()
20 .context("failed to parse registry server URL")?
21 };
22
23 match url.scheme() {
24 "https" => {}
25 "http" => {
26 match url
28 .host()
29 .ok_or_else(|| anyhow!("expected a host for URL `{url}`"))?
30 {
31 Host::Domain(d) => {
32 if d != "localhost" {
33 bail!("an unsecured connection is not permitted to `{d}`");
34 }
35 }
36 Host::Ipv4(ip) => {
37 if !ip.is_loopback() {
38 bail!("an unsecured connection is not permitted to address `{ip}`");
39 }
40 }
41 Host::Ipv6(ip) => {
42 if !ip.is_loopback() {
43 bail!("an unsecured connection is not permitted to address `{ip}`");
44 }
45 }
46 }
47 }
48 _ => bail!("expected a HTTPS scheme for URL `{url}`"),
49 }
50
51 if !url.path().ends_with('/') {
53 url.set_path(&(url.path().to_string() + "/"));
54 }
55
56 Ok(Self(url))
57 }
58
59 pub fn safe_label(&self) -> String {
64 let mut label = match self.0.host().unwrap() {
66 Host::Domain(domain) => domain.to_string(),
67 Host::Ipv4(ip) => ip.to_string(),
68 Host::Ipv6(ip) => format!("ipv6_{ip}").replace(':', "."),
69 };
70 if let Some(port) = self.0.port() {
72 label += &format!("-{port}");
73 }
74 let path = self.0.path().trim_matches('/');
76 if !path.is_empty() {
77 label += "_";
78 for ch in path.chars() {
80 match ch {
81 '/' => label += "_",
82 '%' => label += ".",
83 '*' => label += ".2A",
84 '.' => label += ".2E",
85 '_' => label += ".5F",
86 oth => label.push(oth),
87 }
88 }
89 }
90 label
91 }
92
93 pub fn registry_domain(&self) -> RegistryDomain {
95 RegistryDomain::new(self.safe_label())
96 }
97
98 pub(crate) fn into_url(self) -> Url {
99 self.0
100 }
101
102 pub(crate) fn join(&self, path: &str) -> String {
103 self.0.join(path).unwrap().to_string()
106 }
107}
108
109impl std::str::FromStr for RegistryUrl {
110 type Err = anyhow::Error;
111
112 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
113 Self::new(s)
114 }
115}
116
117impl std::fmt::Display for RegistryUrl {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 write!(f, "{}", self.0)
120 }
121}
122
123impl std::fmt::Debug for RegistryUrl {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_tuple("RegistryUrl")
126 .field(&self.0.as_str())
127 .finish()
128 }
129}
130
131impl From<RegistryUrl> for Url {
132 fn from(value: RegistryUrl) -> Self {
133 value.into_url()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn must_parse(input: &str) -> RegistryUrl {
142 RegistryUrl::new(input)
143 .unwrap_or_else(|err| panic!("failed to parse valid input {input:?}: {err:?}"))
144 }
145
146 #[test]
147 fn new_valid() {
148 for (input, expected) in [
149 ("bare-host", "https://bare-host/"),
150 ("https://warg.io", "https://warg.io/"),
151 ("https://warg.io/with/path", "https://warg.io/with/path/"),
152 ("http://localhost", "http://localhost/"),
153 ("http://127.0.0.1", "http://127.0.0.1/"),
154 ("http://[::1]", "http://[::1]/"),
155 ("http://localhost:8080", "http://localhost:8080/"),
156 ("https://unchanged/", "https://unchanged/"),
157 ] {
158 assert_eq!(
159 must_parse(input).to_string(),
160 expected,
161 "incorrect output for input {input:?}"
162 )
163 }
164 }
165
166 #[test]
167 fn new_invalid() {
168 for input in [
169 "invalid:url",
170 "bad://scheme",
171 "http://insecure-domain",
172 "http://6.6.6.6/insecure/ipv4",
173 "http://[abcd::1234]/insecure/ipv6",
174 ] {
175 let res = RegistryUrl::new(input);
176 assert!(
177 res.is_err(),
178 "input {input:?} should have failed; got {res:?}"
179 );
180 }
181 }
182
183 #[test]
184 fn safe_label_works() {
185 for (input, expected) in [
186 ("warg.io", "warg.io"),
187 ("http://localhost:80", "localhost"),
188 ("example.com/with/path", "example.com_with_path"),
189 ("port:1234", "port-1234"),
190 ("port:1234/with/path", "port-1234_with_path"),
191 ("https://1.2.3.4:1234/1234", "1.2.3.4-1234_1234"),
192 ("https://[abcd::1234]:5678", "ipv6_abcd..1234-5678"),
193 ("syms/splat*dot.lowdash_", "syms_splat.2Adot.2Elowdash.5F"),
194 ("☃︎/☃︎", "xn--n3h_.E2.98.83.EF.B8.8E"), ] {
196 let url = must_parse(input);
197 assert_eq!(url.safe_label(), expected);
198 }
199 }
200}