Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for base64-encoded hash values to 'get_file_hash' #1339

Merged
merged 4 commits into from
Feb 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be cleaner to pass the base64 arg to those functions than to duplicate them all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it but there were already 3 different functions for the 3 possible values of sha_type so I opted for keeping it consistent.

Wouldn't it be even better to pass sha_type and base64 to one unique function handling all the possible combinations?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be even better to pass sha_type and base64 to one unique function handling all the possible combinations?

Even better!

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 @@ -143,26 +143,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 %}