This repository has been archived by the owner on Feb 12, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# Django HTTP2 Middleware | ||
|
||
This is middlware for django to assist with generating preload headers for HTTP2-push, with support for using StreamingHttpResponse to send cached preload headers in advance of the actual response being generated. This allows nginx to serve push preload resources before | ||
django is even finished running the view and returning a response! | ||
|
||
It's also fully compatible with [`django-csp`](https://django-csp.readthedocs.io/en/latest/configuration.html), it sends `request.csp_nonce` | ||
in preload headers correctly so that preloads aren't rejected by your | ||
csp policy if they require a nonce. | ||
|
||
## How it works | ||
|
||
It works by providing a templatetag `{% http2static %}` that works just | ||
like `{% static %}`, except it records all the urls on `request.to_preload` automatically as it renders the template. | ||
|
||
Those urls are then transformed into an HTTP preload header which is attached to the response. When `settings.HTTP2_PRESEND_CACHED_HEADERS = True`, the first response's preload headers will be cached and automatically sent in advance during later requests (using [`StreamingHttpResponse`](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.StreamingHttpResponse) to send them before the view executes) . Upstream servers like Nginx and CloudFlare will then use these headers to do HTTP2 server push, delivering the resources to clients before they are requested during browser parse & rendering. | ||
|
||
## Note about performance | ||
|
||
While modern and shiny, this wont necessarily make your site faster. In fact, it can often make sites slower because later requests have the resources cached anyway, so pusing uneeded resources on every request only wastes network bandwidth and hogs IO. Server push is best for sites where first-visit speed is a top priority. It's up to you to toggle the options and find what the best tradeoffs are for your own needs. | ||
|
||
## Usage | ||
```jija2 | ||
<!-- Create a preload html tag at the top, not strictly necessary --> | ||
<!-- but it's a good fallback in case HTTP2 is not supported --> | ||
<link rel="preload" as="style" href="{% http2static 'css/base.css' %}" crossorigin nonce="{{request.csp_nonce}}"> | ||
... | ||
<!-- Place the actual tag anywhere on the page, it will likely --> | ||
<!-- already be pushed and downloaded by time the browser parses it. --> | ||
<link rel="stylesheet" href="{% http2static 'css/base.css' %}" type="text/css" crossorigin nonce="{{request.csp_nonce}}"> | ||
``` | ||
|
||
## Install: | ||
|
||
cd /opt/your-project/project-django | ||
git clone https://github.com/pirate/django-http2-middleware http2 | ||
|
||
Then add the following to `settings.py` | ||
```python | ||
# (adding "http2" to INSTALLED_APPS is not needed) | ||
|
||
TEMPLATES = [ | ||
{ | ||
'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||
... | ||
'OPTIONS': { | ||
'context_processors': [ | ||
'django.template.context_processors.request', | ||
... | ||
], | ||
'builtins': [ | ||
... | ||
'http2.templatetags', | ||
], | ||
}, | ||
}, | ||
... | ||
] | ||
MIDDLEWARE = [ | ||
... | ||
'csp.middleware.CSPMiddleware', # (optional, must be above http2) | ||
'http2.middleware.HTTP2Middleware', | ||
] | ||
|
||
# attach any {% http2static %} urls in template as http preload header | ||
HTTP2_PRELOAD_HEADERS = True | ||
|
||
# cache first request's preload urls and send in advance on subsequent requests | ||
HTTP2_PRESEND_CACHED_HEADERS = True | ||
|
||
# allow upstream servers to server-push any files in preload headers | ||
HTTP2_SERVER_PUSH = False | ||
|
||
# optional recommended django-csp settings if you use CSP with nonce validation | ||
CSP_DEFAULT_SRC = ("'self'", ...) | ||
CSP_INCLUDE_NONCE_IN = ('default-src', ...) | ||
... | ||
``` | ||
|
||
## Example Nginx Configuration: | ||
```nginx | ||
http2_push_preload on; | ||
... | ||
server { | ||
listen 443 ssl http2; | ||
... | ||
} | ||
``` |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
from django.conf import settings | ||
from django.http import StreamingHttpResponse | ||
|
||
|
||
PRELOAD_AS = { | ||
'js': 'script', | ||
'css': 'style', | ||
'png': 'image', | ||
'jpg': 'image', | ||
'jpeg': 'image', | ||
'webp': 'image', | ||
'svg': 'image', | ||
'gif': 'image', | ||
'ttf': 'font', | ||
'woff': 'font', | ||
'woff2': 'font' | ||
} | ||
PRELOAD_ORDER = { | ||
'css': 0, | ||
'ttf': 1, | ||
'woff': 1, | ||
'woff2': 1, | ||
'js': 2, | ||
} | ||
|
||
|
||
cached_preload_urls = {} | ||
cached_response_types = {} | ||
|
||
|
||
|
||
def record_file_to_preload(request, url): | ||
"""save a staticfile to the list of files to push via HTTP2 preload""" | ||
if not hasattr(request, 'to_preload'): | ||
request.to_preload = set() | ||
|
||
request.to_preload.add(url) | ||
|
||
|
||
def create_preload_header(urls, nonce=None): | ||
"""Compose the Link: header contents from a list of urls""" | ||
without_vers = lambda url: url.split('?', 1)[0] | ||
extension = lambda url: url.rsplit('.', 1)[-1].lower() | ||
preload_priority = lambda url: PRELOAD_ORDER.get(url[1], 100) | ||
|
||
urls_with_ext = ((url, extension(without_vers(url))) for url in urls) | ||
sorted_urls = sorted(urls_with_ext, key=preload_priority) | ||
|
||
nonce = f'; nonce={nonce}' if nonce else '' | ||
nopush = '; nopush' if settings.HTTP2_SERVER_PUSH else '' | ||
|
||
preload_tags = ( | ||
f'<{url}>; rel=preload; crossorigin; as={PRELOAD_AS[ext]}{nonce}{nopush}' | ||
if ext in PRELOAD_AS else | ||
f'<{url}>; rel=preload; crossorigin{nonce}{nopush}' | ||
for url, ext in sorted_urls | ||
) | ||
return ', '.join(preload_tags) | ||
|
||
|
||
def get_cached_response_type(request): | ||
global cached_response_types | ||
return cached_response_types.get(request.path, '') | ||
|
||
def set_cached_response_type(request, response): | ||
global cached_response_types | ||
cached_response_types[request.path] = response['Content-Type'].split(';', 1)[0] | ||
|
||
def get_cached_preload_urls(request): | ||
global cached_preload_urls | ||
|
||
if settings.HTTP2_PRESEND_CACHED_HEADERS and request.path in cached_preload_urls: | ||
return cached_preload_urls[request.path] | ||
|
||
return () | ||
|
||
def set_cached_preload_urls(request): | ||
global cached_preload_urls | ||
|
||
if settings.HTTP2_PRESEND_CACHED_HEADERS and getattr(request, 'to_preload', None): | ||
cached_preload_urls[request.path] = request.to_preload | ||
|
||
|
||
def should_preload(request): | ||
request_type = request.META.get('HTTP_ACCEPT', '')[:36] | ||
cached_response_type = get_cached_response_type(request) | ||
# print('REQUEST TYPE', request_type) | ||
# print('CACHED RESPONSE TYPE', cached_response_type) | ||
return ( | ||
settings.HTTP2_PRELOAD_HEADERS | ||
and 'text/html' in request_type | ||
and 'text/html' in cached_response_type | ||
) | ||
|
||
def early_preload_response(request, get_response, nonce): | ||
def generate_response(): | ||
yield '' | ||
response = get_response(request) | ||
set_cached_response_type(request, response) | ||
yield response.content | ||
|
||
response = StreamingHttpResponse(generate_response()) | ||
response['Link'] = create_preload_header(request.to_preload, nonce) | ||
response['X-HTTP2-PRELOAD'] = 'early' | ||
|
||
# print('SENDING EARLY PRELOAD REQUEST', request.path, response['Content-Type']) | ||
return response | ||
|
||
def late_preload_response(request, get_response, nonce): | ||
response = get_response(request) | ||
set_cached_response_type(request, response) | ||
|
||
if getattr(request, 'to_preload'): | ||
preload_header = create_preload_header(request.to_preload, nonce) | ||
response['Link'] = preload_header | ||
set_cached_preload_urls(request) | ||
response['X-HTTP2-PRELOAD'] = 'late' | ||
|
||
# print('SENDING LATE PRELOAD REQUEST', request.path, response['Content-Type']) | ||
return response | ||
|
||
def preload_response(request, get_response): | ||
nonce = getattr(request, 'csp_nonce', None) | ||
cached_preload_urls = get_cached_preload_urls(request) | ||
if cached_preload_urls: | ||
request.to_preload = cached_preload_urls | ||
return early_preload_response(request, get_response, nonce) | ||
|
||
return late_preload_response(request, get_response, nonce) | ||
|
||
def no_preload_response(request, get_response): | ||
response = get_response(request) | ||
set_cached_response_type(request, response) | ||
# print('SENDING NO PRELOAD REQUEST', request.path, response['Content-Type']) | ||
response['X-HTTP2-PRELOAD'] = 'off' | ||
return response | ||
|
||
|
||
def HTTP2Middleware(get_response): | ||
def middleware(request): | ||
"""Attach a Link: header containing preload links for every staticfile | ||
referenced during the request by the {% http2static %} templatetag | ||
""" | ||
if should_preload(request): | ||
return preload_response(request, get_response) | ||
return no_preload_response(request, get_response) | ||
return middleware |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from django import template | ||
from django.contrib.staticfiles.templatetags.staticfiles import static | ||
|
||
from .middleware import record_file_to_preload | ||
|
||
register = template.Library() | ||
|
||
@register.simple_tag(takes_context=True) | ||
def http2static(context: dict, path: str, version: str=None) -> str: | ||
""" | ||
same as static templatetag, except it saves the list of files used | ||
to request.to_preload in order to push them up to the user | ||
before they request it using HTTP2 push via the HTTP2PushMiddleware | ||
""" | ||
url = f'{static(path)}?v={version}' if version else static(path) | ||
record_file_to_preload(context['request'], url) | ||
return url |