1use crate::error::Error;
2use serde::{Deserialize, Serialize};
3#[cfg(feature = "speedy")]
4use speedy::{Readable, Writable};
5use std::fmt;
6
7#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)]
9#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
10pub struct UncheckedUrl(pub String);
11
12impl fmt::Display for UncheckedUrl {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 write!(f, "{}", self.0)
15 }
16}
17
18impl UncheckedUrl {
19 #[allow(clippy::should_implement_trait)]
23 pub fn from_str(s: &str) -> UncheckedUrl {
24 UncheckedUrl(s.to_owned())
25 }
26
27 pub fn from_string(s: String) -> UncheckedUrl {
29 UncheckedUrl(s)
30 }
31
32 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36
37 pub fn as_bech32_string(&self) -> String {
39 bech32::encode::<bech32::Bech32>(*crate::HRP_NRELAY, self.0.as_bytes()).unwrap()
40 }
41
42 pub fn try_from_bech32_string(s: &str) -> Result<UncheckedUrl, Error> {
44 let data = bech32::decode(s)?;
45 if data.0 != *crate::HRP_NRELAY {
46 Err(Error::WrongBech32(
47 crate::HRP_NRELAY.to_lowercase(),
48 data.0.to_lowercase(),
49 ))
50 } else {
51 let s = std::str::from_utf8(&data.1)?.to_owned();
52 Ok(UncheckedUrl(s))
53 }
54 }
55
56 #[allow(dead_code)]
58 pub(crate) fn mock() -> UncheckedUrl {
59 UncheckedUrl("/home/user/file.txt".to_string())
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
68#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
69pub struct Url(String);
70
71impl fmt::Display for Url {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{}", self.0)
74 }
75}
76
77impl Url {
78 pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result<Url, Error> {
80 Url::try_from_str(&u.0)
81 }
82
83 pub fn try_from_str(s: &str) -> Result<Url, Error> {
85 let url = url::Url::parse(s.trim())?;
87
88 if !url.has_authority() {
89 return Err(Error::InvalidUrlMissingAuthority);
90 }
91
92 if let Some(host) = url.host() {
93 match host {
94 url::Host::Domain(_) => {
95 let s = format!("{host}");
97 if s != s.trim() || s.starts_with("localhost") {
98 return Err(Error::InvalidUrlHost(s));
99 }
100 }
101 url::Host::Ipv4(addr) => {
102 let addrx = core_net::Ipv4Addr::from(addr.octets());
103 if !addrx.is_global() {
104 return Err(Error::InvalidUrlHost(format!("{host}")));
105 }
106 }
107 url::Host::Ipv6(addr) => {
108 let addrx = core_net::Ipv6Addr::from(addr.octets());
109 if !addrx.is_global() {
110 return Err(Error::InvalidUrlHost(format!("{host}")));
111 }
112 }
113 }
114 } else {
115 return Err(Error::InvalidUrlHost("".to_string()));
116 }
117
118 Ok(Url(url.as_str().to_owned()))
119 }
120
121 pub fn to_unchecked_url(&self) -> UncheckedUrl {
123 UncheckedUrl(self.0.clone())
124 }
125
126 pub fn as_str(&self) -> &str {
128 &self.0
129 }
130
131 pub fn into_string(self) -> String {
133 self.0
134 }
135
136 pub fn as_url_crate_url(&self) -> url::Url {
138 url::Url::parse(&self.0).unwrap()
139 }
140
141 #[allow(dead_code)]
143 pub(crate) fn mock() -> Url {
144 Url("http://example.com/avatar.png".to_string())
145 }
146}
147
148#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
151#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
152pub struct RelayUrl(String);
153
154impl fmt::Display for RelayUrl {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(f, "{}", self.0)
157 }
158}
159
160impl RelayUrl {
161 pub fn try_from_url(u: &Url) -> Result<RelayUrl, Error> {
163 if u.0.contains(",wss://") || u.0.contains(",ws://") {
167 return Err(Error::Url(format!(
168 "URL appears to be a list of multiple URLs: {}",
169 u.0
170 )));
171 }
172
173 let url = url::Url::parse(&u.0)?;
174
175 if url.scheme() != "wss" && url.scheme() != "ws" {
177 return Err(Error::InvalidUrlScheme(url.scheme().to_owned()));
178 }
179
180 if !url.has_host() {
182 return Err(Error::Url(format!("URL has no host: {}", u.0)));
183 }
184
185 Ok(RelayUrl(url.as_str().to_owned()))
186 }
187
188 pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result<RelayUrl, Error> {
190 Self::try_from_str(&u.0)
191 }
192
193 pub fn try_from_str(s: &str) -> Result<RelayUrl, Error> {
195 let url = Url::try_from_str(s)?;
196 RelayUrl::try_from_url(&url)
197 }
198
199 pub fn to_url(&self) -> Url {
202 Url(self.0.clone())
203 }
204
205 pub fn as_url_crate_url(&self) -> url::Url {
207 url::Url::parse(&self.0).unwrap()
208 }
209
210 pub fn as_bech32_string(&self) -> String {
212 bech32::encode::<bech32::Bech32>(*crate::HRP_NRELAY, self.0.as_bytes()).unwrap()
213 }
214
215 pub fn to_unchecked_url(&self) -> UncheckedUrl {
217 UncheckedUrl(self.0.clone())
218 }
219
220 pub fn host(&self) -> String {
222 self.as_url_crate_url().host_str().unwrap().to_owned()
223 }
224
225 pub fn as_str(&self) -> &str {
227 &self.0
228 }
229
230 pub fn into_string(self) -> String {
232 self.0
233 }
234
235 #[allow(dead_code)]
237 pub(crate) fn mock() -> Url {
238 Url("wss://example.com".to_string())
239 }
240}
241
242impl TryFrom<Url> for RelayUrl {
243 type Error = Error;
244
245 fn try_from(u: Url) -> Result<RelayUrl, Error> {
246 RelayUrl::try_from_url(&u)
247 }
248}
249
250impl TryFrom<&Url> for RelayUrl {
251 type Error = Error;
252
253 fn try_from(u: &Url) -> Result<RelayUrl, Error> {
254 RelayUrl::try_from_url(u)
255 }
256}
257
258impl From<RelayUrl> for Url {
259 fn from(ru: RelayUrl) -> Url {
260 ru.to_url()
261 }
262}
263
264#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)]
267#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
268pub struct RelayOrigin(String);
269
270impl RelayOrigin {
271 pub fn from_relay_url(url: RelayUrl) -> RelayOrigin {
273 let mut xurl = url::Url::parse(url.as_str()).unwrap();
274 xurl.set_fragment(None);
275 xurl.set_query(None);
276 xurl.set_path("/");
277 let _ = xurl.set_username("");
278 let _ = xurl.set_password(None);
279 RelayOrigin(xurl.into())
280 }
281
282 pub fn try_from_str(s: &str) -> Result<RelayOrigin, Error> {
284 let url = RelayUrl::try_from_str(s)?;
285 Ok(RelayOrigin::from_relay_url(url))
286 }
287
288 pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result<RelayOrigin, Error> {
290 let relay_url = RelayUrl::try_from_str(&u.0)?;
291 Ok(relay_url.into())
292 }
293
294 pub fn into_relay_url(self) -> RelayUrl {
296 RelayUrl(self.0)
297 }
298
299 pub fn as_relay_url(&self) -> RelayUrl {
301 RelayUrl(self.0.clone())
302 }
303
304 pub fn to_unchecked_url(&self) -> UncheckedUrl {
306 UncheckedUrl(self.0.clone())
307 }
308
309 pub fn as_str(&self) -> &str {
311 &self.0
312 }
313
314 pub fn into_string(self) -> String {
316 self.0
317 }
318}
319
320impl fmt::Display for RelayOrigin {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 write!(f, "{}", self.0)
323 }
324}
325
326impl From<RelayUrl> for RelayOrigin {
327 fn from(ru: RelayUrl) -> RelayOrigin {
328 RelayOrigin::from_relay_url(ru)
329 }
330}
331
332impl From<RelayOrigin> for RelayUrl {
333 fn from(ru: RelayOrigin) -> RelayUrl {
334 ru.into_relay_url()
335 }
336}
337
338#[cfg(test)]
339mod test {
340 use super::*;
341
342 test_serde! {UncheckedUrl, test_unchecked_url_serde}
343
344 #[test]
345 fn test_url_case() {
346 let url = Url::try_from_str("Wss://MyRelay.example.COM/PATH?Query").unwrap();
347 assert_eq!(url.as_str(), "wss://myrelay.example.com/PATH?Query");
348 }
349
350 #[test]
351 fn test_relay_url_slash() {
352 let input = "Wss://MyRelay.example.COM";
353 let url = RelayUrl::try_from_str(input).unwrap();
354 assert_eq!(url.as_str(), "wss://myrelay.example.com/");
355 }
356
357 #[test]
358 fn test_relay_origin() {
359 let input = "wss://user:pass@filter.nostr.wine:444/npub1234?x=y#z";
360 let relay_url = RelayUrl::try_from_str(input).unwrap();
361 let origin: RelayOrigin = relay_url.into();
362 assert_eq!(origin.as_str(), "wss://filter.nostr.wine:444/");
363 }
364}