rivetkit_shared_types/
endpoint.rs1use std::fmt;
7use std::net::IpAddr;
8
9use url::Url;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct ParsedEndpoint {
14 pub endpoint: String,
16 pub namespace: Option<String>,
17 pub token: Option<String>,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum EndpointParseError {
22 InvalidUrl {
23 endpoint: String,
24 },
25 QueryString,
26 Fragment,
27 TokenWithoutNamespace,
28 InvalidCredentialEncoding,
30 CredentialsUnsupported,
31}
32
33impl fmt::Display for EndpointParseError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::InvalidUrl { endpoint } => write!(f, "invalid URL: {endpoint}"),
37 Self::QueryString => write!(f, "endpoint cannot contain a query string"),
38 Self::Fragment => write!(f, "endpoint cannot contain a fragment"),
39 Self::TokenWithoutNamespace => {
40 write!(f, "endpoint cannot have a token without a namespace")
41 }
42 Self::InvalidCredentialEncoding => {
43 write!(f, "invalid percent-encoding in endpoint URL auth")
44 }
45 Self::CredentialsUnsupported => {
46 write!(f, "endpoint URL does not support credentials")
47 }
48 }
49 }
50}
51
52impl std::error::Error for EndpointParseError {}
53
54pub fn parse_endpoint_auth(endpoint: &str) -> Result<ParsedEndpoint, EndpointParseError> {
59 let mut url = Url::parse(endpoint).map_err(|_| EndpointParseError::InvalidUrl {
60 endpoint: endpoint.to_owned(),
61 })?;
62 if url.query().is_some() {
63 return Err(EndpointParseError::QueryString);
64 }
65 if url.fragment().is_some() {
66 return Err(EndpointParseError::Fragment);
67 }
68
69 let namespace = if url.username().is_empty() {
70 None
71 } else {
72 Some(percent_decode(url.username())?)
73 };
74 let token = match url.password() {
75 None | Some("") => None,
76 Some(password) => Some(percent_decode(password)?),
77 };
78 if token.is_some() && namespace.is_none() {
79 return Err(EndpointParseError::TokenWithoutNamespace);
80 }
81
82 if namespace.is_some() || token.is_some() {
86 if url.set_username("").is_err() || url.set_password(None).is_err() {
87 return Err(EndpointParseError::CredentialsUnsupported);
88 }
89 }
90
91 Ok(ParsedEndpoint {
92 endpoint: url.to_string(),
93 namespace,
94 token,
95 })
96}
97
98pub fn is_local_engine_endpoint(endpoint: &str) -> bool {
102 let Ok(url) = Url::parse(endpoint) else {
103 return false;
104 };
105 let Some(host) = url.host_str() else {
106 return false;
107 };
108
109 if host.eq_ignore_ascii_case("localhost") {
110 return true;
111 }
112
113 let ip_literal = host.trim_start_matches('[').trim_end_matches(']');
114 match ip_literal.parse::<IpAddr>() {
115 Ok(ip) => ip.is_loopback() || ip.is_unspecified(),
116 Err(_) => false,
117 }
118}
119
120fn percent_decode(value: &str) -> Result<String, EndpointParseError> {
121 let bytes = value.as_bytes();
122 let mut out = Vec::with_capacity(bytes.len());
123 let mut i = 0;
124 while i < bytes.len() {
125 if bytes[i] == b'%' {
126 if i + 2 >= bytes.len() {
127 return Err(EndpointParseError::InvalidCredentialEncoding);
128 }
129 let hi =
130 hex_value(bytes[i + 1]).ok_or(EndpointParseError::InvalidCredentialEncoding)?;
131 let lo =
132 hex_value(bytes[i + 2]).ok_or(EndpointParseError::InvalidCredentialEncoding)?;
133 out.push((hi << 4) | lo);
134 i += 3;
135 } else {
136 out.push(bytes[i]);
137 i += 1;
138 }
139 }
140 String::from_utf8(out).map_err(|_| EndpointParseError::InvalidCredentialEncoding)
141}
142
143fn hex_value(byte: u8) -> Option<u8> {
144 match byte {
145 b'0'..=b'9' => Some(byte - b'0'),
146 b'a'..=b'f' => Some(byte - b'a' + 10),
147 b'A'..=b'F' => Some(byte - b'A' + 10),
148 _ => None,
149 }
150}