11use crate :: heroku_web_server_config:: {
2- ErrorsConfig , Header , HerokuWebServerConfig , DEFAULT_DOC_INDEX , DEFAULT_DOC_ROOT ,
2+ ErrorsConfig , HerokuWebServerConfig , PathMatchedHeader , DEFAULT_DOC_INDEX , DEFAULT_DOC_ROOT ,
33} ;
44use crate :: o11y:: * ;
55use indexmap:: IndexMap ;
6+ use serde:: Serialize ;
67use serde_json:: json;
78use std:: collections:: HashMap ;
89
910/// Transforms the given [`HerokuWebServerConfig`] into an equivalent Caddy JSON configuration.
1011/// Keeping this as a single function, because many lines are just the JSON itself being assembled.
1112#[ allow( clippy:: too_many_lines) ]
12- pub ( crate ) fn caddy_json_config ( config : HerokuWebServerConfig ) -> serde_json:: Value {
13+ pub ( crate ) fn caddy_json_config ( config : & HerokuWebServerConfig ) -> serde_json:: Value {
1314 let mut routes = vec ! [ ] ;
1415
1516 // Header routes come first so headers will be added to any response down the chain.
16- if let Some ( headers) = config. headers {
17+ if let Some ( ref headers) = config. headers {
1718 tracing:: info!( { CONFIG_RESPONSE_HEADERS_ENABLED } = true , "config" ) ;
18- routes. extend ( generate_response_headers_routes ( & headers) ) ;
19+ routes. extend ( generate_response_headers_routes ( headers) ) ;
1920 }
2021
2122 let doc_root = config
2223 . root
24+ . clone ( )
2325 . map_or ( String :: from ( DEFAULT_DOC_ROOT ) , |path_buf| {
2426 String :: from ( path_buf. to_string_lossy ( ) )
2527 } ) ;
@@ -63,6 +65,8 @@ pub(crate) fn caddy_json_config(config: HerokuWebServerConfig) -> serde_json::Va
6365 } ) ) ;
6466 }
6567
68+ generate_static_response_handlers ( config, & mut static_file_handlers) ;
69+
6670 static_file_handlers. push ( json ! (
6771 {
6872 "handler" : "encode" ,
@@ -191,10 +195,67 @@ pub(crate) fn caddy_json_config(config: HerokuWebServerConfig) -> serde_json::Va
191195 } )
192196}
193197
194- fn generate_response_headers_routes ( headers : & Vec < Header > ) -> Vec < serde_json:: Value > {
198+ fn generate_static_response_handlers (
199+ config : & HerokuWebServerConfig ,
200+ static_file_handlers : & mut Vec < serde_json:: Value > ,
201+ ) {
202+ if let Some ( static_responses) = config
203+ . caddy_server_opts
204+ . as_ref ( )
205+ . and_then ( |v| v. static_responses . clone ( ) )
206+ {
207+ tracing:: info!(
208+ { CONFIG_CADDY_SERVER_OPTS_STATIC_RESPONSES } = true ,
209+ "config"
210+ ) ;
211+ for static_response in static_responses {
212+ let mut match_array = vec ! [ ] ;
213+
214+ if let Some ( host_matcher) = static_response. host_matcher {
215+ let mut host_match = serde_json:: Map :: new ( ) ;
216+ host_match. insert ( "host" . to_string ( ) , json ! ( vec![ host_matcher] ) ) ;
217+ match_array. push ( serde_json:: Value :: Object ( host_match) ) ;
218+ }
219+ if let Some ( path_matcher) = static_response. path_matcher {
220+ let mut path_match = serde_json:: Map :: new ( ) ;
221+ path_match. insert ( "path" . to_string ( ) , json ! ( vec![ path_matcher] ) ) ;
222+ match_array. push ( serde_json:: Value :: Object ( path_match) ) ;
223+ }
224+
225+ let headers = static_response. headers . map ( |headers_vec| {
226+ headers_vec
227+ . into_iter ( )
228+ . map ( |header| ( header. key , vec ! [ header. value] ) )
229+ . collect :: < HashMap < _ , _ > > ( )
230+ } ) ;
231+
232+ let static_response_handler = StaticResponseHandler {
233+ handler : "static_response" . to_string ( ) ,
234+ status_code : static_response. status . unwrap_or ( 200 ) ,
235+ headers,
236+ body : static_response. body ,
237+ } ;
238+ let static_response_handler_json = serde_json:: to_value ( static_response_handler)
239+ . expect ( "StaticResponseHandler should serialize to JSON" ) ;
240+
241+ static_file_handlers. push ( json ! (
242+ {
243+ "handler" : "subroute" ,
244+ "routes" : [ {
245+ "match" : match_array,
246+ "handle" : [ static_response_handler_json] ,
247+ "terminal" : true
248+ } ] ,
249+
250+ } ) ) ;
251+ }
252+ }
253+ }
254+
255+ fn generate_response_headers_routes ( headers : & Vec < PathMatchedHeader > ) -> Vec < serde_json:: Value > {
195256 // Group headers with the same matcher while preserving the order of the matchers
196257 // by "when-first-seen".
197- let mut groups = IndexMap :: < String , Vec < & Header > > :: new ( ) ;
258+ let mut groups = IndexMap :: < String , Vec < & PathMatchedHeader > > :: new ( ) ;
198259 for header in headers {
199260 if let Some ( headers) = groups. get_mut ( & header. path_matcher ) {
200261 headers. push ( header) ;
@@ -284,27 +345,37 @@ const DEFAULT_404_HTML: &str = r#"
284345 </html>
285346"# ;
286347
348+ #[ derive( Serialize ) ]
349+ struct StaticResponseHandler {
350+ handler : String ,
351+ status_code : u16 ,
352+ headers : Option < HashMap < String , Vec < String > > > ,
353+ body : Option < String > ,
354+ }
355+
287356#[ cfg( test) ]
288357mod tests {
289358 use super :: * ;
290- use crate :: heroku_web_server_config:: { ErrorConfig , ErrorsConfig } ;
359+ use crate :: heroku_web_server_config:: {
360+ CaddyServerOpts , CaddyStaticResponseConfig , ErrorConfig , ErrorsConfig , Header ,
361+ } ;
291362 use std:: path:: PathBuf ;
292363
293364 #[ test]
294365 fn generates_matched_response_headers_routes ( ) {
295366 let heroku_config = HerokuWebServerConfig {
296367 headers : Some ( vec ! [
297- Header {
368+ PathMatchedHeader {
298369 path_matcher: String :: from( "*" ) ,
299370 key: String :: from( "X-Foo" ) ,
300371 value: String :: from( "Bar" ) ,
301372 } ,
302- Header {
373+ PathMatchedHeader {
303374 path_matcher: String :: from( "*.html" ) ,
304375 key: String :: from( "X-Baz" ) ,
305376 value: String :: from( "Buz" ) ,
306377 } ,
307- Header {
378+ PathMatchedHeader {
308379 path_matcher: String :: from( "*" ) ,
309380 key: String :: from( "X-Zuu" ) ,
310381 value: String :: from( "Zem" ) ,
@@ -327,7 +398,7 @@ mod tests {
327398 #[ test]
328399 fn generates_global_response_headers_routes ( ) {
329400 let heroku_config = HerokuWebServerConfig {
330- headers : Some ( vec ! [ Header {
401+ headers : Some ( vec ! [ PathMatchedHeader {
331402 path_matcher: String :: from( "*" ) ,
332403 key: String :: from( "X-Foo" ) ,
333404 value: String :: from( "Bar" ) ,
@@ -390,4 +461,113 @@ mod tests {
390461 json!( { "handle" : [ { "handler" : "rewrite" , "uri" : "index.html" } , { "handler" : "file_server" , "index_names" : [ "index.html" ] , "pass_thru" : false , "root" : "tests/fixtures/client_side_routing/public" , "status_code" : "200" } ] } )
391462 ) ;
392463 }
464+
465+ #[ test]
466+ fn generates_static_response_handlers ( ) {
467+ let heroku_config = HerokuWebServerConfig {
468+ caddy_server_opts : Some ( CaddyServerOpts {
469+ static_responses : Some ( vec ! [
470+ CaddyStaticResponseConfig {
471+ host_matcher: Some ( "original.example.com" . to_string( ) ) ,
472+ path_matcher: None ,
473+ status: Some ( 301 ) ,
474+ headers: Some ( vec![
475+ Header {
476+ key: "Location" . to_string( ) ,
477+ value: "https://new.example.com{http.request.uri}" . to_string( ) ,
478+ } ,
479+ Header {
480+ key: "X-Redirected-From" . to_string( ) ,
481+ value: "original.example.com" . to_string( ) ,
482+ } ,
483+ ] ) ,
484+ body: None ,
485+ } ,
486+ CaddyStaticResponseConfig {
487+ host_matcher: Some ( "original.example.com" . to_string( ) ) ,
488+ path_matcher: Some ( "/blog/*" . to_string( ) ) ,
489+ status: Some ( 301 ) ,
490+ headers: Some ( vec![ Header {
491+ key: "Location" . to_string( ) ,
492+ value:
493+ "https://{http.request.host}/new-blog/{http.request.uri.path.file}"
494+ . to_string( ) ,
495+ } ] ) ,
496+ body: None ,
497+ } ,
498+ CaddyStaticResponseConfig {
499+ host_matcher: None ,
500+ path_matcher: Some ( "/api/*" . to_string( ) ) ,
501+ status: Some ( 500 ) ,
502+ headers: Some ( vec![ Header {
503+ key: "Content-Type" . to_string( ) ,
504+ value: "application/json" . to_string( ) ,
505+ } ] ) ,
506+ body: Some ( r#"{"error":"Service not available"}"# . to_string( ) ) ,
507+ } ,
508+ ] ) ,
509+ ..CaddyServerOpts :: default ( )
510+ } ) ,
511+ ..HerokuWebServerConfig :: default ( )
512+ } ;
513+
514+ let mut handlers = vec ! [ ] ;
515+ generate_static_response_handlers ( & heroku_config, & mut handlers) ;
516+
517+ assert_eq ! ( handlers. len( ) , 3 ) ;
518+
519+ assert_eq ! (
520+ handlers[ 0 ] ,
521+ json!( {
522+ "handler" : "subroute" ,
523+ "routes" : [ {
524+ "match" : [ { "host" : [ "original.example.com" ] } ] ,
525+ "handle" : [ {
526+ "handler" : "static_response" ,
527+ "status_code" : 301 ,
528+ "headers" : {
529+ "Location" : [ "https://new.example.com{http.request.uri}" ] ,
530+ "X-Redirected-From" : [ "original.example.com" ]
531+ } ,
532+ "body" : null
533+ } ] ,
534+ "terminal" : true
535+ } ]
536+ } )
537+ ) ;
538+
539+ assert_eq ! (
540+ handlers[ 1 ] ,
541+ json!( {
542+ "handler" : "subroute" ,
543+ "routes" : [ {
544+ "match" : [ { "host" : [ "original.example.com" ] } , { "path" : [ "/blog/*" ] } ] ,
545+ "handle" : [ {
546+ "handler" : "static_response" ,
547+ "status_code" : 301 ,
548+ "headers" : { "Location" : [ "https://{http.request.host}/new-blog/{http.request.uri.path.file}" ] } ,
549+ "body" : null
550+ } ] ,
551+ "terminal" : true
552+ } ]
553+ } )
554+ ) ;
555+
556+ assert_eq ! (
557+ handlers[ 2 ] ,
558+ json!( {
559+ "handler" : "subroute" ,
560+ "routes" : [ {
561+ "match" : [ { "path" : [ "/api/*" ] } ] ,
562+ "handle" : [ {
563+ "handler" : "static_response" ,
564+ "status_code" : 500 ,
565+ "headers" : { "Content-Type" : [ "application/json" ] } ,
566+ "body" : r#"{"error":"Service not available"}"#
567+ } ] ,
568+ "terminal" : true
569+ } ]
570+ } )
571+ ) ;
572+ }
393573}
0 commit comments