[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
This commit is contained in:
Skyper 2021-02-20 12:31:37 +00:00 committed by GitHub
parent 6d6df45f23
commit d3ab3936de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 15 deletions

View file

@ -761,8 +761,11 @@ fn can_get_hash_for_static_files() {
"index.html", "index.html",
"src=\"https://replace-this-with-your-url.com/scripts/hello.js\"" "src=\"https://replace-this-with-your-url.com/scripts/hello.js\""
)); ));
assert!(file_contains!(public, "index.html", assert!(file_contains!(
"integrity=\"sha384-01422f31eaa721a6c4ac8c6fa09a27dd9259e0dfcf3c7593d7810d912a9de5ca2f582df978537bcd10f76896db61fbb9\"")); public,
"index.html",
"integrity=\"sha384-AUIvMeqnIabErIxvoJon3ZJZ4N/PPHWT14ENkSqd5covWC35eFN7zRD3aJbbYfu5\""
));
} }
#[test] #[test]

View file

@ -4,6 +4,7 @@ use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::{fs, io, result}; use std::{fs, io, result};
use base64::encode as encode_b64;
use sha2::{Digest, Sha256, Sha384, Sha512}; use sha2::{Digest, Sha256, Sha384, Sha512};
use svg_metadata as svg; use svg_metadata as svg;
use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; 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<String, io::Error>
Ok(format!("{:x}", hasher.finalize())) Ok(format!("{:x}", hasher.finalize()))
} }
fn compute_file_sha256_base64(mut file: fs::File) -> result::Result<String, io::Error> {
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<String, io::Error> { fn compute_file_sha384(mut file: fs::File) -> result::Result<String, io::Error> {
let mut hasher = Sha384::new(); let mut hasher = Sha384::new();
io::copy(&mut file, &mut hasher)?; io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize())) Ok(format!("{:x}", hasher.finalize()))
} }
fn compute_file_sha384_base64(mut file: fs::File) -> result::Result<String, io::Error> {
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<String, io::Error> { fn compute_file_sha512(mut file: fs::File) -> result::Result<String, io::Error> {
let mut hasher = Sha512::new(); let mut hasher = Sha512::new();
io::copy(&mut file, &mut hasher)?; io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize())) Ok(format!("{:x}", hasher.finalize()))
} }
fn compute_file_sha512_base64(mut file: fs::File) -> result::Result<String, io::Error> {
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<Value> { fn file_not_found_err(search_paths: &[PathBuf], url: &str) -> Result<Value> {
Err(format!( Err(format!(
"file `{}` not found; searched in{}", "file `{}` not found; searched in{}",
@ -178,6 +197,7 @@ impl GetFileHash {
} }
const DEFAULT_SHA_TYPE: u16 = 384; const DEFAULT_SHA_TYPE: u16 = 384;
const DEFAULT_BASE64: bool = false;
impl TeraFn for GetFileHash { impl TeraFn for GetFileHash {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> { fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
@ -192,12 +212,21 @@ impl TeraFn for GetFileHash {
"`get_file_hash`: `sha_type` must be 256, 384 or 512" "`get_file_hash`: `sha_type` must be 256, 384 or 512"
) )
.unwrap_or(DEFAULT_SHA_TYPE); .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 { let compute_hash_fn = match (sha_type, base64) {
256 => compute_file_sha256, (256, true) => compute_file_sha256_base64,
384 => compute_file_sha384, (256, false) => compute_file_sha256,
512 => compute_file_sha512, (384, true) => compute_file_sha384_base64,
_ => return Err("`get_file_hash`: `sha_type` must be 256, 384 or 512".into()), (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); 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] #[test]
fn can_get_file_hash_sha384() { fn can_get_file_hash_sha384() {
let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]); let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap()); 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] #[test]
@ -833,7 +887,23 @@ title = "A title"
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap()); args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(512).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] #[test]

View file

@ -144,26 +144,34 @@ An example is:
In the case of non-internal links, you can also add a cachebust of the format `?h=<sha256>` at the end of a URL In the case of non-internal links, you can also add a cachebust of the format `?h=<sha256>` at the end of a URL
by passing `cachebust=true` to the `get_url` function. by passing `cachebust=true` to the `get_url` function.
### `get_file_hash` ### `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 ```jinja2
{{/* get_file_hash(path="js/app.js", sha_type=256) */}} {{/* 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 ```jinja2
<script src="{{/* get_url(path="js/app.js") */}}" <script src="{{/* get_url(path="js/app.js") */}}"
integrity="sha384-{{/* get_file_hash(path="js/app.js", sha_type=384) */}}"></script> integrity="sha384-{{ get_file_hash(path="js/app.js", sha_type=384, base64=true) | safe }}"></script>
``` ```
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` ### `get_image_metadata`
Gets metadata for an image. This supports common formats like JPEG, PNG, as well as SVG. 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`. Currently, the only supported keys are `width` and `height`.

View file

@ -14,5 +14,5 @@
{% block script %} {% block script %}
<script src="{{ get_url(path="scripts/hello.js") | safe }}" <script src="{{ get_url(path="scripts/hello.js") | safe }}"
integrity="sha384-{{ get_file_hash(path="scripts/hello.js") }}"></script> integrity="sha384-{{ get_file_hash(path="scripts/hello.js", base64=true) | safe }}"></script>
{% endblock script %} {% endblock script %}