@@ -3,6 +3,7 @@ use std::ops::Range;
3
3
use anyhow:: { bail, ensure, Context } ;
4
4
use spin_factors:: { App , AppComponent } ;
5
5
use spin_locked_app:: MetadataKey ;
6
+ use url:: Host ;
6
7
7
8
const ALLOWED_HOSTS_KEY : MetadataKey < Vec < String > > = MetadataKey :: new ( "allowed_outbound_hosts" ) ;
8
9
const ALLOWED_HTTP_KEY : MetadataKey < Vec < String > > = MetadataKey :: new ( "allowed_http_hosts" ) ;
@@ -105,10 +106,17 @@ impl AllowedHostConfig {
105
106
None => rest,
106
107
} ;
107
108
109
+ let port = PortConfig :: parse ( port, scheme)
110
+ . with_context ( || format ! ( "Invalid allowed host port {port:?}" ) ) ?;
111
+ let scheme = SchemeConfig :: parse ( scheme)
112
+ . with_context ( || format ! ( "Invalid allowed host scheme {scheme:?}" ) ) ?;
113
+ let host =
114
+ HostConfig :: parse ( host) . with_context ( || format ! ( "Invalid allowed host {host:?}" ) ) ?;
115
+
108
116
Ok ( Self {
109
- scheme : SchemeConfig :: parse ( scheme ) ? ,
110
- host : HostConfig :: parse ( host ) ? ,
111
- port : PortConfig :: parse ( port , scheme ) ? ,
117
+ scheme,
118
+ host,
119
+ port,
112
120
original,
113
121
} )
114
122
}
@@ -128,6 +136,11 @@ impl AllowedHostConfig {
128
136
& self . port
129
137
}
130
138
139
+ /// Returns true if this config is for service chaining requests.
140
+ pub fn is_for_service_chaining ( & self ) -> bool {
141
+ self . host . is_for_service_chaining ( )
142
+ }
143
+
131
144
/// Returns true if the given url is allowed by this config.
132
145
fn allows ( & self , url : & OutboundUrl ) -> bool {
133
146
self . scheme . allows ( & url. scheme )
@@ -172,11 +185,11 @@ impl SchemeConfig {
172
185
173
186
if scheme. starts_with ( '{' ) {
174
187
// TODO:
175
- bail ! ( "scheme lists are not yet supported" )
188
+ anyhow :: bail!( "scheme lists are not yet supported" )
176
189
}
177
190
178
191
if scheme. chars ( ) . any ( |c| !c. is_alphabetic ( ) ) {
179
- anyhow:: bail!( " scheme {scheme:?} contains non alphabetic character" ) ;
192
+ anyhow:: bail!( "only alphabetic character(s) are allowed " ) ;
180
193
}
181
194
182
195
Ok ( Self :: List ( vec ! [ scheme. into( ) ] ) )
@@ -202,7 +215,7 @@ pub enum HostConfig {
202
215
Any ,
203
216
AnySubdomain ( String ) ,
204
217
ToSelf ,
205
- List ( Vec < String > ) ,
218
+ Literal ( Host ) ,
206
219
Cidr ( ip_network:: IpNetwork ) ,
207
220
}
208
221
@@ -227,49 +240,66 @@ impl HostConfig {
227
240
return Ok ( Self :: Cidr ( net) ) ;
228
241
}
229
242
230
- if matches ! ( host. split( '/' ) . nth( 1 ) , Some ( path) if !path. is_empty( ) ) {
231
- bail ! ( "hosts must not contain paths" ) ;
243
+ host = host. trim_end_matches ( '/' ) ;
244
+ if host. contains ( '/' ) {
245
+ bail ! ( "must not include a path" ) ;
232
246
}
233
247
234
248
if let Some ( domain) = host. strip_prefix ( "*." ) {
235
249
if domain. contains ( '*' ) {
236
- bail ! ( "Invalid allowed host {host}: wildcards are allowed only as prefixes" ) ;
250
+ bail ! ( "wildcards are allowed only as prefixes" ) ;
237
251
}
238
252
return Ok ( Self :: AnySubdomain ( format ! ( ".{domain}" ) ) ) ;
239
253
}
240
254
241
255
if host. contains ( '*' ) {
242
- bail ! ( "Invalid allowed host {host}: wildcards are allowed only as subdomains" ) ;
256
+ bail ! ( "wildcards are allowed only as subdomains" ) ;
243
257
}
244
258
245
- // Remove trailing slashes
246
- host = host . trim_end_matches ( '/' ) ;
259
+ Self :: literal ( host )
260
+ }
247
261
248
- Ok ( Self :: List ( vec ! [ host. into( ) ] ) )
262
+ /// Returns a HostConfig from the given literal host name.
263
+ fn literal ( host : & str ) -> anyhow:: Result < Self > {
264
+ Ok ( Self :: Literal ( Host :: parse ( host) ?) )
249
265
}
250
266
251
267
/// Returns true if the given host is allowed by this config.
252
268
fn allows ( & self , host : & str ) -> bool {
253
- match self {
254
- HostConfig :: Any => true ,
255
- HostConfig :: AnySubdomain ( suffix) => host. ends_with ( suffix) ,
256
- HostConfig :: List ( l) => l. iter ( ) . any ( |h| h. as_str ( ) == host) ,
257
- HostConfig :: ToSelf => false ,
258
- HostConfig :: Cidr ( c) => {
259
- let Ok ( ip) = host. parse :: < std:: net:: IpAddr > ( ) else {
260
- return false ;
261
- } ;
262
- c. contains ( ip)
269
+ let host: Host = match Host :: parse ( host) {
270
+ Ok ( host) => host,
271
+ Err ( err) => {
272
+ tracing:: warn!( ?err, "invalid host in HostConfig::allows" ) ;
273
+ return false ;
263
274
}
275
+ } ;
276
+ match ( self , host) {
277
+ ( HostConfig :: Any , _) => true ,
278
+ ( HostConfig :: AnySubdomain ( suffix) , Host :: Domain ( domain) ) => domain. ends_with ( suffix) ,
279
+ ( HostConfig :: Literal ( literal) , host) => host == * literal,
280
+ ( HostConfig :: Cidr ( c) , Host :: Ipv4 ( ip) ) => c. contains ( ip) ,
281
+ ( HostConfig :: Cidr ( c) , Host :: Ipv6 ( ip) ) => c. contains ( ip) ,
282
+ // Note: HostConfig::ToSelf is checked with ::allow_relative
283
+ _ => false ,
264
284
}
265
285
}
266
286
267
287
/// Returns true if this config allows relative ("self") requests.
268
288
fn allows_relative ( & self ) -> bool {
269
289
matches ! ( self , Self :: Any | Self :: ToSelf )
270
290
}
291
+
292
+ /// Returns true if this config is for service chaining requests.
293
+ fn is_for_service_chaining ( & self ) -> bool {
294
+ match self {
295
+ Self :: Literal ( Host :: Domain ( domain) ) => domain. ends_with ( SERVICE_CHAINING_DOMAIN_SUFFIX ) ,
296
+ Self :: AnySubdomain ( suffix) => suffix == SERVICE_CHAINING_DOMAIN_SUFFIX ,
297
+ _ => false ,
298
+ }
299
+ }
271
300
}
272
301
302
+ /// Represents the port part of an allowed_outbound_hosts item.
273
303
#[ derive( Debug , PartialEq , Eq , Clone ) ]
274
304
pub enum PortConfig {
275
305
Any ,
@@ -561,9 +591,10 @@ mod test {
561
591
}
562
592
563
593
impl HostConfig {
564
- fn new ( host : & str ) -> Self {
565
- Self :: List ( vec ! [ host. into ( ) ] )
594
+ fn unwrap_literal ( host : & str ) -> Self {
595
+ Self :: literal ( host) . unwrap_or_else ( |_| panic ! ( "invalid host {host:?}" ) )
566
596
}
597
+
567
598
fn subdomain ( domain : & str ) -> Self {
568
599
Self :: AnySubdomain ( format ! ( ".{domain}" ) )
569
600
}
@@ -612,7 +643,7 @@ mod test {
612
643
assert_eq ! (
613
644
AllowedHostConfig :: new(
614
645
SchemeConfig :: new( "http" ) ,
615
- HostConfig :: new ( "spin.fermyon.dev" ) ,
646
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
616
647
PortConfig :: new( 80 )
617
648
) ,
618
649
AllowedHostConfig :: parse( "http://spin.fermyon.dev" ) . unwrap( )
@@ -622,7 +653,7 @@ mod test {
622
653
AllowedHostConfig :: new(
623
654
SchemeConfig :: new( "http" ) ,
624
655
// Trailing slash is removed
625
- HostConfig :: new ( "spin.fermyon.dev" ) ,
656
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
626
657
PortConfig :: new( 80 )
627
658
) ,
628
659
AllowedHostConfig :: parse( "http://spin.fermyon.dev/" ) . unwrap( )
@@ -631,7 +662,7 @@ mod test {
631
662
assert_eq ! (
632
663
AllowedHostConfig :: new(
633
664
SchemeConfig :: new( "https" ) ,
634
- HostConfig :: new ( "spin.fermyon.dev" ) ,
665
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
635
666
PortConfig :: new( 443 )
636
667
) ,
637
668
AllowedHostConfig :: parse( "https://spin.fermyon.dev" ) . unwrap( )
@@ -643,23 +674,23 @@ mod test {
643
674
assert_eq ! (
644
675
AllowedHostConfig :: new(
645
676
SchemeConfig :: new( "http" ) ,
646
- HostConfig :: new ( "spin.fermyon.dev" ) ,
677
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
647
678
PortConfig :: new( 4444 )
648
679
) ,
649
680
AllowedHostConfig :: parse( "http://spin.fermyon.dev:4444" ) . unwrap( )
650
681
) ;
651
682
assert_eq ! (
652
683
AllowedHostConfig :: new(
653
684
SchemeConfig :: new( "http" ) ,
654
- HostConfig :: new ( "spin.fermyon.dev" ) ,
685
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
655
686
PortConfig :: new( 4444 )
656
687
) ,
657
688
AllowedHostConfig :: parse( "http://spin.fermyon.dev:4444/" ) . unwrap( )
658
689
) ;
659
690
assert_eq ! (
660
691
AllowedHostConfig :: new(
661
692
SchemeConfig :: new( "https" ) ,
662
- HostConfig :: new ( "spin.fermyon.dev" ) ,
693
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
663
694
PortConfig :: new( 5555 )
664
695
) ,
665
696
AllowedHostConfig :: parse( "https://spin.fermyon.dev:5555" ) . unwrap( )
@@ -671,7 +702,7 @@ mod test {
671
702
assert_eq ! (
672
703
AllowedHostConfig :: new(
673
704
SchemeConfig :: new( "http" ) ,
674
- HostConfig :: new ( "spin.fermyon.dev" ) ,
705
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
675
706
PortConfig :: range( 4444 ..5555 )
676
707
) ,
677
708
AllowedHostConfig :: parse( "http://spin.fermyon.dev:4444..5555" ) . unwrap( )
@@ -693,7 +724,7 @@ mod test {
693
724
assert_eq ! (
694
725
AllowedHostConfig :: new(
695
726
SchemeConfig :: Any ,
696
- HostConfig :: new ( "spin.fermyon.dev" ) ,
727
+ HostConfig :: unwrap_literal ( "spin.fermyon.dev" ) ,
697
728
PortConfig :: new( 7777 )
698
729
) ,
699
730
AllowedHostConfig :: parse( "*://spin.fermyon.dev:7777" ) . unwrap( )
@@ -718,7 +749,7 @@ mod test {
718
749
assert_eq ! (
719
750
AllowedHostConfig :: new(
720
751
SchemeConfig :: new( "http" ) ,
721
- HostConfig :: new ( "localhost" ) ,
752
+ HostConfig :: unwrap_literal ( "localhost" ) ,
722
753
PortConfig :: new( 80 )
723
754
) ,
724
755
AllowedHostConfig :: parse( "http://localhost" ) . unwrap( )
@@ -727,7 +758,7 @@ mod test {
727
758
assert_eq ! (
728
759
AllowedHostConfig :: new(
729
760
SchemeConfig :: new( "http" ) ,
730
- HostConfig :: new ( "localhost" ) ,
761
+ HostConfig :: unwrap_literal ( "localhost" ) ,
731
762
PortConfig :: new( 3001 )
732
763
) ,
733
764
AllowedHostConfig :: parse( "http://localhost:3001" ) . unwrap( )
@@ -751,23 +782,23 @@ mod test {
751
782
assert_eq ! (
752
783
AllowedHostConfig :: new(
753
784
SchemeConfig :: new( "http" ) ,
754
- HostConfig :: new ( "192.168.1.1" ) ,
785
+ HostConfig :: unwrap_literal ( "192.168.1.1" ) ,
755
786
PortConfig :: new( 80 )
756
787
) ,
757
788
AllowedHostConfig :: parse( "http://192.168.1.1" ) . unwrap( )
758
789
) ;
759
790
assert_eq ! (
760
791
AllowedHostConfig :: new(
761
792
SchemeConfig :: new( "http" ) ,
762
- HostConfig :: new ( "192.168.1.1" ) ,
793
+ HostConfig :: unwrap_literal ( "192.168.1.1" ) ,
763
794
PortConfig :: new( 3002 )
764
795
) ,
765
796
AllowedHostConfig :: parse( "http://192.168.1.1:3002" ) . unwrap( )
766
797
) ;
767
798
assert_eq ! (
768
799
AllowedHostConfig :: new(
769
800
SchemeConfig :: new( "http" ) ,
770
- HostConfig :: new ( "[::1]" ) ,
801
+ HostConfig :: unwrap_literal ( "[::1]" ) ,
771
802
PortConfig :: new( 8001 )
772
803
) ,
773
804
AllowedHostConfig :: parse( "http://[::1]:8001" ) . unwrap( )
@@ -811,6 +842,19 @@ mod test {
811
842
assert ! ( AllowedHostConfig :: parse( "http://*.fermyon.dev/a" ) . is_err( ) ) ;
812
843
}
813
844
845
+ #[ test]
846
+ fn test_rejects_invalid_parts ( ) {
847
+ for invalid in [
848
+ "h@x://localhost" ,
849
+ "http://invalid host" ,
850
+ "http://inv@lid-host" ,
851
+ "http://" ,
852
+ "http://:80" ,
853
+ ] {
854
+ AllowedHostConfig :: parse ( invalid) . expect_err ( invalid) ;
855
+ }
856
+ }
857
+
814
858
#[ test]
815
859
fn test_allowed_hosts_respects_allow_all ( ) {
816
860
assert ! ( AllowedHostsConfig :: parse( & [ "insecure:allow-all" ] , & dummy_resolver( ) ) . is_err( ) ) ;
0 commit comments