diff --git a/examples/http-certification/assets/README.md b/examples/http-certification/assets/README.md index 1f25795..2d2c3c5 100644 --- a/examples/http-certification/assets/README.md +++ b/examples/http-certification/assets/README.md @@ -229,6 +229,13 @@ fn certify_all_assets() { from: "/old-url".to_string(), to: "/".to_string(), kind: AssetRedirectKind::Permanent, + headers: get_asset_headers(vec![ + ("content-type".to_string(), "text/plain".to_string()), + ( + "cache-control".to_string(), + NO_CACHE_ASSET_CACHE_CONTROL.to_string(), + ), + ]), }, ]; diff --git a/examples/http-certification/assets/src/backend/src/lib.rs b/examples/http-certification/assets/src/backend/src/lib.rs index 5beb421..cc982e4 100644 --- a/examples/http-certification/assets/src/backend/src/lib.rs +++ b/examples/http-certification/assets/src/backend/src/lib.rs @@ -132,6 +132,13 @@ fn certify_all_assets() { from: "/old-url".to_string(), to: "/".to_string(), kind: AssetRedirectKind::Permanent, + headers: get_asset_headers(vec![ + ("content-type".to_string(), "text/plain".to_string()), + ( + "cache-control".to_string(), + NO_CACHE_ASSET_CACHE_CONTROL.to_string(), + ), + ]), }, ]; diff --git a/examples/http-certification/assets/src/tests/src/http.spec.ts b/examples/http-certification/assets/src/tests/src/http.spec.ts index a0712cb..8db5086 100644 --- a/examples/http-certification/assets/src/tests/src/http.spec.ts +++ b/examples/http-certification/assets/src/tests/src/http.spec.ts @@ -103,8 +103,12 @@ describe('Assets', () => { expect(response.status_code).toBe(301); expectHeader(response.headers, ['location', '/']); - // enable when additional headers can be added to redirect responses - // expectSecurityHeaders(response.headers); + expectHeader(response.headers, ['content-type', 'text/plain']); + expectHeader(response.headers, [ + 'cache-control', + 'public, no-cache, no-store', + ]); + expectSecurityHeaders(response.headers); let verificationResult = verifyRequestResponsePair( request, @@ -116,14 +120,16 @@ describe('Assets', () => { CERTIFICATE_VERSION, ); + const verifiedResponse = verificationResult.response; expect(verificationResult.verificationVersion).toEqual(CERTIFICATE_VERSION); - expect(verificationResult.response?.statusCode).toBe(301); - expect(verificationResult.response?.headers).toContainEqual([ - 'location', - '/', + expect(verifiedResponse?.statusCode).toBe(301); + expectHeader(verifiedResponse?.headers, ['location', '/']); + expectHeader(verifiedResponse?.headers, ['content-type', 'text/plain']); + expectHeader(verifiedResponse?.headers, [ + 'cache-control', + 'public, no-cache, no-store', ]); - // enable when additional headers can be added to redirect responses - // expectSecurityHeaders(verificationResult.response?.headers); + expectSecurityHeaders(verifiedResponse?.headers); }); // paths must be updated if the asset content changes diff --git a/packages/ic-asset-certification/README.md b/packages/ic-asset-certification/README.md index a1ae954..de6ed51 100644 --- a/packages/ic-asset-certification/README.md +++ b/packages/ic-asset-certification/README.md @@ -328,6 +328,10 @@ let config = AssetConfig::Redirect { from: "/old".to_string(), to: "/new".to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], }; ``` @@ -429,6 +433,10 @@ let asset_configs = vec![ from: "/old".to_string(), to: "/new".to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], }, ]; @@ -631,6 +639,10 @@ let asset_configs = vec![ from: "/old".to_string(), to: "/new".to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], }, ]; @@ -742,6 +754,10 @@ asset_router from: "/old".to_string(), to: "/new".to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], }], ) .unwrap(); @@ -864,6 +880,7 @@ let asset_configs = vec![ from: "/old".to_string(), to: "/new".to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![("content-type".to_string(), "text/plain".to_string())], }, ]; diff --git a/packages/ic-asset-certification/src/asset_config.rs b/packages/ic-asset-certification/src/asset_config.rs index cc8b95f..373b904 100644 --- a/packages/ic-asset-certification/src/asset_config.rs +++ b/packages/ic-asset-certification/src/asset_config.rs @@ -161,6 +161,10 @@ use std::fmt::{Display, Formatter}; /// from: "/old".to_string(), /// to: "/new".to_string(), /// kind: AssetRedirectKind::Temporary, +/// headers: vec![( +/// "content-type".to_string(), +/// "text/plain; charset=utf-8".to_string(), +/// )], /// }; /// ``` /// @@ -176,6 +180,10 @@ use std::fmt::{Display, Formatter}; /// from: "/old".to_string(), /// to: "/new".to_string(), /// kind: AssetRedirectKind::Permanent, +/// headers: vec![( +/// "content-type".to_string(), +/// "text/plain; charset=utf-8".to_string(), +/// )], /// }; /// ``` #[derive(Debug, Clone)] @@ -365,6 +373,14 @@ pub enum AssetConfig { /// The kind redirect to configure. kind: AssetRedirectKind, + + /// Additional headers to be inserted into the response. Each additional + /// header added will be included in certification and served by the + /// [AssetRouter](crate::AssetRouter) for matching [Assets](Asset). + /// + /// Note that the `Location` header will be automatically added to the + /// response with the value of the `to` field. + headers: Vec<(String, String)>, }, } @@ -535,6 +551,7 @@ pub(crate) enum NormalizedAssetConfig { from: String, to: String, kind: AssetRedirectKind, + headers: Vec<(String, String)>, }, } @@ -569,9 +586,17 @@ impl TryFrom for NormalizedAssetConfig { headers, encodings, }), - AssetConfig::Redirect { from, to, kind } => { - Ok(NormalizedAssetConfig::Redirect { from, to, kind }) - } + AssetConfig::Redirect { + from, + to, + kind, + headers, + } => Ok(NormalizedAssetConfig::Redirect { + from, + to, + kind, + headers, + }), } } } @@ -726,6 +751,10 @@ mod tests { from: asset_path.to_string(), to: asset_path.to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], } .try_into() .unwrap(); diff --git a/packages/ic-asset-certification/src/asset_router.rs b/packages/ic-asset-certification/src/asset_router.rs index 6015474..1dd6a5d 100644 --- a/packages/ic-asset-certification/src/asset_router.rs +++ b/packages/ic-asset-certification/src/asset_router.rs @@ -78,11 +78,19 @@ use std::{borrow::Cow, cell::RefCell, cmp, collections::HashMap, rc::Rc}; /// from: "/old-url".to_string(), /// to: "/".to_string(), /// kind: AssetRedirectKind::Permanent, +/// headers: vec![( +/// "content-type".to_string(), +/// "text/plain; charset=utf-8".to_string(), +/// )], /// }, /// AssetConfig::Redirect { /// from: "/css/app.css".to_string(), /// to: "/css/app-ba74b708.css".to_string(), /// kind: AssetRedirectKind::Temporary, +/// headers: vec![( +/// "content-type".to_string(), +/// "text/plain; charset=utf-8".to_string(), +/// )], /// }, /// ]; /// @@ -311,8 +319,14 @@ impl<'content> AssetRouter<'content> { } for asset_config in asset_configs { - if let NormalizedAssetConfig::Redirect { from, to, kind } = asset_config { - self.insert_redirect(from, to, kind)?; + if let NormalizedAssetConfig::Redirect { + from, + to, + kind, + headers, + } = asset_config + { + self.insert_redirect(from, to, kind, headers)?; } } @@ -367,8 +381,14 @@ impl<'content> AssetRouter<'content> { } for asset_config in asset_configs { - if let NormalizedAssetConfig::Redirect { from, to, kind } = asset_config { - self.delete_redirect(from, to, kind)?; + if let NormalizedAssetConfig::Redirect { + from, + to, + kind, + headers, + } = asset_config + { + self.delete_redirect(from, to, kind, headers)?; } } @@ -799,8 +819,9 @@ impl<'content> AssetRouter<'content> { from: String, to: String, kind: AssetRedirectKind, + additional_headers: Vec<(String, String)>, ) -> AssetCertificationResult<()> { - let response = Self::prepare_redirect(from.clone(), to, kind)?; + let response = Self::prepare_redirect(from.clone(), to, kind, additional_headers)?; self.tree.borrow_mut().insert(&response.tree_entry); @@ -815,8 +836,9 @@ impl<'content> AssetRouter<'content> { from: String, to: String, kind: AssetRedirectKind, + addtional_headers: Vec<(String, String)>, ) -> AssetCertificationResult<()> { - let response = Self::prepare_redirect(from.clone(), to, kind)?; + let response = Self::prepare_redirect(from.clone(), to, kind, addtional_headers)?; self.tree.borrow_mut().delete(&response.tree_entry); self.responses.remove(&RequestKey::new(&from, None, None)); @@ -828,13 +850,15 @@ impl<'content> AssetRouter<'content> { from: String, to: String, kind: AssetRedirectKind, + addtional_headers: Vec<(String, String)>, ) -> AssetCertificationResult> { let status_code = match kind { AssetRedirectKind::Permanent => StatusCode::MOVED_PERMANENTLY, AssetRedirectKind::Temporary => StatusCode::TEMPORARY_REDIRECT, }; - let headers = vec![("location".to_string(), to)]; + let mut headers = vec![("location".to_string(), to)]; + headers.extend(addtional_headers); let (response, certification) = Self::prepare_response_and_certification( from.clone(), @@ -3309,6 +3333,10 @@ mod tests { CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr.clone(), ), + ( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + ), ]) .build(); let mut expected_old_url_response = HttpResponse::builder() @@ -3317,6 +3345,10 @@ mod tests { ("content-length".to_string(), "0".to_string()), ("location".to_string(), "/".to_string()), (CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr), + ( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + ), ]) .build(); @@ -3891,6 +3923,10 @@ mod tests { from: "/old-url".to_string(), to: "/".to_string(), kind: AssetRedirectKind::Permanent, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], } } @@ -3900,6 +3936,10 @@ mod tests { from: "/css/app.css".to_string(), to: "/css/app-ba74b708.css".to_string(), kind: AssetRedirectKind::Temporary, + headers: vec![( + "content-type".to_string(), + "text/plain; charset=utf-8".to_string(), + )], } } diff --git a/packages/ic-asset-certification/src/lib.rs b/packages/ic-asset-certification/src/lib.rs index b42e26a..0e14d9d 100644 --- a/packages/ic-asset-certification/src/lib.rs +++ b/packages/ic-asset-certification/src/lib.rs @@ -328,6 +328,10 @@ //! from: "/old".to_string(), //! to: "/new".to_string(), //! kind: AssetRedirectKind::Permanent, +//! headers: vec![( +//! "content-type".to_string(), +//! "text/plain; charset=utf-8".to_string(), +//! )], //! }; //! ``` //! @@ -430,6 +434,10 @@ //! from: "/old".to_string(), //! to: "/new".to_string(), //! kind: AssetRedirectKind::Permanent, +//! headers: vec![( +//! "content-type".to_string(), +//! "text/plain; charset=utf-8".to_string(), +//! )], //! }, //! ]; //! @@ -620,6 +628,10 @@ //! from: "/old".to_string(), //! to: "/new".to_string(), //! kind: AssetRedirectKind::Permanent, +//! headers: vec![( +//! "content-type".to_string(), +//! "text/plain; charset=utf-8".to_string(), +//! )], //! }, //! ]; //! @@ -750,6 +762,10 @@ //! from: "/old".to_string(), //! to: "/new".to_string(), //! kind: AssetRedirectKind::Permanent, +//! headers: vec![( +//! "content-type".to_string(), +//! "text/plain; charset=utf-8".to_string(), +//! )], //! }], //! ) //! .unwrap(); @@ -872,6 +888,7 @@ //! from: "/old".to_string(), //! to: "/new".to_string(), //! kind: AssetRedirectKind::Permanent, +//! headers: vec![("content-type".to_string(), "text/plain".to_string())], //! }, //! ]; //!