Skip to content

Commit

Permalink
Merge pull request #521 from VorpalBlade/sri_calculation
Browse files Browse the repository at this point in the history
Sub-resource integrity
  • Loading branch information
miracle2k committed Nov 18, 2019
2 parents 3b2fc3a + 967844f commit 8d4a5d6
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 14 deletions.
36 changes: 31 additions & 5 deletions src/webassets/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .exceptions import BundleError, BuildError
from .utils import cmp_debug_levels, hash_func
from .env import ConfigurationContext, DictConfigStorage, BaseEnvironment
from .utils import is_url
from .utils import is_url, calculate_sri_on_file


__all__ = ('Bundle', 'get_all_bundle_files',)
Expand Down Expand Up @@ -737,6 +737,8 @@ def _urls(self, ctx, extra_filters, *args, **kwargs):
"""Return a list of urls for this bundle, and all subbundles,
and, when it becomes necessary, start a build process.
"""
# Check if we should calculate SRI
calculate_sri = kwargs.pop('calculate_sri', False)

# Look at the debug value to see if this bundle should return the
# source urls (in debug mode), or a single url of the bundle in built
Expand Down Expand Up @@ -764,7 +766,11 @@ def _urls(self, ctx, extra_filters, *args, **kwargs):
if ctx.auto_build:
self._build(ctx, extra_filters=extra_filters, force=False,
*args, **kwargs)
return [self._make_output_url(ctx)]
if calculate_sri:
return [{'uri': self._make_output_url(ctx),
'sri': calculate_sri_on_file(ctx.resolver.resolve_output_to_path(ctx, self.output, self))}]
else:
return [self._make_output_url(ctx)]
else:
# We either have no files (nothing to build), or we are
# in debug mode: Instead of building the bundle, we
Expand All @@ -775,20 +781,34 @@ def _urls(self, ctx, extra_filters, *args, **kwargs):
urls.extend(org._urls(
wrap(ctx, cnt),
merge_filters(extra_filters, self.filters),
*args, **kwargs))
*args,
calculate_sri=calculate_sri,
**kwargs))
elif is_url(cnt):
urls.append(cnt)
# Can't calculate SRI for non file
if calculate_sri:
urls.append({'uri': cnt, 'sri': None})
else:
urls.append(cnt)
else:
sri = None
try:
url = ctx.resolver.resolve_source_to_url(ctx, cnt, org)
if calculate_sri:
sri = calculate_sri_on_file(ctx.resolver.resolve_output_to_path(ctx, cnt, org))
except ValueError:
# If we cannot generate a url to a path outside the
# media directory. So if that happens, we copy the
# file into the media directory.
external = pull_external(ctx, cnt)
url = ctx.resolver.resolve_source_to_url(ctx, external, org)
if calculate_sri:
sri = calculate_sri_on_file(ctx.resolver.resolve_output_to_path(ctx, external, org))

urls.append(url)
if calculate_sri:
urls.append({'uri': url, 'sri': sri})
else:
urls.append(url)
return urls

def urls(self, *args, **kwargs):
Expand All @@ -800,6 +820,12 @@ def urls(self, *args, **kwargs):
Insofar necessary, this will automatically create or update the files
behind these urls.
:param calculate_sri: Set to true to calculate a sub-resource integrity
string for the URLs. This changes the returned format.
:return: List of URIs if calculate_sri is False. If calculate_sri is
true: list of {'uri': '<uri>', 'sri': '<sri-hash>'}.
"""
ctx = wrap(self.env, self)
urls = []
Expand Down
12 changes: 8 additions & 4 deletions src/webassets/ext/jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def parse(self, parser):
#
# Summary: We have to be satisfied with a single EXTRA variable.
args = [nodes.Name('ASSET_URL', 'param'),
nodes.Name('ASSET_SRI', 'param'),
nodes.Name('EXTRA', 'param')]

# Return a ``CallBlock``, which means Jinja2 will call a Python method
Expand Down Expand Up @@ -176,20 +177,23 @@ def _render_assets(self, filter, output, dbg, depends, files, caller=None):
'output': output,
'filters': filter,
'debug': dbg,
'depends': depends,
'depends': depends
}
bundle = self.BundleClass(
*self.resolve_contents(files, env), **bundle_kwargs)

# Retrieve urls (this may or may not cause a build)
with bundle.bind(env):
urls = bundle.urls()
urls = bundle.urls(calculate_sri=True)

# For each url, execute the content of this template tag (represented
# by the macro ```caller`` given to use by Jinja2).
result = u""
for url in urls:
result += caller(url, bundle.extra)
for entry in urls:
if isinstance(entry, dict):
result += caller(entry['uri'], entry.get('sri', None), bundle.extra)
else:
result += caller(entry, None, bundle.extra)
return result


Expand Down
37 changes: 37 additions & 0 deletions src/webassets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
'common_path_prefix', 'working_directory', 'is_url')


import base64

if sys.version_info >= (2, 5):
import hashlib
md5_constructor = hashlib.md5
Expand All @@ -34,6 +36,14 @@
set = set


try:
FileNotFoundError
except NameError:
FileNotFoundError = IOError
else:
FileNotFoundError = FileNotFoundError


from webassets.six import StringIO


Expand Down Expand Up @@ -210,3 +220,30 @@ def is_url(s):
return False
parsed = urlparse.urlsplit(s)
return bool(parsed.scheme and parsed.netloc) and len(parsed.scheme) > 1


def calculate_sri(data):
"""Calculate SRI string for data buffer."""
hash = hashlib.sha384()
hash.update(data)
hash = hash.digest()
hash_base64 = base64.b64encode(hash).decode()
return 'sha384-{}'.format(hash_base64)


def calculate_sri_on_file(file_name):
"""Calculate SRI string if file can be found. Otherwise silently return None"""
BUF_SIZE = 65536
hash = hashlib.sha384()
try:
with open(file_name, 'rb') as f:
while True:
data = f.read(BUF_SIZE)
if not data:
break
hash.update(data)
hash = hash.digest()
hash_base64 = base64.b64encode(hash).decode()
return 'sha384-{}'.format(hash_base64)
except FileNotFoundError:
return None
Loading

0 comments on commit 8d4a5d6

Please sign in to comment.