@@ -5,6 +5,7 @@ use fastly::backend::Backend;
55use url:: Url ;
66
77use crate :: error:: TrustedServerError ;
8+ use crate :: host_header:: validate_host_header_override_value;
89
910/// Returns the default port for the given scheme (443 for HTTPS, 80 for HTTP).
1011#[ inline]
@@ -33,6 +34,19 @@ fn compute_host_header(scheme: &str, host: &str, port: u16) -> String {
3334 }
3435}
3536
37+ fn sanitize_backend_name_component ( value : & str ) -> String {
38+ value
39+ . chars ( )
40+ . map ( |ch| {
41+ if ch. is_ascii_alphanumeric ( ) || matches ! ( ch, '-' | '_' ) {
42+ ch
43+ } else {
44+ '_'
45+ }
46+ } )
47+ . collect ( )
48+ }
49+
3650/// Default first-byte timeout for backends (15 seconds).
3751pub ( crate ) const DEFAULT_FIRST_BYTE_TIMEOUT : Duration = Duration :: from_secs ( 15 ) ;
3852
@@ -46,6 +60,7 @@ pub struct BackendConfig<'a> {
4660 port : Option < u16 > ,
4761 certificate_check : bool ,
4862 first_byte_timeout : Duration ,
63+ host_header_override : Option < & ' a str > ,
4964}
5065
5166impl < ' a > BackendConfig < ' a > {
@@ -61,6 +76,7 @@ impl<'a> BackendConfig<'a> {
6176 port : None ,
6277 certificate_check : true ,
6378 first_byte_timeout : DEFAULT_FIRST_BYTE_TIMEOUT ,
79+ host_header_override : None ,
6480 }
6581 }
6682
@@ -90,6 +106,13 @@ impl<'a> BackendConfig<'a> {
90106 self
91107 }
92108
109+ /// Set the outbound Host header sent to the backend origin.
110+ #[ must_use]
111+ pub fn host_header_override ( mut self , host : Option < & ' a str > ) -> Self {
112+ self . host_header_override = host;
113+ self
114+ }
115+
93116 /// Compute the deterministic backend name and resolved port without
94117 /// registering anything.
95118 ///
@@ -114,21 +137,33 @@ impl<'a> BackendConfig<'a> {
114137 message : "scheme contains control characters" . to_string ( ) ,
115138 } ) ) ;
116139 }
140+ if let Some ( host_header_override) = self . host_header_override {
141+ validate_host_header_override_value ( host_header_override) . map_err ( |reason| {
142+ Report :: new ( TrustedServerError :: Proxy {
143+ message : format ! ( "host header override {reason}" ) ,
144+ } )
145+ } ) ?;
146+ }
117147
118148 let target_port = self
119149 . port
120150 . unwrap_or_else ( || default_port_for_scheme ( self . scheme ) ) ;
121151
122152 let name_base = format ! ( "{}_{}_{}" , self . scheme, self . host, target_port) ;
153+ let host_override_suffix = self
154+ . host_header_override
155+ . map ( |host| format ! ( "_oh_{}" , sanitize_backend_name_component( host) ) )
156+ . unwrap_or_default ( ) ;
123157 let cert_suffix = if self . certificate_check {
124158 ""
125159 } else {
126160 "_nocert"
127161 } ;
128162 let timeout_ms = self . first_byte_timeout . as_millis ( ) ;
129163 let backend_name = format ! (
130- "backend_{}{}_t{}" ,
131- name_base. replace( [ '.' , ':' ] , "_" ) ,
164+ "backend_{}{}{}_t{}" ,
165+ sanitize_backend_name_component( & name_base) ,
166+ host_override_suffix,
132167 cert_suffix,
133168 timeout_ms
134169 ) ;
@@ -165,7 +200,10 @@ impl<'a> BackendConfig<'a> {
165200
166201 let host_with_port = format ! ( "{}:{}" , self . host, target_port) ;
167202
168- let host_header = compute_host_header ( self . scheme , self . host , target_port) ;
203+ let host_header = self
204+ . host_header_override
205+ . map ( str:: to_owned)
206+ . unwrap_or_else ( || compute_host_header ( self . scheme , self . host , target_port) ) ;
169207
170208 // Target base is host[:port]; SSL is enabled only for https scheme
171209 let mut builder = Backend :: builder ( & backend_name, & host_with_port)
@@ -219,6 +257,7 @@ impl<'a> BackendConfig<'a> {
219257 ///
220258 /// Centralises URL parsing so that [`from_url`](Self::from_url),
221259 /// [`from_url_with_first_byte_timeout`](Self::from_url_with_first_byte_timeout),
260+ /// [`from_url_with_host_header_override`](Self::from_url_with_host_header_override),
222261 /// and [`backend_name_for_url`](Self::backend_name_for_url) share one
223262 /// code-path.
224263 fn parse_origin (
@@ -278,13 +317,47 @@ impl<'a> BackendConfig<'a> {
278317 origin_url : & str ,
279318 certificate_check : bool ,
280319 first_byte_timeout : Duration ,
320+ ) -> Result < String , Report < TrustedServerError > > {
321+ Self :: from_url_with_first_byte_timeout_and_host_header_override (
322+ origin_url,
323+ certificate_check,
324+ first_byte_timeout,
325+ None ,
326+ )
327+ }
328+
329+ /// Parse an origin URL and ensure a dynamic backend with a custom Host header.
330+ ///
331+ /// # Errors
332+ ///
333+ /// Returns an error if the URL cannot be parsed or lacks a host, or if
334+ /// backend creation fails.
335+ pub fn from_url_with_host_header_override (
336+ origin_url : & str ,
337+ certificate_check : bool ,
338+ host_header_override : Option < & str > ,
339+ ) -> Result < String , Report < TrustedServerError > > {
340+ Self :: from_url_with_first_byte_timeout_and_host_header_override (
341+ origin_url,
342+ certificate_check,
343+ DEFAULT_FIRST_BYTE_TIMEOUT ,
344+ host_header_override,
345+ )
346+ }
347+
348+ fn from_url_with_first_byte_timeout_and_host_header_override (
349+ origin_url : & str ,
350+ certificate_check : bool ,
351+ first_byte_timeout : Duration ,
352+ host_header_override : Option < & str > ,
281353 ) -> Result < String , Report < TrustedServerError > > {
282354 let ( scheme, host, port) = Self :: parse_origin ( origin_url) ?;
283355
284356 BackendConfig :: new ( & scheme, & host)
285357 . port ( port)
286358 . certificate_check ( certificate_check)
287359 . first_byte_timeout ( first_byte_timeout)
360+ . host_header_override ( host_header_override)
288361 . ensure ( )
289362 }
290363
@@ -437,6 +510,65 @@ mod tests {
437510 ) ;
438511 }
439512
513+ #[ test]
514+ fn host_header_overrides_produce_different_names ( ) {
515+ let ( name_a, _) = BackendConfig :: new ( "https" , "origin.example.com" )
516+ . host_header_override ( Some ( "www.example.com" ) )
517+ . compute_name ( )
518+ . expect ( "should compute name with host header override" ) ;
519+ let ( name_b, _) = BackendConfig :: new ( "https" , "origin.example.com" )
520+ . host_header_override ( Some ( "m.example.com" ) )
521+ . compute_name ( )
522+ . expect ( "should compute name with different host header override" ) ;
523+
524+ assert_ne ! (
525+ name_a, name_b,
526+ "backends with different host header overrides should have different names"
527+ ) ;
528+ assert_eq ! (
529+ name_a,
530+ "backend_https_origin_example_com_443_oh_www_example_com_t15000"
531+ ) ;
532+ assert_eq ! (
533+ name_b,
534+ "backend_https_origin_example_com_443_oh_m_example_com_t15000"
535+ ) ;
536+ }
537+
538+ #[ test]
539+ fn host_header_override_rejects_control_characters ( ) {
540+ let err = BackendConfig :: new ( "https" , "origin.example.com" )
541+ . host_header_override ( Some ( "www\n .example.com" ) )
542+ . predict_name ( )
543+ . expect_err ( "should reject host header override containing newline" ) ;
544+
545+ assert ! (
546+ err. to_string( ) . contains( "control characters" ) ,
547+ "should report control characters in error message"
548+ ) ;
549+ }
550+
551+ #[ test]
552+ fn host_header_override_rejects_invalid_values ( ) {
553+ for host_header_override in [
554+ "https://www.example.com" ,
555+ "www.example.com/path" ,
556+ "www.example.com:" ,
557+ "example..com" ,
558+ "-" ,
559+ ] {
560+ let err = BackendConfig :: new ( "https" , "origin.example.com" )
561+ . host_header_override ( Some ( host_header_override) )
562+ . predict_name ( )
563+ . expect_err ( "should reject invalid host header override" ) ;
564+
565+ assert ! (
566+ err. to_string( ) . contains( "host header override" ) ,
567+ "should report host header override error for {host_header_override:?}"
568+ ) ;
569+ }
570+ }
571+
440572 #[ test]
441573 fn different_timeouts_produce_different_names ( ) {
442574 use std:: time:: Duration ;
0 commit comments