From d3ab3936de8657ee405693b7ed79790af90a313f Mon Sep 17 00:00:00 2001 From: Skyper Date: Sat, 20 Feb 2021 12:31:37 +0000 Subject: [PATCH] [WIP] Add support for base64-encoded hash values to 'get_file_hash' (#1339) * Add support for base64-encoded hash values The global template function 'get_file_hash' can now return a base64-encoded hash value when its 'base64' parameter is set to true. See discussion in #519. * Fix integrity attribute's value in test site SRI hash values must be base64-encoded. * Update documentation about 'get_file_hash' * Fix 'can_get_hash_for_static_files' unit test --- components/site/tests/site.rs | 7 +- components/templates/src/global_fns/mod.rs | 84 +++++++++++++++++-- .../documentation/templates/overview.md | 18 ++-- test_site/templates/index.html | 2 +- 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index 1958b25d..178cda04 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -761,8 +761,11 @@ fn can_get_hash_for_static_files() { "index.html", "src=\"https://replace-this-with-your-url.com/scripts/hello.js\"" )); - assert!(file_contains!(public, "index.html", - "integrity=\"sha384-01422f31eaa721a6c4ac8c6fa09a27dd9259e0dfcf3c7593d7810d912a9de5ca2f582df978537bcd10f76896db61fbb9\"")); + assert!(file_contains!( + public, + "index.html", + "integrity=\"sha384-AUIvMeqnIabErIxvoJon3ZJZ4N/PPHWT14ENkSqd5covWC35eFN7zRD3aJbbYfu5\"" + )); } #[test] diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 7f4d48d4..8b8fc910 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::{fs, io, result}; +use base64::encode as encode_b64; use sha2::{Digest, Sha256, Sha384, Sha512}; use svg_metadata as svg; use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; @@ -95,18 +96,36 @@ fn compute_file_sha256(mut file: fs::File) -> result::Result Ok(format!("{:x}", hasher.finalize())) } +fn compute_file_sha256_base64(mut file: fs::File) -> result::Result { + let mut hasher = Sha256::new(); + io::copy(&mut file, &mut hasher)?; + Ok(format!("{}", encode_b64(hasher.finalize()))) +} + fn compute_file_sha384(mut file: fs::File) -> result::Result { let mut hasher = Sha384::new(); io::copy(&mut file, &mut hasher)?; Ok(format!("{:x}", hasher.finalize())) } +fn compute_file_sha384_base64(mut file: fs::File) -> result::Result { + let mut hasher = Sha384::new(); + io::copy(&mut file, &mut hasher)?; + Ok(format!("{}", encode_b64(hasher.finalize()))) +} + fn compute_file_sha512(mut file: fs::File) -> result::Result { let mut hasher = Sha512::new(); io::copy(&mut file, &mut hasher)?; Ok(format!("{:x}", hasher.finalize())) } +fn compute_file_sha512_base64(mut file: fs::File) -> result::Result { + let mut hasher = Sha512::new(); + io::copy(&mut file, &mut hasher)?; + Ok(format!("{}", encode_b64(hasher.finalize()))) +} + fn file_not_found_err(search_paths: &[PathBuf], url: &str) -> Result { Err(format!( "file `{}` not found; searched in{}", @@ -178,6 +197,7 @@ impl GetFileHash { } const DEFAULT_SHA_TYPE: u16 = 384; +const DEFAULT_BASE64: bool = false; impl TeraFn for GetFileHash { fn call(&self, args: &HashMap) -> Result { @@ -192,12 +212,21 @@ impl TeraFn for GetFileHash { "`get_file_hash`: `sha_type` must be 256, 384 or 512" ) .unwrap_or(DEFAULT_SHA_TYPE); + let base64 = optional_arg!( + bool, + args.get("base64"), + "`get_file_hash`: `base64` must be true or false" + ) + .unwrap_or(DEFAULT_BASE64); - let compute_hash_fn = match sha_type { - 256 => compute_file_sha256, - 384 => compute_file_sha384, - 512 => compute_file_sha512, - _ => return Err("`get_file_hash`: `sha_type` must be 256, 384 or 512".into()), + let compute_hash_fn = match (sha_type, base64) { + (256, true) => compute_file_sha256_base64, + (256, false) => compute_file_sha256, + (384, true) => compute_file_sha384_base64, + (384, false) => compute_file_sha384, + (512, true) => compute_file_sha512_base64, + (512, false) => compute_file_sha512, + _ => return Err("`get_file_hash`: bad arguments".into()), }; let hash = open_file(&self.search_paths, &path).and_then(compute_hash_fn); @@ -819,12 +848,37 @@ title = "A title" ); } + #[test] + fn can_get_file_hash_sha256_base64() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(256).unwrap()); + args.insert("base64".to_string(), to_value(true).unwrap()); + assert_eq!(static_fn.call(&args).unwrap(), "Vy5pHcaMP81lOuRjJhvbOPNdxvAXFdnOaHmTGd0ViEA="); + } + #[test] fn can_get_file_hash_sha384() { let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); - assert_eq!(static_fn.call(&args).unwrap(), "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414"); + assert_eq!( + static_fn.call(&args).unwrap(), + "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414" + ); + } + + #[test] + fn can_get_file_hash_sha384_base64() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("base64".to_string(), to_value(true).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "FBwJvSiJl3O3crvgZNi3GPodbyhSt+r9XtZonSa3SIO3ni6BTNadW1KrR2qihMQU" + ); } #[test] @@ -833,7 +887,23 @@ title = "A title" let mut args = HashMap::new(); args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("sha_type".to_string(), to_value(512).unwrap()); - assert_eq!(static_fn.call(&args).unwrap(), "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f"); + assert_eq!( + static_fn.call(&args).unwrap(), + "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f" + ); + } + + #[test] + fn can_get_file_hash_sha512_base64() { + let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("app.css").unwrap()); + args.insert("sha_type".to_string(), to_value(512).unwrap()); + args.insert("base64".to_string(), to_value(true).unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "N536s1EjuRWdnk6S3JDivkTPPC9/CbLi34Chshm0Yd41Vsk+GpzrMAjpmeLWpUtPHWXum+m+Y/pF7IiTFiM3Lw==" + ); } #[test] diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md index 118eeb4d..f324eb36 100644 --- a/docs/content/documentation/templates/overview.md +++ b/docs/content/documentation/templates/overview.md @@ -144,26 +144,34 @@ An example is: In the case of non-internal links, you can also add a cachebust of the format `?h=` at the end of a URL by passing `cachebust=true` to the `get_url` function. - ### `get_file_hash` -Gets the hash digest for a static file. Supported hashes are SHA-256, SHA-384 (default) and SHA-512. Requires `path`. The `sha_type` key is optional and must be one of 256, 384 or 512. +Returns the hash digest of a static file. Supported hashing algorithms are +SHA-256, SHA-384 (default) and SHA-512. Requires `path`. The `sha_type` +parameter is optional and must be one of 256, 384 or 512. ```jinja2 {{/* get_file_hash(path="js/app.js", sha_type=256) */}} ``` -This can be used to implement subresource integrity. Do note that subresource integrity is typically used when using external scripts, which `get_file_hash` does not support. +The function can also output a base64-encoded hash value when its `base64` +parameter is set to `true`. This can be used to implement [subresource +integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). ```jinja2 + integrity="sha384-{{ get_file_hash(path="js/app.js", sha_type=384, base64=true) | safe }}"> ``` -Whenever hashing files, whether using `get_file_hash` or `get_url(..., cachebust=true)`, the file is searched for in three places: `static/`, `content/` and the output path (so e.g. compiled SASS can be hashed, too.) +Do note that subresource integrity is typically used when using external +scripts, which `get_file_hash` does not support. +Whenever hashing files, whether using `get_file_hash` or `get_url(..., +cachebust=true)`, the file is searched for in three places: `static/`, +`content/` and the output path (so e.g. compiled SASS can be hashed, too.) ### `get_image_metadata` + Gets metadata for an image. This supports common formats like JPEG, PNG, as well as SVG. Currently, the only supported keys are `width` and `height`. diff --git a/test_site/templates/index.html b/test_site/templates/index.html index 9ed72df3..35a24be4 100644 --- a/test_site/templates/index.html +++ b/test_site/templates/index.html @@ -14,5 +14,5 @@ {% block script %} + integrity="sha384-{{ get_file_hash(path="scripts/hello.js", base64=true) | safe }}"> {% endblock script %}