use crate::error::Error;
use serde::{Deserialize, Serialize};
#[cfg(feature = "speedy")]
use speedy::{Readable, Writable};
use std::fmt;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)]
#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
pub struct UncheckedUrl(pub String);
impl fmt::Display for UncheckedUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl UncheckedUrl {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> UncheckedUrl {
UncheckedUrl(s.to_owned())
}
pub fn from_string(s: String) -> UncheckedUrl {
UncheckedUrl(s)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_bech32_string(&self) -> String {
bech32::encode::<bech32::Bech32>(*crate::HRP_NRELAY, self.0.as_bytes()).unwrap()
}
pub fn try_from_bech32_string(s: &str) -> Result<UncheckedUrl, Error> {
let data = bech32::decode(s)?;
if data.0 != *crate::HRP_NRELAY {
Err(Error::WrongBech32(
crate::HRP_NRELAY.to_lowercase(),
data.0.to_lowercase(),
))
} else {
let s = std::str::from_utf8(&data.1)?.to_owned();
Ok(UncheckedUrl(s))
}
}
#[allow(dead_code)]
pub(crate) fn mock() -> UncheckedUrl {
UncheckedUrl("/home/user/file.txt".to_string())
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
pub struct Url(String);
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Url {
pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result<Url, Error> {
Url::try_from_str(&u.0)
}
pub fn try_from_str(s: &str) -> Result<Url, Error> {
let url = url::Url::parse(s.trim())?;
if !url.has_authority() {
return Err(Error::InvalidUrlMissingAuthority);
}
if let Some(host) = url.host() {
match host {
url::Host::Domain(_) => {
let s = format!("{host}");
if s != s.trim() || s.starts_with("localhost") {
return Err(Error::InvalidUrlHost(s));
}
}
url::Host::Ipv4(addr) => {
let addrx = core_net::Ipv4Addr::from(addr.octets());
if !addrx.is_global() {
return Err(Error::InvalidUrlHost(format!("{host}")));
}
}
url::Host::Ipv6(addr) => {
let addrx = core_net::Ipv6Addr::from(addr.octets());
if !addrx.is_global() {
return Err(Error::InvalidUrlHost(format!("{host}")));
}
}
}
} else {
return Err(Error::InvalidUrlHost("".to_string()));
}
Ok(Url(url.as_str().to_owned()))
}
pub fn to_unchecked_url(&self) -> UncheckedUrl {
UncheckedUrl(self.0.clone())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
pub fn as_url_crate_url(&self) -> url::Url {
url::Url::parse(&self.0).unwrap()
}
#[allow(dead_code)]
pub(crate) fn mock() -> Url {
Url("http://example.com/avatar.png".to_string())
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
pub struct RelayUrl(String);
impl fmt::Display for RelayUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl RelayUrl {
pub fn try_from_url(u: &Url) -> Result<RelayUrl, Error> {
if u.0.contains(",wss://") || u.0.contains(",ws://") {
return Err(Error::Url(format!(
"URL appears to be a list of multiple URLs: {}",
u.0
)));
}
let url = url::Url::parse(&u.0)?;
if url.scheme() != "wss" && url.scheme() != "ws" {
return Err(Error::InvalidUrlScheme(url.scheme().to_owned()));
}
if !url.has_host() {
return Err(Error::Url(format!("URL has no host: {}", u.0)));
}
Ok(RelayUrl(url.as_str().to_owned()))
}
pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result<RelayUrl, Error> {
Self::try_from_str(&u.0)
}
pub fn try_from_str(s: &str) -> Result<RelayUrl, Error> {
let url = Url::try_from_str(s)?;
RelayUrl::try_from_url(&url)
}
pub fn to_url(&self) -> Url {
Url(self.0.clone())
}
pub fn as_url_crate_url(&self) -> url::Url {
url::Url::parse(&self.0).unwrap()
}
pub fn as_bech32_string(&self) -> String {
bech32::encode::<bech32::Bech32>(*crate::HRP_NRELAY, self.0.as_bytes()).unwrap()
}
pub fn to_unchecked_url(&self) -> UncheckedUrl {
UncheckedUrl(self.0.clone())
}
pub fn host(&self) -> String {
self.as_url_crate_url().host_str().unwrap().to_owned()
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
#[allow(dead_code)]
pub(crate) fn mock() -> Url {
Url("wss://example.com".to_string())
}
}
impl TryFrom<Url> for RelayUrl {
type Error = Error;
fn try_from(u: Url) -> Result<RelayUrl, Error> {
RelayUrl::try_from_url(&u)
}
}
impl TryFrom<&Url> for RelayUrl {
type Error = Error;
fn try_from(u: &Url) -> Result<RelayUrl, Error> {
RelayUrl::try_from_url(u)
}
}
impl From<RelayUrl> for Url {
fn from(ru: RelayUrl) -> Url {
ru.to_url()
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)]
#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
pub struct RelayOrigin(String);
impl RelayOrigin {
pub fn from_relay_url(url: RelayUrl) -> RelayOrigin {
let mut xurl = url::Url::parse(url.as_str()).unwrap();
xurl.set_fragment(None);
xurl.set_query(None);
xurl.set_path("/");
let _ = xurl.set_username("");
let _ = xurl.set_password(None);
RelayOrigin(xurl.into())
}
pub fn try_from_str(s: &str) -> Result<RelayOrigin, Error> {
let url = RelayUrl::try_from_str(s)?;
Ok(RelayOrigin::from_relay_url(url))
}
pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result<RelayOrigin, Error> {
let relay_url = RelayUrl::try_from_str(&u.0)?;
Ok(relay_url.into())
}
pub fn into_relay_url(self) -> RelayUrl {
RelayUrl(self.0)
}
pub fn as_relay_url(&self) -> RelayUrl {
RelayUrl(self.0.clone())
}
pub fn to_unchecked_url(&self) -> UncheckedUrl {
UncheckedUrl(self.0.clone())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for RelayOrigin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<RelayUrl> for RelayOrigin {
fn from(ru: RelayUrl) -> RelayOrigin {
RelayOrigin::from_relay_url(ru)
}
}
impl From<RelayOrigin> for RelayUrl {
fn from(ru: RelayOrigin) -> RelayUrl {
ru.into_relay_url()
}
}
#[cfg(test)]
mod test {
use super::*;
test_serde! {UncheckedUrl, test_unchecked_url_serde}
#[test]
fn test_url_case() {
let url = Url::try_from_str("Wss://MyRelay.example.COM/PATH?Query").unwrap();
assert_eq!(url.as_str(), "wss://myrelay.example.com/PATH?Query");
}
#[test]
fn test_relay_url_slash() {
let input = "Wss://MyRelay.example.COM";
let url = RelayUrl::try_from_str(input).unwrap();
assert_eq!(url.as_str(), "wss://myrelay.example.com/");
}
#[test]
fn test_relay_origin() {
let input = "wss://user:pass@filter.nostr.wine:444/npub1234?x=y#z";
let relay_url = RelayUrl::try_from_str(input).unwrap();
let origin: RelayOrigin = relay_url.into();
assert_eq!(origin.as_str(), "wss://filter.nostr.wine:444/");
}
}