Skip to content

Commit 31088bb

Browse files
Support Redis & Memcached cache servers
* Sign pickles before caching to prevent tampering * Move view tracking to a receiver so it doesn't get cached
1 parent 5f47db8 commit 31088bb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+426
-111
lines changed

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ coverage.xml
6363
status_auth.txt
6464
!docker/uwsgi.ini
6565

66+
# Redis stuff
67+
*.rdb
68+
*.aof
69+
6670
# MangAdventure stuff
6771
media/
6872
static/admin/

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ DB_URL="sqlite:///db.sqlite3"
7878
## Example (TLS): smtp+tls://admin:6ceab2590b52c249741b@example.com:587
7979
EMAIL_URL="smtp+tls://<user>:<password>@smtp.gmail.com"
8080

81+
# The URL of your cache server. Special characters must be urlencoded.
82+
# You can set multiple servers by separating them with a semicolon.
83+
## Redis:
84+
### Example (basic): redis://127.0.0.1:6379
85+
### Example (authenticated): redis://username:password@127.0.0.1:6379
86+
### Example (replicas): redis://127.0.0.1:6379;redis://127.0.0.1:6378
87+
## Memcached:
88+
### Example (HTTP): 127.0.0.1:11211
89+
### Example (socket): unix:/var/tmp/memcached.sock
90+
### Example (distributed): 172.19.26.240:11211;172.19.26.242:11211
91+
CACHE_URL=""
92+
8193
# The default e-mail address of your site.
8294
EMAIL_ADDRESS="<user>@${DOMAIN}"
8395

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
with:
2020
python-version: "3.8"
2121
- name: "Install dependencies"
22-
run: pip install wheel && pip install -e .[dev,csp]
22+
run: pip install wheel && pip install -e .[dev,csp,redis,memc]
2323
- name: "Set up .env file"
2424
run: cp .env.example .env
2525
- name: "Lint project"
@@ -76,7 +76,7 @@ jobs:
7676
- name: "Lint project"
7777
run: flake8 && isort -q -c --df . && mypy .
7878
- name: "Run tests"
79-
run: py.test
79+
run: py.test --cov-report=xml
8080
if: success()
8181
env:
8282
DB_TYPE: mysql
@@ -129,7 +129,7 @@ jobs:
129129
- name: "Lint project"
130130
run: flake8 && isort -q -c --df . && mypy .
131131
- name: "Run tests"
132-
run: py.test
132+
run: py.test --cov-report=xml
133133
if: success()
134134
env:
135135
DB_TYPE: postgresql

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ coverage.xml
6363
status_auth.txt
6464
!docker/uwsgi.ini
6565

66+
# Redis stuff
67+
*.rdb
68+
*.aof
69+
6670
# MangAdventure stuff
6771
media/
6872
static/admin/

MangAdventure/cache.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from hashlib import blake2b
2+
from pickle import UnpicklingError, dumps, loads
3+
from secrets import compare_digest
4+
from typing import Any, Tuple
5+
6+
from django.conf import settings
7+
from django.core.cache.backends.memcached import PyLibMCCache
8+
from django.core.cache.backends.redis import (
9+
RedisCache, RedisCacheClient, RedisSerializer
10+
)
11+
12+
13+
def _sign_data(data: bytes) -> bytes:
14+
return blake2b(
15+
data, digest_size=16,
16+
key=settings.SECRET_KEY.encode()
17+
).digest()
18+
19+
20+
class SignedRedisCache(RedisCache):
21+
"A cache binding using redis and signed pickles"
22+
23+
def __init__(self, *args):
24+
super().__init__(*args)
25+
26+
class _SignedRedisSerializer(RedisSerializer):
27+
def dumps(self, obj: Any) -> Any:
28+
if type(obj) is int:
29+
return obj
30+
data = dumps(obj, self.protocol)
31+
return _sign_data(data) + data
32+
33+
def loads(self, data: Any) -> Any:
34+
try:
35+
return int(data)
36+
except ValueError:
37+
sig, obj = data[:16], data[16:]
38+
if compare_digest(sig, _sign_data(obj)):
39+
return loads(obj)
40+
raise UnpicklingError('Signatures do not match')
41+
42+
class _SignedRedisCacheClient(RedisCacheClient):
43+
def __init__(self, *args, **kwargs):
44+
super().__init__(*args, **kwargs)
45+
self._serializer = _SignedRedisSerializer()
46+
47+
self._class = _SignedRedisCacheClient
48+
49+
50+
class SignedPyLibMCCache(PyLibMCCache):
51+
"A cache binding using pylibmc and signed pickles"
52+
53+
def __init__(self, *args):
54+
super().__init__(*args)
55+
56+
def _is_pickle(flag: int) -> bool:
57+
return flag & 23 == 1
58+
59+
class _SignedMCClient(self._lib.Client):
60+
def serialize(self, value: Any) -> Tuple[bytes, int]:
61+
data, flag = super().serialize(value)
62+
if _is_pickle(flag):
63+
return _sign_data(data) + data, flag
64+
return data, flag
65+
66+
def deserialize(self, data: bytes, flag: int) -> Any:
67+
if _is_pickle(flag):
68+
sig, obj = data[:16], data[16:]
69+
if compare_digest(sig, _sign_data(obj)):
70+
return loads(obj)
71+
raise UnpicklingError('Signatures do not match')
72+
return super().deserialize(data, flag)
73+
74+
self._class = _SignedMCClient
75+
76+
77+
__all__ = ['SignedRedisCache', 'SignedPyLibMCCache']

