-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
Copy pathmiddleware.py
247 lines (210 loc) · 8.84 KB
/
middleware.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import logging
from django.conf import settings
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.http import Http404, HttpResponseBadRequest
from django.urls.base import set_urlconf
from django.utils.deprecation import MiddlewareMixin
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import render
from readthedocs.projects.models import Domain, Project
log = logging.getLogger(__name__)
LOG_TEMPLATE = '(Middleware) {msg} [{host}{path}]'
SUBDOMAIN_URLCONF = getattr(
settings,
'SUBDOMAIN_URLCONF',
'readthedocs.core.urls.subdomain',
)
SINGLE_VERSION_URLCONF = getattr(
settings,
'SINGLE_VERSION_URLCONF',
'readthedocs.core.urls.single_version',
)
class SubdomainMiddleware(MiddlewareMixin):
"""Middleware to display docs for non-dashboard domains."""
def process_request(self, request):
"""
Process requests for unhandled domains.
If the request is not for our ``PUBLIC_DOMAIN``, or if ``PUBLIC_DOMAIN``
is not set and the request is for a subdomain on ``PRODUCTION_DOMAIN``,
process the request as a request a documentation project.
"""
if not getattr(settings, 'USE_SUBDOMAIN', False):
return None
full_host = host = request.get_host().lower()
path = request.get_full_path()
log_kwargs = dict(host=host, path=path)
public_domain = getattr(settings, 'PUBLIC_DOMAIN', None)
production_domain = getattr(
settings,
'PRODUCTION_DOMAIN',
'readthedocs.org',
)
if public_domain is None:
public_domain = production_domain
if ':' in host:
host = host.split(':')[0]
domain_parts = host.split('.')
# Serve subdomains - but don't depend on the production domain only having 2 parts
if len(domain_parts) == len(public_domain.split('.')) + 1:
subdomain = domain_parts[0]
is_www = subdomain.lower() == 'www'
if not is_www and ( # Support ports during local dev
public_domain in host or public_domain in full_host
):
if not Project.objects.filter(slug=subdomain).exists():
raise Http404(_('Project not found'))
request.subdomain = True
request.slug = subdomain
request.urlconf = SUBDOMAIN_URLCONF
return None
# Serve CNAMEs
if (
public_domain not in host and production_domain not in host and
'localhost' not in host and 'testserver' not in host
):
request.cname = True
domains = Domain.objects.filter(domain=host)
if domains.count():
for domain in domains:
if domain.domain == host:
request.slug = domain.project.slug
request.urlconf = SUBDOMAIN_URLCONF
request.domain_object = True
log.debug(
LOG_TEMPLATE.format(
msg='Domain Object Detected: %s' % domain.domain,
**log_kwargs
),
)
break
if (
not hasattr(request, 'domain_object') and
'HTTP_X_RTD_SLUG' in request.META
):
request.slug = request.META['HTTP_X_RTD_SLUG'].lower()
request.urlconf = SUBDOMAIN_URLCONF
request.rtdheader = True
log.debug(
LOG_TEMPLATE.format(
msg='X-RTD-Slug header detected: %s' % request.slug,
**log_kwargs
),
)
# Try header first, then DNS
elif not hasattr(request, 'domain_object'):
# Some person is CNAMEing to us without configuring a domain - 404.
log.warning(LOG_TEMPLATE.format(msg='CNAME 404', **log_kwargs))
return render(request, 'core/dns-404.html', context={'host': host}, status=404)
# Google was finding crazy www.blah.readthedocs.org domains.
# Block these explicitly after trying CNAME logic.
if len(domain_parts) > 3 and not settings.DEBUG:
# Stop www.fooo.readthedocs.org
if domain_parts[0] == 'www':
log.debug(LOG_TEMPLATE.format(
msg='404ing long domain', **log_kwargs
))
return HttpResponseBadRequest(_('Invalid hostname'))
log.debug(LOG_TEMPLATE.format(
msg='Allowing long domain name', **log_kwargs
))
# Normal request.
return None
def process_response(self, request, response):
# Reset URLconf for this thread
# to the original one.
set_urlconf(None)
return response
class SingleVersionMiddleware(MiddlewareMixin):
"""
Reset urlconf for requests for 'single_version' docs.
In settings.MIDDLEWARE, SingleVersionMiddleware must follow after
SubdomainMiddleware.
"""
def _get_slug(self, request):
"""
Get slug from URLs requesting docs.
If URL is like '/docs/<project_name>/', we split path
and pull out slug.
If URL is subdomain or CNAME, we simply read request.slug, which is
set by SubdomainMiddleware.
"""
slug = None
if hasattr(request, 'slug'):
# Handle subdomains and CNAMEs.
slug = request.slug.lower()
else:
# Handle '/docs/<project>/' URLs
path = request.get_full_path()
path_parts = path.split('/')
if len(path_parts) > 2 and path_parts[1] == 'docs':
slug = path_parts[2].lower()
return slug
def process_request(self, request):
slug = self._get_slug(request)
if slug:
try:
proj = Project.objects.get(slug=slug)
except (ObjectDoesNotExist, MultipleObjectsReturned):
# Let 404 be handled further up stack.
return None
if getattr(proj, 'single_version', False):
request.urlconf = SINGLE_VERSION_URLCONF
# Logging
host = request.get_host()
path = request.get_full_path()
log_kwargs = dict(host=host, path=path)
log.debug(
LOG_TEMPLATE.
format(msg='Handling single_version request', **log_kwargs),
)
return None
# Forked from old Django
class ProxyMiddleware(MiddlewareMixin):
"""
Middleware that sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, if the.
latter is set. This is useful if you're sitting behind a reverse proxy that
causes each request's REMOTE_ADDR to be set to 127.0.0.1. Note that this
does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind a reverse proxy
that sets HTTP_X_FORWARDED_FOR automatically, do not use this middleware.
Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and because this sets
REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means anybody can "fake"
their IP address. Only use this when you can absolutely trust the value of
HTTP_X_FORWARDED_FOR.
"""
def process_request(self, request):
try:
real_ip = request.META['HTTP_X_FORWARDED_FOR']
except KeyError:
return None
else:
# HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. The
# client's IP will be the first one.
real_ip = real_ip.split(',')[0].strip()
request.META['REMOTE_ADDR'] = real_ip
class FooterNoSessionMiddleware(SessionMiddleware):
"""
Middleware that doesn't create a session on logged out doc views.
This will reduce the size of our session table drastically.
"""
IGNORE_URLS = [
'/api/v2/footer_html', '/sustainability/view', '/sustainability/click',
]
def process_request(self, request):
for url in self.IGNORE_URLS:
if (
request.path_info.startswith(url) and
settings.SESSION_COOKIE_NAME not in request.COOKIES
):
# Hack request.session otherwise the Authentication middleware complains.
request.session = {}
return
super().process_request(request)
def process_response(self, request, response):
for url in self.IGNORE_URLS:
if (
request.path_info.startswith(url) and
settings.SESSION_COOKIE_NAME not in request.COOKIES
):
return response
return super().process_response(request, response)