use alloc::string::String;
use core::cmp::Ordering;
use core::convert::Infallible;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::str::FromStr;
#[cfg(feature = "std")]
use std::net::IpAddr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "std")]
pub use url::*;
#[cfg(not(feature = "std"))]
pub use url_fork::*;
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
Url(ParseError),
UnsupportedScheme,
MultipleSchemeSeparators,
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Url(e) => e.fmt(f),
Self::UnsupportedScheme => f.write_str("Unsupported scheme"),
Self::MultipleSchemeSeparators => f.write_str("Multiple scheme separators"),
}
}
}
impl From<ParseError> for Error {
fn from(e: ParseError) -> Self {
Self::Url(e)
}
}
#[derive(Clone)]
pub struct RelayUrl {
url: Url,
has_trailing_slash: bool,
}
impl fmt::Debug for RelayUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let url: &str = self.as_str();
f.debug_tuple("RelayUrl").field(&url).finish()
}
}
impl PartialEq for RelayUrl {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Eq for RelayUrl {}
impl PartialOrd for RelayUrl {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RelayUrl {
fn cmp(&self, other: &Self) -> Ordering {
self.url.cmp(&other.url)
}
}
impl Hash for RelayUrl {
fn hash<H: Hasher>(&self, state: &mut H) {
self.url.hash(state);
}
}
impl RelayUrl {
#[inline]
pub fn parse(url: &str) -> Result<Self, Error> {
if url.matches("://").count() > 1 {
return Err(Error::MultipleSchemeSeparators);
}
let has_trailing_slash: bool = url.ends_with('/');
let url: Url = Url::parse(url)?;
match url.scheme() {
"ws" | "wss" => Ok(Self {
url,
has_trailing_slash,
}),
_ => Err(Error::UnsupportedScheme),
}
}
#[cfg(feature = "std")]
pub fn is_local_addr(&self) -> bool {
if let Some(host) = self.url.host_str() {
if let Ok(addr) = IpAddr::from_str(host) {
return match addr {
IpAddr::V4(ipv4) => ipv4.is_loopback() || ipv4.is_private(),
IpAddr::V6(ipv6) => ipv6.is_loopback(),
};
}
}
false
}
#[inline]
pub fn is_onion(&self) -> bool {
self.url
.domain()
.is_some_and(|host| host.ends_with(".onion"))
}
#[inline]
pub fn domain(&self) -> Option<&str> {
self.url.domain()
}
#[inline]
pub fn host(&self) -> Option<Host<&str>> {
self.url.host()
}
#[inline]
pub fn as_str_without_trailing_slash(&self) -> &str {
self.url.as_str().trim_end_matches('/')
}
#[inline]
pub fn as_str(&self) -> &str {
if !self.has_trailing_slash {
return self.as_str_without_trailing_slash();
}
self.url.as_str()
}
}
impl fmt::Display for RelayUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for RelayUrl {
type Err = Error;
fn from_str(relay_url: &str) -> Result<Self, Self::Err> {
Self::parse(relay_url)
}
}
impl Serialize for RelayUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for RelayUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let url: String = String::deserialize(deserializer)?;
Self::parse(&url).map_err(serde::de::Error::custom)
}
}
impl From<RelayUrl> for Url {
fn from(relay_url: RelayUrl) -> Self {
relay_url.url
}
}
impl<'a> From<&'a RelayUrl> for &'a Url {
fn from(relay_url: &'a RelayUrl) -> Self {
&relay_url.url
}
}
pub trait TryIntoUrl {
type Err: fmt::Debug;
fn try_into_url(self) -> Result<RelayUrl, Self::Err>;
}
impl TryIntoUrl for RelayUrl {
type Err = Infallible;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
Ok(self)
}
}
impl TryIntoUrl for &RelayUrl {
type Err = Infallible;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
Ok(self.clone())
}
}
impl TryIntoUrl for Url {
type Err = Error;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
RelayUrl::parse(self.as_str())
}
}
impl TryIntoUrl for &Url {
type Err = Error;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
RelayUrl::parse(self.as_str())
}
}
impl TryIntoUrl for String {
type Err = Error;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
RelayUrl::parse(self.as_str())
}
}
impl TryIntoUrl for &String {
type Err = Error;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
RelayUrl::parse(self)
}
}
impl TryIntoUrl for &str {
type Err = Error;
#[inline]
fn try_into_url(self) -> Result<RelayUrl, Self::Err> {
RelayUrl::parse(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relay_url_valid() {
assert!(RelayUrl::parse("ws://127.0.0.1:7777").is_ok());
assert!(RelayUrl::parse("wss://relay.damus.io").is_ok());
assert!(RelayUrl::parse("ws://example.com").is_ok());
assert!(RelayUrl::parse("wss://example.com/path/to/resource").is_ok());
}
#[test]
fn test_relay_url_invalid() {
assert_eq!(
RelayUrl::parse("https://relay.damus.io").unwrap_err(),
Error::UnsupportedScheme
);
assert_eq!(
RelayUrl::parse("ftp://relay.damus.io").unwrap_err(),
Error::UnsupportedScheme
);
assert_eq!(
RelayUrl::parse("wss://relay.damus.io,ws://127.0.0.1:7777").unwrap_err(),
Error::MultipleSchemeSeparators
);
assert_eq!(
RelayUrl::parse("wss://relay.damus.iowss://127.0.0.1:8888").unwrap_err(),
Error::MultipleSchemeSeparators
);
assert_eq!(
RelayUrl::parse("wss://").unwrap_err(),
Error::Url(ParseError::EmptyHost)
);
}
#[test]
fn test_relay_url_as_str() {
let relay_url = RelayUrl::parse("ws://example.com").unwrap();
assert_eq!(relay_url.as_str(), "ws://example.com");
let relay_url = RelayUrl::parse("ws://example.com/").unwrap();
assert_eq!(relay_url.as_str(), "ws://example.com/");
let relay_url = RelayUrl::parse("ws://example.com/").unwrap();
assert_eq!(
relay_url.as_str_without_trailing_slash(),
"ws://example.com"
);
}
#[test]
fn test_relay_url_from_str() {
let relay_url: Result<RelayUrl, _> = "ws://example.com".parse();
assert!(relay_url.is_ok());
}
#[test]
fn test_serde_relay_url() {
let relay_url = RelayUrl::parse("ws://example.com").unwrap();
let serialized = serde_json::to_string(&relay_url).unwrap();
let deserialized: RelayUrl = serde_json::from_str(&serialized).unwrap();
assert_eq!(relay_url, deserialized);
}
#[test]
#[cfg(feature = "std")]
fn test_is_local() {
let url = RelayUrl::parse("ws://127.0.0.1:7777").unwrap();
assert!(url.is_local_addr());
let url = RelayUrl::parse("ws://10.10.10.10:7777").unwrap();
assert!(url.is_local_addr());
let url = RelayUrl::parse("ws://172.16.10.11:7777").unwrap();
assert!(url.is_local_addr());
let url = RelayUrl::parse("ws://192.168.1.10:7777").unwrap();
assert!(url.is_local_addr());
let onion_url =
RelayUrl::parse("ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion")
.unwrap();
assert!(!onion_url.is_local_addr());
let url = RelayUrl::parse("wss://relay.damus.io").unwrap();
assert!(!url.is_local_addr());
}
#[test]
fn test_is_onion() {
let onion_url =
RelayUrl::parse("ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion")
.unwrap();
assert!(onion_url.is_onion());
let non_onion_url = RelayUrl::parse("wss://relay.damus.io").unwrap();
assert!(!non_onion_url.is_onion());
let non_onion_url = RelayUrl::parse("ws://example.com:81").unwrap();
assert!(!non_onion_url.is_onion());
let non_onion_url = RelayUrl::parse("ws://127.0.0.1:7777").unwrap();
assert!(!non_onion_url.is_onion());
}
#[test]
fn test_domain() {
let url = RelayUrl::parse("wss://example.com").unwrap();
assert_eq!(url.domain(), Some("example.com"));
let url = RelayUrl::parse("wss://relay.example.com/").unwrap();
assert_eq!(url.domain(), Some("relay.example.com"));
let url = RelayUrl::parse("wss://example.com/path/to/resource").unwrap();
assert_eq!(url.domain(), Some("example.com"));
let url = RelayUrl::parse("wss://127.0.0.1:7777").unwrap();
assert_eq!(url.domain(), None);
}
}