Skip to content

Commit f4daf9f

Browse files
committed
geolocation: add client-side validation for result destination
1 parent 5590c23 commit f4daf9f

1 file changed

Lines changed: 91 additions & 3 deletions

File tree

smartcontract/cli/src/geolocation/user/set_result_destination.rs

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::{geoclicommand::GeoCliCommand, validators::validate_code};
22
use clap::Args;
3+
use doublezero_geolocation::validation::validate_public_ip;
34
use doublezero_sdk::geolocation::geolocation_user::{
45
get::GetGeolocationUserCommand, set_result_destination::SetResultDestinationCommand,
56
};
67
use solana_sdk::pubkey::Pubkey;
7-
use std::io::Write;
8+
use std::{io::Write, net::Ipv4Addr};
89

910
#[derive(Args, Debug)]
1011
pub struct SetResultDestinationCliCommand {
@@ -19,13 +20,69 @@ pub struct SetResultDestinationCliCommand {
1920
pub clear: bool,
2021
}
2122

23+
fn validate_domain(host: &str) -> eyre::Result<()> {
24+
if host.len() > 253 {
25+
return Err(eyre::eyre!(
26+
"domain too long ({} chars, max 253)",
27+
host.len()
28+
));
29+
}
30+
let labels: Vec<&str> = host.split('.').collect();
31+
if labels.len() < 2 {
32+
return Err(eyre::eyre!(
33+
"domain must have at least two labels (e.g., \"example.com\")"
34+
));
35+
}
36+
for label in &labels {
37+
if label.is_empty() || label.len() > 63 {
38+
return Err(eyre::eyre!("invalid domain label length: {}", label.len()));
39+
}
40+
if label.starts_with('-') || label.ends_with('-') {
41+
return Err(eyre::eyre!(
42+
"domain label \"{}\" cannot start or end with a hyphen",
43+
label
44+
));
45+
}
46+
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
47+
return Err(eyre::eyre!(
48+
"domain label \"{}\" contains invalid characters",
49+
label
50+
));
51+
}
52+
}
53+
Ok(())
54+
}
55+
56+
fn validate_destination(destination: &str) -> eyre::Result<()> {
57+
let colon_pos = destination.rfind(':').ok_or_else(|| {
58+
eyre::eyre!("invalid destination \"{destination}\": expected host:port format")
59+
})?;
60+
let host = &destination[..colon_pos];
61+
let port_str = &destination[colon_pos + 1..];
62+
63+
port_str
64+
.parse::<u16>()
65+
.map_err(|_| eyre::eyre!("invalid port \"{port_str}\": must be a number 0-65535"))?;
66+
67+
if let Ok(ip) = host.parse::<Ipv4Addr>() {
68+
validate_public_ip(&ip).map_err(|e| eyre::eyre!("invalid IP address {host}: {e}"))?;
69+
return Ok(());
70+
}
71+
72+
validate_domain(host)?;
73+
Ok(())
74+
}
75+
2276
impl SetResultDestinationCliCommand {
2377
pub fn execute<C: GeoCliCommand, W: Write>(self, client: &C, out: &mut W) -> eyre::Result<()> {
2478
let destination = if self.clear {
2579
String::new()
2680
} else {
27-
self.destination
28-
.ok_or_else(|| eyre::eyre!("--destination is required (or use --clear)"))?
81+
let dest = self
82+
.destination
83+
.ok_or_else(|| eyre::eyre!("--destination is required (or use --clear)"))?;
84+
validate_destination(&dest)?;
85+
dest
2986
};
3087

3188
let (_, user) = client.get_geolocation_user(GetGeolocationUserCommand {
@@ -195,4 +252,35 @@ mod tests {
195252
assert!(res.is_err());
196253
assert!(res.unwrap_err().to_string().contains("--destination"));
197254
}
255+
256+
#[test]
257+
fn test_cli_set_result_destination_invalid_destinations() {
258+
let cases = vec![
259+
("no-port", "expected host:port"),
260+
("10.0.0.1:9000", "invalid IP"),
261+
("192.168.1.1:9000", "invalid IP"),
262+
("example.com:99999", "invalid port"),
263+
("example.com:abc", "invalid port"),
264+
("bad..domain:80", "invalid domain label length"),
265+
("-bad.example.com:80", "cannot start or end with a hyphen"),
266+
("localhost:9000", "at least two labels"),
267+
("bad_label.example.com:80", "invalid characters"),
268+
];
269+
for (dest, expected_msg) in cases {
270+
let client = MockGeoCliCommand::new();
271+
let mut output = Vec::new();
272+
let res = SetResultDestinationCliCommand {
273+
user: "geo-user-01".to_string(),
274+
destination: Some(dest.to_string()),
275+
clear: false,
276+
}
277+
.execute(&client, &mut output);
278+
assert!(res.is_err(), "expected error for destination \"{dest}\"");
279+
let err = res.unwrap_err().to_string();
280+
assert!(
281+
err.contains(expected_msg),
282+
"destination \"{dest}\": expected error containing \"{expected_msg}\", got \"{err}\""
283+
);
284+
}
285+
}
198286
}

0 commit comments

Comments
 (0)