Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pirate committed Jun 23, 2019
1 parent 362cb88 commit d03c813
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
89 changes: 89 additions & 0 deletions README.md
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 added __init__.py
Empty file.
147 changes: 147 additions & 0 deletions middleware.py
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
17 changes: 17 additions & 0 deletions templatetags.py
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

0 comments on commit d03c813

Please sign in to comment.