MangAdventure/middleware.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class HttpResponseTooEarly(HttpResponse):
1919

2020
class BaseMiddleware(CommonMiddleware):
2121
"""``CommonMiddleware`` with custom patches."""
22+
2223
def __call__(self, request: HttpRequest) -> HttpResponse:
2324
"""
2425
Patched to allow :const:`blocked user agents
@@ -41,6 +42,7 @@ def __call__(self, request: HttpRequest) -> HttpResponse:
4142

4243
class PreloadMiddleware:
4344
"""Middleware that allows for preloading resources."""
45+
4446
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
4547
self.get_response = get_response
4648

MangAdventure/settings.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@
8787
'django.contrib.auth.middleware.AuthenticationMiddleware',
8888
'django.contrib.messages.middleware.MessageMiddleware',
8989
'django.middleware.cache.FetchFromCacheMiddleware',
90-
'django.middleware.clickjacking.XFrameOptionsMiddleware',
9190
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
9291
]
9392

@@ -117,13 +116,37 @@
117116
#: Default primary key field type to use.
118117
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
119118

120-
##################
121-
# Database #
122-
##################
119+
############################
120+
# Database & Caching #
121+
############################
123122

124123
#: Database settings dictionary. See :setting:`DATABASES`.
125124
DATABASES = {'default': env.db('DB_URL', 'sqlite:///db.sqlite3')}
126125

126+
#: Cache settings dictionary. See :setting:`CACHES`
127+
CACHES = {}
128+
if find_spec('redis'): # pragma: no cover
129+
CACHES['default'] = {
130+
'BACKEND': 'MangAdventure.cache.SignedRedisCache',
131+
'LOCATION': env['CACHE_URL'],
132+
'KEY_PREFIX': env['NAME']
133+
}
134+
elif find_spec('pylibmc'): # pragma: no cover
135+
CACHES['default'] = {
136+
'BACKEND': 'MangAdventure.cache.SignedPyLibMCCache',
137+
'LOCATION': env['CACHE_URL'],
138+
'KEY_PREFIX': env['NAME']
139+
}
140+
else:
141+
__import__('warnings').warn_explicit((
142+
"Neither redis-py nor pylibmc were installed. "
143+
"Falling back to unsigned local memory cache."
144+
), UserWarning, __file__, 146)
145+
CACHES['default'] = {
146+
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
147+
'KEY_PREFIX': env['NAME']
148+
}
149+
127150
##################################
128151
# Logging & Error Handling #
129152
##################################
@@ -382,8 +405,7 @@
382405
SESSION_COOKIE_SAMESITE = 'Strict'
383406

384407
if env.bool('HTTPS', True):
385-
env.ENV['wsgi.url_scheme'] = 'https'
386-
MIDDLEWARE.append('MangAdventure.middleware.PreloadMiddleware')
408+
MIDDLEWARE.insert(-2, 'MangAdventure.middleware.PreloadMiddleware')
387409

388410
#: HTTP header/value combination that signifies a secure request.
389411
#: See :setting:`SECURE_PROXY_SSL_HEADER`.
@@ -560,7 +582,7 @@
560582
ALLOWED_HOSTS += [f'192.168.1.{i}' for i in range(2, 256)]
561583
if find_spec('debug_toolbar'):
562584
INSTALLED_APPS.append('debug_toolbar')
563-
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
585+
MIDDLEWARE.insert(3, 'debug_toolbar.middleware.DebugToolbarMiddleware')
564586
TEMPLATES[0]['OPTIONS']['context_processors'].append(
565587
'django.template.context_processors.debug'
566588
)

MangAdventure/sitemaps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
class MiscSitemap(Sitemap):
1010
"""Sitemap for miscellaneous pages."""
11+
1112
def items(self) -> Tuple[str, ...]:
1213
"""
1314
Get a tuple of the sitemap's items.

MangAdventure/storage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class CDNStorage(FileSystemStorage):
2525
.. _weserv: https://images.weserv.nl/docs/
2626
.. _photon: https://developer.wordpress.com/docs/photon/
2727
"""
28+
2829
def __init__(self, fit: Optional[Tuple[int, int]] = None):
2930
super().__init__()
3031
self._cdn = cast(str, settings.CONFIG['USE_CDN']).lower()

MangAdventure/templates/manifest.webmanifest

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"start_url": "/",
3-
"lang": "{{lang}}",
4-
"name": "{{name}}",
5-
"description": "{{description}}",
6-
"background_color": "{{background}}",
7-
"theme_color": "{{color}}",
3+
"lang": "{{ lang }}",
4+
"name": "{{ name }}",
5+
"description": "{{ description }}",
6+
"background_color": "{{ background }}",
7+
"theme_color": "{{ color }}",
88
"display": "standalone",
99
"orientation": "portrait-primary",
1010
"prefer_related_applications": false,

0 commit comments

Comments
 (0)