forked from morepath/morepath
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcore.py
More file actions
224 lines (156 loc) · 6.45 KB
/
core.py
File metadata and controls
224 lines (156 loc) · 6.45 KB
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
"""This module contains default Morepath configuration shared by
all Morepath applications. It is the only part of the Morepath
implementation that uses directives like user of Morepath does.
It uses Morepath directives to configure:
* view predicates (for model, request method, etc), including
what HTTP errors should be returned when a view cannot be matched.
* converters for common Python values (int, date, etc)
* a tween that catches exceptions raised by application code
and looks up an exception view for it.
* a default exception view for HTTP exceptions defined by
:mod:`webob.exc`, i.e. subclasses of :class:`webob.exc.HTTPException`.
Should you wish to do so you could even override these directives in a
subclass of :class:`morepath.App`. We do not guarantee we won't break
your code with future version of Morepath if you do that, though.
"""
import re
from reg import KeyIndex, ClassIndex
from datetime import datetime, date
from time import mktime, strptime
from webob.exc import (
HTTPException, HTTPNotFound, HTTPMethodNotAllowed,
HTTPUnprocessableEntity, HTTPOk, HTTPRedirection, HTTPBadRequest)
from .directive import App # install directives with App
from .converter import Converter, IDENTITY_CONVERTER
@App.predicate(App.get_view, name='model', default=None, index=ClassIndex)
def model_predicate(obj):
"""match model argument by class.
Predicate for :meth:`morepath.App.view`.
"""
return obj.__class__
@App.predicate_fallback(App.get_view, model_predicate)
def model_not_found(self, obj, request):
"""if model not matched, HTTP 404.
Fallback for :meth:`morepath.App.view`.
"""
raise HTTPNotFound()
@App.predicate(App.get_view, name='name', default='', index=KeyIndex,
after=model_predicate)
def name_predicate(request):
"""match name argument with request.view_name.
Predicate for :meth:`morepath.App.view`.
"""
return request.view_name
@App.predicate_fallback(App.get_view, name_predicate)
def name_not_found(self, obj, request):
"""if name not matched, HTTP 404.
Fallback for :meth:`morepath.App.view`.
"""
raise HTTPNotFound()
@App.predicate(App.get_view, name='request_method', default='GET',
index=KeyIndex, after=name_predicate)
def request_method_predicate(request):
"""match request method.
Predicate for :meth:`morepath.App.view`.
"""
return request.method
@App.predicate_fallback(App.get_view, request_method_predicate)
def method_not_allowed(self, obj, request):
"""if request predicate not matched, method not allowed.
Fallback for :meth:`morepath.App.view`.
"""
raise HTTPMethodNotAllowed()
@App.predicate(App.get_view, name='body_model', default=object,
index=ClassIndex, after=request_method_predicate)
def body_model_predicate(request):
"""match request.body_obj with body_model by class.
Predicate for :meth:`morepath.App.view`.
"""
return request.body_obj.__class__
@App.predicate_fallback(App.get_view, body_model_predicate)
def body_model_unprocessable(self, obj, request):
"""if body_model not matched, 422.
Fallback for :meth:`morepath.App.view`.
"""
raise HTTPUnprocessableEntity()
@App.converter(type=int)
def int_converter():
"""Converter for int."""
return Converter(int)
@App.converter(type=type(u""))
def unicode_converter():
"""Converter for text."""
return IDENTITY_CONVERTER
# Python 2
if type(u"") != type(""): # pragma: no cover # noqa
@App.converter(type=type(""))
def str_converter():
"""Converter for non-text str."""
# XXX do we want to decode/encode unicode?
return IDENTITY_CONVERTER
def date_decode(s):
return date.fromtimestamp(mktime(strptime(s, '%Y%m%d')))
def date_encode(d):
return d.strftime('%Y%m%d')
@App.converter(type=date)
def date_converter():
"""Converter for date."""
return Converter(date_decode, date_encode)
def datetime_decode(s):
return datetime.fromtimestamp(mktime(strptime(s, '%Y%m%dT%H%M%S')))
def datetime_encode(d):
return d.strftime('%Y%m%dT%H%M%S')
@App.converter(type=datetime)
def datetime_converter():
"""Converter for datetime."""
return Converter(datetime_decode, datetime_encode)
@App.tween_factory()
def excview_tween_factory(app, handler):
"""Exception views.
If an exception is raised by application code and a view is
declared for that exception class, use it.
If no view can be found, raise it all the way up -- this will be a
500 internal server error and an exception logged.
"""
def excview_tween(request):
try:
response = handler(request)
except Exception as exc:
# we must use component_by_keys here because we
# do not want the request to feature in the lookup;
# we don't want its request method or name to influence
# exception lookup
view = app.get_view.component_by_keys(model=exc.__class__)
if view is None:
raise
# we don't want to run any after already set in the exception view
if not isinstance(exc, (HTTPOk, HTTPRedirection)):
request.clear_after()
return view(app, exc, request)
return response
return excview_tween
@App.tween_factory(over=excview_tween_factory)
def poisoned_host_header_protection_tween_factory(app, handler):
"""Protect Morepath applications against the most basic host header
poisoning attacts.
The regex approach has been copied from the Django project. To find more
about this particular kind of attack have a look at the following
references:
* http://skeletonscribe.net/2013/05/practical-http-host-header-attacks
* https://www.djangoproject.com/weblog/2012/dec/10/security/
* https://github.com/django/django/commit/77b06e41516d8136b56c040cba7e235b
"""
valid_host_re = re.compile(
r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9:]+\])(:\d+)?$")
def poisoned_host_header_protection_tween(request):
if not valid_host_re.match(request.host):
return HTTPBadRequest("Invalid HOST header")
return handler(request)
return poisoned_host_header_protection_tween
@App.view(model=HTTPException)
def standard_exception_view(self, request):
"""We want the webob standard responses for any webob-based HTTP exception.
Applies to subclasses of :class:`webob.HTTPException`.
"""
# webob HTTPException is a response already
return self