1use crate::parse_socket_addr;
2use core::fmt::{Display, Formatter};
3use core::net::IpAddr;
4use core::net::Ipv4Addr;
5use core::net::SocketAddr;
6use core::str::FromStr;
7use minicbor::{CborLen, Decode, Encode};
8use ockam_core::compat::format;
9use ockam_core::compat::string::{String, ToString};
10use ockam_core::errcode::{Kind, Origin};
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12#[cfg(feature = "std")]
13use url::Url;
14
15pub struct StaticHostnamePort {
17 hostname: &'static str,
18 port: u16,
19}
20
21impl StaticHostnamePort {
22 pub const fn new(hostname: &'static str, port: u16) -> Self {
23 Self { hostname, port }
24 }
25
26 pub const fn localhost(port: u16) -> Self {
27 Self {
28 hostname: "127.0.0.1",
29 port,
30 }
31 }
32}
33
34impl TryFrom<StaticHostnamePort> for HostnamePort {
35 type Error = ockam_core::Error;
36
37 fn try_from(value: StaticHostnamePort) -> ockam_core::Result<Self> {
38 HostnamePort::new(value.hostname, value.port)
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, CborLen)]
44#[rustfmt::skip]
45pub struct HostnamePort {
46 #[n(0)] pub hostname: String,
47 #[n(1)] pub port: u16,
48}
49
50impl HostnamePort {
51 pub fn new(hostname: impl Into<String>, port: u16) -> ockam_core::Result<HostnamePort> {
53 let _self = Self {
54 hostname: hostname.into(),
55 port,
56 };
57 Self::validate(&_self.to_string())
58 }
59
60 pub fn hostname(&self) -> String {
62 self.hostname.clone()
63 }
64
65 pub fn port(&self) -> u16 {
67 self.port
68 }
69
70 #[cfg(feature = "std")]
71 pub fn into_url(self, scheme: &str) -> ockam_core::Result<Url> {
72 Url::parse(&format!("{}://{}:{}", scheme, self.hostname, self.port))
73 .map_err(|_| ockam_core::Error::new(Origin::Api, Kind::Serialization, "invalid url"))
74 }
75
76 fn validate(hostname_port: &str) -> ockam_core::Result<Self> {
77 if let Ok(socket) = parse_socket_addr(hostname_port) {
79 return Ok(HostnamePort::from(socket));
80 }
81
82 let (hostname, port_str) = match hostname_port.split_once(':') {
84 None => {
85 return Err(ockam_core::Error::new(
86 Origin::Core,
87 Kind::Parse,
88 "Invalid format. Expected 'hostname:port'".to_string(),
89 ))
90 }
91 Some((hostname, port_str)) => (hostname, port_str),
92 };
93
94 let port = port_str.parse::<u16>().map_err(|_| {
96 ockam_core::Error::new(
97 Origin::Core,
98 Kind::Parse,
99 format!("Invalid port number {port_str}"),
100 )
101 })?;
102
103 if !hostname.is_ascii() {
105 return Err(ockam_core::Error::new(
106 Origin::Core,
107 Kind::Parse,
108 format!("Hostname must be ascii: {hostname_port}"),
109 ));
110 }
111
112 if hostname.is_empty() {
114 return Err(ockam_core::Error::new(
115 Origin::Core,
116 Kind::Parse,
117 format!("Hostname cannot be empty {hostname}"),
118 ));
119 }
120
121 if hostname.len() > 253 {
123 return Err(ockam_core::Error::new(
124 Origin::Api,
125 Kind::Serialization,
126 format!("Hostname too long {hostname}"),
127 ));
128 }
129
130 if hostname.starts_with('-')
132 || hostname.ends_with('-')
133 || hostname.starts_with('.')
134 || hostname.ends_with('.')
135 {
136 return Err(ockam_core::Error::new(
137 Origin::Core,
138 Kind::Parse,
139 format!("Hostname cannot start or end with a hyphen or dot {hostname}"),
140 ));
141 }
142
143 for segment in hostname.split('.') {
145 if segment.is_empty() {
147 return Err(ockam_core::Error::new(
148 Origin::Core,
149 Kind::Parse,
150 format!("Hostname segment cannot be empty {hostname}"),
151 ));
152 }
153
154 if segment.len() > 63 {
156 return Err(ockam_core::Error::new(
157 Origin::Core,
158 Kind::Parse,
159 format!("Hostname segment too long {hostname}"),
160 ));
161 }
162 if !segment
164 .chars()
165 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
166 {
167 return Err(ockam_core::Error::new(
168 Origin::Core,
169 Kind::Parse,
170 format!("Hostname contains invalid characters {hostname}"),
171 ));
172 }
173 }
174
175 Ok(Self {
176 hostname: hostname.to_string(),
177 port,
178 })
179 }
180
181 pub fn localhost(port: u16) -> Self {
182 Self {
183 hostname: Ipv4Addr::LOCALHOST.to_string(),
184 port,
185 }
186 }
187}
188
189impl From<SocketAddr> for HostnamePort {
190 fn from(socket_addr: SocketAddr) -> Self {
191 let ip = match socket_addr.ip() {
192 IpAddr::V4(ip) => ip.to_string(),
193 IpAddr::V6(ip) => format!("[{ip}]"),
194 };
195 Self {
196 hostname: ip,
197 port: socket_addr.port(),
198 }
199 }
200}
201
202impl Serialize for HostnamePort {
203 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204 where
205 S: Serializer,
206 {
207 serializer.serialize_str(&self.to_string())
208 }
209}
210
211impl<'de> Deserialize<'de> for HostnamePort {
212 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
213 where
214 D: Deserializer<'de>,
215 {
216 let s = String::deserialize(deserializer)?;
217 HostnamePort::from_str(&s).map_err(serde::de::Error::custom)
218 }
219}
220
221impl TryFrom<String> for HostnamePort {
222 type Error = ockam_core::Error;
223
224 fn try_from(value: String) -> ockam_core::Result<Self> {
225 FromStr::from_str(value.as_str())
226 }
227}
228
229impl TryFrom<&str> for HostnamePort {
230 type Error = ockam_core::Error;
231
232 fn try_from(value: &str) -> ockam_core::Result<Self> {
233 FromStr::from_str(value)
234 }
235}
236
237impl FromStr for HostnamePort {
238 type Err = ockam_core::Error;
239
240 fn from_str(hostname_port: &str) -> ockam_core::Result<HostnamePort> {
242 if let Ok(port) = hostname_port.parse::<u16>() {
244 return Ok(HostnamePort::localhost(port));
245 }
246
247 if let Some(port_str) = hostname_port.strip_prefix(':') {
248 if let Ok(port) = port_str.parse::<u16>() {
249 return Ok(HostnamePort::localhost(port));
250 }
251 }
252
253 if let Ok(socket) = parse_socket_addr(hostname_port) {
254 return Ok(HostnamePort::from(socket));
255 }
256
257 Self::validate(hostname_port)
259 }
260}
261
262impl Display for HostnamePort {
263 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
264 f.write_str(&format!("{}:{}", self.hostname, self.port))
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use core::str::FromStr;
272
273 #[test]
274 fn hostname_port_valid_inputs() -> ockam_core::Result<()> {
275 let valid_cases = vec![
276 ("localhost:80", HostnamePort::new("localhost", 80)?),
277 ("33domain:80", HostnamePort::new("33domain", 80)?),
278 ("127.0.0.1:80", HostnamePort::localhost(80)),
279 ("xn--74h.com:80", HostnamePort::new("xn--74h.com", 80)?),
280 (
281 "sub.xn_74h.com:80",
282 HostnamePort::new("sub.xn_74h.com", 80)?,
283 ),
284 (":80", HostnamePort::localhost(80)),
285 ("80", HostnamePort::localhost(80)),
286 (
287 "[2001:db8:85a3::8a2e:370:7334]:8080",
288 HostnamePort::new("[2001:db8:85a3::8a2e:370:7334]", 8080)?,
289 ),
290 ("[::1]:8080", HostnamePort::new("[::1]", 8080)?),
291 (
292 "[2001:db8:85a3::8a2e:370:7334]:8080",
293 HostnamePort::new("[2001:db8:85a3::8a2e:370:7334]", 8080)?,
294 ),
295 ];
296 for (input, expected) in valid_cases {
297 let actual = HostnamePort::from_str(input).ok().unwrap();
298 assert_eq!(actual, expected);
299 }
300
301 let socket_address_cases = vec![
302 (
303 SocketAddr::from_str("127.0.0.1:8080").unwrap(),
304 HostnamePort::localhost(8080),
305 ),
306 (
307 SocketAddr::from_str("[2001:db8:85a3::8a2e:370:7334]:8080").unwrap(),
308 HostnamePort::new("[2001:db8:85a3::8a2e:370:7334]", 8080)?,
309 ),
310 (
311 SocketAddr::from_str("[::1]:8080").unwrap(),
312 HostnamePort::new("[::1]", 8080)?,
313 ),
314 ];
315 for (input, expected) in socket_address_cases {
316 let actual = HostnamePort::from(input);
317 assert_eq!(actual, expected);
318 }
319
320 Ok(())
321 }
322
323 #[test]
324 fn hostname_port_invalid_inputs() {
325 let cases = [
326 "invalid",
327 "localhost:80:80",
328 "192,166,0.1:9999",
329 "-hostname-with-leading-hyphen:80",
330 "hostname-with-trailing-hyphen-:80",
331 ".hostname-with-leading-dot:80",
332 "hostname-with-trailing-dot.:80",
333 "hostname..with..multiple..dots:80",
334 "hostname_with_invalid_characters!@#:80",
335 "hostname_with_ space:80",
336 "hostname_with_backslash\\:80",
337 "hostname_with_slash/:80",
338 "hostname_with_colon::80",
339 "hostname_with_semicolon;:80",
340 "hostname_with_quote\":80",
341 "hostname_with_single_quote':80",
342 "hostname_with_question_mark?:80",
343 "hostname_with_asterisk*:80",
344 "hostname_with_ampersand&:80",
345 "hostname_with_percent%:80",
346 "hostname_with_dollar$:80",
347 "hostname_with_hash#:80",
348 "hostname_with_at@:80",
349 "hostname_with_exclamation!:80",
350 "hostname_with_tilde~:80",
351 "hostname_with_caret^:80",
352 "hostname_with_open_bracket[:80",
353 "hostname_with_close_bracket]:80",
354 "hostname_with_open_brace{:80",
355 "hostname_with_close_brace}:80",
356 "hostname_with_open_parenthesis(:80",
357 "hostname_with_close_parenthesis):80",
358 "hostname_with_plus+:80",
359 "hostname_with_equal=:80",
360 "hostname_with_comma,:80",
361 "hostname_with_less_than<:80",
362 "hostname_with_greater_than>:80",
363 ];
364 for case in cases.iter() {
365 if HostnamePort::from_str(case).is_ok() {
366 panic!("HostnamePort should fail for '{case}'");
367 }
368 }
369 }
370}