@@ -14,45 +14,112 @@ pub fn check_address(
1414 allowed_hosts : & Option < AllowedHosts > ,
1515 default : bool ,
1616) -> bool {
17- let Ok ( url ) = parse_url_with_host ( address, scheme) else {
17+ let Ok ( address ) = Address :: parse ( address, Some ( scheme) ) else {
1818 terminal:: warn!(
19- "A component tried to make a request to an address that could not be parsed as a url {address:? }."
20- ) ;
19+ "A component tried to make a request to an address that could not be parsed {address}." ,
20+ ) ;
2121 return false ;
2222 } ;
2323 let is_allowed = if let Some ( allowed_hosts) = allowed_hosts {
24- allowed_hosts. allows ( url . clone ( ) )
24+ allowed_hosts. allows ( & address )
2525 } else {
2626 default
2727 } ;
2828
2929 if !is_allowed {
30- terminal:: warn!( "A component tried to make a request to non-allowed address {address:?}." ) ;
31- if let ( Some ( host) , Some ( port) ) = ( url. host_str ( ) , url. port_or_known_default ( ) ) {
32- eprintln ! ( "To allow requests, add 'allowed_outbound_hosts = '[\" {host}:{port}\" ]' to the manifest component section." ) ;
33- }
30+ terminal:: warn!( "A component tried to make a request to non-allowed address '{address}'." ) ;
31+ let ( host, port) = ( address. host ( ) , address. port ( ) ) ;
32+ eprintln ! ( "To allow requests, add 'allowed_outbound_hosts = '[\" {host}:{port}\" ]' to the manifest component section." ) ;
3433 }
3534 is_allowed
3635}
3736
38- /// Try to parse the url that may or not include the provided scheme.
39- ///
40- /// If the parsing fails, the url is appended with the scheme and parsing
41- /// is tried again.
42- pub fn parse_url_with_host ( url : & str , scheme : & str ) -> anyhow:: Result < Url > {
43- match Url :: parse ( url) {
44- Ok ( url) if url. has_host ( ) => Ok ( url) ,
45- first_try => {
46- let second_try = format ! ( "{scheme}://{url}" )
47- . as_str ( )
48- . try_into ( )
49- . context ( "could not convert into a url" ) ;
50- match ( second_try, first_try. map_err ( |e| e. into ( ) ) ) {
51- ( Ok ( u) , _) => Ok ( u) ,
52- // Return an error preferring the error from the first attempt if present
53- ( _, Err ( e) ) | ( Err ( e) , _) => Err ( e) ,
37+ /// An address is a url-like string that contains a host, a port, and an optional scheme
38+ struct Address {
39+ inner : Url ,
40+ original : String ,
41+ has_scheme : bool ,
42+ }
43+
44+ impl Address {
45+ /// Try to parse the address.
46+ ///
47+ /// If the parsing fails, the address is prepended with the scheme and parsing
48+ /// is tried again.
49+ pub fn parse ( url : & str , scheme : Option < & str > ) -> anyhow:: Result < Self > {
50+ let mut has_scheme = true ;
51+ let mut parsed = match Url :: parse ( url) {
52+ Ok ( url) if url. has_host ( ) => Ok ( url) ,
53+ first_try => {
54+ // Parsing with 'scheme' resolves the ambiguity between 'spin.fermyon.com:80' and 'unix:80'.
55+ // Technically according to the spec a valid url *must* contain a scheme. However,
56+ // we allow url-like address strings without schemes, and we interpret the first part as the host.
57+ let second_try = format ! ( "{}://{url}" , scheme. unwrap_or( "scheme" ) )
58+ . as_str ( )
59+ . try_into ( )
60+ . context ( "could not convert into a url" ) ;
61+ has_scheme = false ;
62+ match ( second_try, first_try. map_err ( |e| e. into ( ) ) ) {
63+ ( Ok ( u) , _) => Ok ( u) ,
64+ // Return an error preferring the error from the first attempt if present
65+ ( _, Err ( e) ) | ( Err ( e) , _) => Err ( e) ,
66+ }
5467 }
68+ } ?;
69+
70+ if parsed. port_or_known_default ( ) . is_none ( ) {
71+ let _ = parsed. set_port ( well_known_port ( parsed. scheme ( ) ) ) ;
72+ }
73+
74+ Ok ( Self {
75+ inner : parsed,
76+ has_scheme,
77+ original : url. to_owned ( ) ,
78+ } )
79+ }
80+
81+ fn scheme ( & self ) -> Option < & str > {
82+ self . has_scheme . then_some ( self . inner . scheme ( ) )
83+ }
84+
85+ fn host ( & self ) -> & str {
86+ self . inner . host_str ( ) . unwrap_or_default ( )
87+ }
88+
89+ fn port ( & self ) -> u16 {
90+ self . inner
91+ . port_or_known_default ( )
92+ . or_else ( || well_known_port ( self . scheme ( ) ?) )
93+ . unwrap_or_default ( )
94+ }
95+
96+ fn validate_as_config ( & self ) -> anyhow:: Result < ( ) > {
97+ if ![ "" , "/" ] . contains ( & self . inner . path ( ) ) {
98+ anyhow:: bail!( "config '{}' contains a path" , self ) ;
99+ }
100+ if self . inner . query ( ) . is_some ( ) {
101+ anyhow:: bail!( "config '{}' contains a query string" , self ) ;
102+ }
103+ if self . port ( ) == 0 {
104+ anyhow:: bail!( "config '{}' did not contain port" , self )
55105 }
106+
107+ Ok ( ( ) )
108+ }
109+ }
110+
111+ impl std:: fmt:: Display for Address {
112+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
113+ f. write_str ( & self . original )
114+ }
115+ }
116+
117+ fn well_known_port ( scheme : & str ) -> Option < u16 > {
118+ match scheme {
119+ "postgres" => Some ( 5432 ) ,
120+ "mysql" => Some ( 3306 ) ,
121+ "redis" => Some ( 6379 ) ,
122+ _ => None ,
56123 }
57124}
58125
@@ -75,15 +142,10 @@ impl AllowedHosts {
75142 Ok ( Self :: SpecificHosts ( allowed) )
76143 }
77144
78- pub fn allows < U : TryInto < Url > > ( & self , url : U ) -> bool {
145+ fn allows ( & self , address : & Address ) -> bool {
79146 match self {
80147 AllowedHosts :: All => true ,
81- AllowedHosts :: SpecificHosts ( hosts) => {
82- let Ok ( url) = url. try_into ( ) else {
83- return false ;
84- } ;
85- hosts. iter ( ) . any ( |h| h. allows ( & url) )
86- }
148+ AllowedHosts :: SpecificHosts ( hosts) => hosts. iter ( ) . any ( |h| h. allows ( address) ) ,
87149 }
88150 }
89151}
@@ -103,53 +165,24 @@ pub struct AllowedHost {
103165
104166impl AllowedHost {
105167 fn parse < U : AsRef < str > > ( url : U ) -> anyhow:: Result < Self > {
106- let url_str = url. as_ref ( ) ;
107- let url: anyhow:: Result < Url > = url_str
108- . try_into ( )
109- . with_context ( || format ! ( "could not convert {url_str:?} into a url" ) ) ;
110- let ( url, has_scheme) = match url {
111- Ok ( url) if url. has_host ( ) => ( url, true ) ,
112- first_try => {
113- // If the url doesn't successfully parse try again with an added scheme.
114- // This resolves the ambiguity between 'spin.fermyon.com:80' and 'unix:80'.
115- // Technically according to the spec a valid url *must* contain a scheme. However,
116- // we allow url-like strings without schemes, and we interpret the first part as the host.
117- let second_try = format ! ( "scheme://{url_str}" )
118- . as_str ( )
119- . try_into ( )
120- . context ( "could not convert into a url" ) ;
121- match ( second_try, first_try) {
122- ( Ok ( u) , _) => ( u, false ) ,
123- // Return an error preferring the error from the first attempt if present
124- ( _, Err ( e) ) | ( Err ( e) , _) => return Err ( e) ,
125- }
126- }
127- } ;
128- let host = url. host_str ( ) . context ( "the url has no host" ) ?. to_owned ( ) ;
168+ let address = Address :: parse ( url. as_ref ( ) , None ) ?;
169+ address. validate_as_config ( ) ?;
129170
130- if ![ "" , "/" ] . contains ( & url. path ( ) ) {
131- anyhow:: bail!( "url contains a path" )
132- }
133- if url. query ( ) . is_some ( ) {
134- anyhow:: bail!( "url contains a query string" )
135- }
136171 Ok ( Self {
137- scheme : has_scheme. then ( || url. scheme ( ) . to_owned ( ) ) ,
138- host,
139- port : url
140- . port_or_known_default ( )
141- . context ( "url did not contain port" ) ?,
172+ scheme : address. scheme ( ) . map ( ToOwned :: to_owned) ,
173+ host : address. host ( ) . to_owned ( ) ,
174+ port : address. port ( ) ,
142175 } )
143176 }
144177
145- fn allows ( & self , url : & Url ) -> bool {
178+ fn allows ( & self , address : & Address ) -> bool {
146179 let scheme_matches = self
147180 . scheme
148- . as_ref ( )
149- . map ( |s| s == url . scheme ( ) )
181+ . as_deref ( )
182+ . map ( |s| Some ( s ) == address . scheme ( ) )
150183 . unwrap_or ( true ) ;
151- let host_matches = url . host_str ( ) . unwrap_or_default ( ) == self . host ;
152- let port_matches = url . port_or_known_default ( ) . unwrap_or_default ( ) == self . port ;
184+ let host_matches = address . host ( ) == self . host ;
185+ let port_matches = address . port ( ) == self . port ;
153186
154187 scheme_matches && host_matches && port_matches
155188 }
@@ -170,7 +203,7 @@ mod test {
170203 use super :: * ;
171204
172205 #[ test]
173- fn test_allowed_hosts_accepts_http_url ( ) {
206+ fn test_allowed_hosts_accepts_url ( ) {
174207 assert_eq ! (
175208 AllowedHost :: new( Some ( "http" ) , "spin.fermyon.dev" , 80 ) ,
176209 AllowedHost :: parse( "http://spin.fermyon.dev" ) . unwrap( )
@@ -183,10 +216,14 @@ mod test {
183216 AllowedHost :: new( Some ( "https" ) , "spin.fermyon.dev" , 443 ) ,
184217 AllowedHost :: parse( "https://spin.fermyon.dev" ) . unwrap( )
185218 ) ;
219+ assert_eq ! (
220+ AllowedHost :: new( Some ( "postgres" ) , "spin.fermyon.dev" , 5432 ) ,
221+ AllowedHost :: parse( "postgres://spin.fermyon.dev" ) . unwrap( )
222+ ) ;
186223 }
187224
188225 #[ test]
189- fn test_allowed_hosts_accepts_http_url_with_port ( ) {
226+ fn test_allowed_hosts_accepts_url_with_port ( ) {
190227 assert_eq ! (
191228 AllowedHost :: new( Some ( "http" ) , "spin.fermyon.dev" , 4444 ) ,
192229 AllowedHost :: parse( "http://spin.fermyon.dev:4444" ) . unwrap( )
@@ -281,9 +318,13 @@ mod test {
281318 fn test_allowed_hosts_can_be_specific ( ) {
282319 let allowed =
283320 AllowedHosts :: parse ( & [ "spin.fermyon.dev:443" , "http://example.com:8383" ] ) . unwrap ( ) ;
284- assert ! ( allowed. allows( Url :: parse( "http://example.com:8383/foo/bar" ) . unwrap( ) ) ) ;
285- assert ! ( allowed. allows( Url :: parse( "https://spin.fermyon.dev/" ) . unwrap( ) ) ) ;
286- assert ! ( !allowed. allows( Url :: parse( "http://example.com/" ) . unwrap( ) ) ) ;
287- assert ! ( !allowed. allows( Url :: parse( "http://google.com/" ) . unwrap( ) ) ) ;
321+ assert ! ( allowed
322+ . allows( & Address :: parse( "http://example.com:8383/foo/bar" , Some ( "http" ) ) . unwrap( ) ) ) ;
323+ assert ! (
324+ allowed. allows( & Address :: parse( "https://spin.fermyon.dev/" , Some ( "https" ) ) . unwrap( ) )
325+ ) ;
326+ assert ! ( !allowed. allows( & Address :: parse( "http://example.com/" , Some ( "http" ) ) . unwrap( ) ) ) ;
327+ assert ! ( !allowed. allows( & Address :: parse( "http://google.com/" , Some ( "http" ) ) . unwrap( ) ) ) ;
328+ assert ! ( allowed. allows( & Address :: parse( "spin.fermyon.dev:443" , Some ( "https" ) ) . unwrap( ) ) ) ;
288329 }
289330}
0 commit comments