Skip to content

Commit

Permalink
[WIP] Add support for base64-encoded hash values to 'get_file_hash' (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
SkypLabs authored Feb 20, 2021
1 parent 6d6df45 commit d3ab393
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 16 deletions.
7 changes: 5 additions & 2 deletions components/site/tests/site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
86 changes: 78 additions & 8 deletions components/templates/src/global_fns/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -95,18 +96,36 @@ fn compute_file_sha256(mut file: fs::File) -> result::Result<String, io::Error>
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> {
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<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> {
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<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> {
Err(format!(
"file `{}` not found; searched in{}",
Expand Down Expand Up @@ -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<String, Value>) -> Result<Value> {
Expand All @@ -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 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 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, 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);
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand Down
18 changes: 13 additions & 5 deletions docs/content/documentation/templates/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
<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`

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`.

Expand Down
2 changes: 1 addition & 1 deletion test_site/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ <h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3>

{% block script %}
<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 %}

0 comments on commit d3ab393

Please sign in to comment.