@@ -13,6 +13,18 @@ pub struct TenantManager {
1313 tenants_dir : PathBuf ,
1414}
1515
16+ fn is_valid_subdomain ( s : & str ) -> bool {
17+ let len = s. len ( ) ;
18+ if len == 0 || len > 63 {
19+ return false ;
20+ }
21+ if s. starts_with ( '-' ) || s. ends_with ( '-' ) {
22+ return false ;
23+ }
24+ s. bytes ( )
25+ . all ( |b| b. is_ascii_lowercase ( ) || b. is_ascii_digit ( ) || b == b'-' )
26+ }
27+
1628impl TenantManager {
1729 #[ must_use]
1830 pub fn new ( tenants_dir : PathBuf ) -> Self {
@@ -47,16 +59,106 @@ impl TenantManager {
4759 }
4860
4961 #[ must_use]
50- #[ allow( clippy:: option_if_let_else) ]
5162 pub fn extract_subdomain ( host : & str ) -> String {
52- if let Some ( subdomain) = host. split ( '.' ) . next ( ) {
53- if subdomain == "srcuri" || subdomain. contains ( ':' ) {
54- "default" . to_string ( )
55- } else {
56- subdomain. to_string ( )
57- }
58- } else {
59- "default" . to_string ( )
63+ let host_without_port = host. split ( ':' ) . next ( ) . unwrap_or ( host) ;
64+
65+ match host_without_port. split ( '.' ) . next ( ) {
66+ Some ( "srcuri" ) => "default" . to_string ( ) ,
67+ Some ( sub) if is_valid_subdomain ( sub) => sub. to_string ( ) ,
68+ _ => "default" . to_string ( ) ,
6069 }
6170 }
6271}
72+
73+ #[ cfg( test) ]
74+ mod tests {
75+ use super :: * ;
76+
77+ #[ test]
78+ fn valid_subdomains ( ) {
79+ assert_eq ! ( TenantManager :: extract_subdomain( "acme.srcuri.com" ) , "acme" ) ;
80+ assert_eq ! (
81+ TenantManager :: extract_subdomain( "my-tenant.srcuri.com" ) ,
82+ "my-tenant"
83+ ) ;
84+ assert_eq ! ( TenantManager :: extract_subdomain( "a1b2.srcuri.com" ) , "a1b2" ) ;
85+ }
86+
87+ #[ test]
88+ fn srcuri_returns_default ( ) {
89+ assert_eq ! ( TenantManager :: extract_subdomain( "srcuri.com" ) , "default" ) ;
90+ }
91+
92+ #[ test]
93+ fn path_traversal_returns_default ( ) {
94+ assert_eq ! (
95+ TenantManager :: extract_subdomain( "../etc.srcuri.com" ) ,
96+ "default"
97+ ) ;
98+ assert_eq ! (
99+ TenantManager :: extract_subdomain( "foo/bar.srcuri.com" ) ,
100+ "default"
101+ ) ;
102+ assert_eq ! (
103+ TenantManager :: extract_subdomain( "..%2fetc.srcuri.com" ) ,
104+ "default"
105+ ) ;
106+ }
107+
108+ #[ test]
109+ fn uppercase_returns_default ( ) {
110+ assert_eq ! (
111+ TenantManager :: extract_subdomain( "ACME.srcuri.com" ) ,
112+ "default"
113+ ) ;
114+ assert_eq ! (
115+ TenantManager :: extract_subdomain( "Acme.srcuri.com" ) ,
116+ "default"
117+ ) ;
118+ }
119+
120+ #[ test]
121+ fn empty_returns_default ( ) {
122+ assert_eq ! ( TenantManager :: extract_subdomain( "" ) , "default" ) ;
123+ assert_eq ! ( TenantManager :: extract_subdomain( ".srcuri.com" ) , "default" ) ;
124+ }
125+
126+ #[ test]
127+ fn too_long_returns_default ( ) {
128+ let long = format ! ( "{}.srcuri.com" , "a" . repeat( 64 ) ) ;
129+ assert_eq ! ( TenantManager :: extract_subdomain( & long) , "default" ) ;
130+ }
131+
132+ #[ test]
133+ fn leading_trailing_hyphen_returns_default ( ) {
134+ assert_eq ! (
135+ TenantManager :: extract_subdomain( "-acme.srcuri.com" ) ,
136+ "default"
137+ ) ;
138+ assert_eq ! (
139+ TenantManager :: extract_subdomain( "acme-.srcuri.com" ) ,
140+ "default"
141+ ) ;
142+ }
143+
144+ #[ test]
145+ fn port_stripping_works ( ) {
146+ assert_eq ! (
147+ TenantManager :: extract_subdomain( "acme.srcuri.com:8080" ) ,
148+ "acme"
149+ ) ;
150+ assert_eq ! (
151+ TenantManager :: extract_subdomain( "srcuri.com:3000" ) ,
152+ "default"
153+ ) ;
154+ }
155+
156+ #[ test]
157+ fn bare_hostname_without_dots ( ) {
158+ assert_eq ! ( TenantManager :: extract_subdomain( "localhost" ) , "localhost" ) ;
159+ assert_eq ! (
160+ TenantManager :: extract_subdomain( "localhost:3000" ) ,
161+ "localhost"
162+ ) ;
163+ }
164+ }
0 commit comments