11use crate :: { geoclicommand:: GeoCliCommand , validators:: validate_code} ;
22use clap:: Args ;
3+ use doublezero_geolocation:: validation:: validate_public_ip;
34use doublezero_sdk:: geolocation:: geolocation_user:: {
45 get:: GetGeolocationUserCommand , set_result_destination:: SetResultDestinationCommand ,
56} ;
67use solana_sdk:: pubkey:: Pubkey ;
7- use std:: io:: Write ;
8+ use std:: { io:: Write , net :: Ipv4Addr } ;
89
910#[ derive( Args , Debug ) ]
1011pub 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+
2276impl 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