cherrypy-foundation 1.0.0a1__py3-none-any.whl → 1.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cherrypy_foundation/components/Field.jinja +14 -6
- cherrypy_foundation/components/Typeahead.css +6 -1
- cherrypy_foundation/components/Typeahead.jinja +2 -2
- cherrypy_foundation/components/__init__.py +1 -1
- cherrypy_foundation/error_page.py +18 -15
- cherrypy_foundation/plugins/db.py +34 -8
- cherrypy_foundation/plugins/ldap.py +28 -22
- cherrypy_foundation/plugins/scheduler.py +197 -84
- cherrypy_foundation/plugins/smtp.py +71 -45
- cherrypy_foundation/plugins/tests/test_db.py +2 -2
- cherrypy_foundation/plugins/tests/test_scheduler.py +58 -50
- cherrypy_foundation/plugins/tests/test_smtp.py +9 -6
- cherrypy_foundation/tests/templates/test_form.html +10 -0
- cherrypy_foundation/tests/test_form.py +119 -0
- cherrypy_foundation/tools/auth.py +28 -11
- cherrypy_foundation/tools/auth_mfa.py +10 -13
- cherrypy_foundation/tools/errors.py +1 -1
- cherrypy_foundation/tools/i18n.py +15 -6
- cherrypy_foundation/tools/jinja2.py +15 -12
- cherrypy_foundation/tools/ratelimit.py +5 -9
- cherrypy_foundation/tools/secure_headers.py +0 -4
- cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +10 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +8 -0
- cherrypy_foundation/tools/tests/test_auth.py +1 -1
- cherrypy_foundation/tools/tests/test_auth_mfa.py +367 -0
- cherrypy_foundation/tools/tests/test_jinja2.py +123 -0
- {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/METADATA +1 -1
- {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/RECORD +33 -25
- {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/WHEEL +0 -0
- {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/licenses/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a3.dist-info}/top_level.txt +0 -0
|
@@ -63,12 +63,15 @@ class SmtpPluginTest(helper.CPWebCase):
|
|
|
63
63
|
|
|
64
64
|
@skipUnless(hasattr(cherrypy, 'scheduler'), reason='Required scheduler')
|
|
65
65
|
def test_queue_mail(self):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
|
|
67
|
+
# Given a mail being queued.
|
|
68
|
+
cherrypy.engine.publish('queue_mail', to='target@test.com', subject='subjet', message='body')
|
|
69
|
+
# When waiting for all task to be processed
|
|
70
|
+
cherrypy.scheduler.wait_for_jobs()
|
|
71
|
+
# Then smtplib is called to send the mail.
|
|
72
|
+
smtplib.SMTP.assert_called_once_with('__default__', 25)
|
|
73
|
+
smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
|
|
74
|
+
smtplib.SMTP.return_value.quit.assert_called_once_with()
|
|
72
75
|
|
|
73
76
|
def test_html2plaintext(self):
|
|
74
77
|
"""
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# CherryPy Foundation
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
import importlib
|
|
17
|
+
from unittest import skipUnless
|
|
18
|
+
|
|
19
|
+
import cherrypy
|
|
20
|
+
from cherrypy.test import helper
|
|
21
|
+
from wtforms.fields import BooleanField, PasswordField, StringField, SubmitField
|
|
22
|
+
from wtforms.validators import InputRequired, Length
|
|
23
|
+
|
|
24
|
+
import cherrypy_foundation.tools.jinja2 # noqa
|
|
25
|
+
from cherrypy_foundation.form import CherryForm
|
|
26
|
+
from cherrypy_foundation.tools.i18n import gettext_lazy as _
|
|
27
|
+
|
|
28
|
+
HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
|
|
29
|
+
|
|
30
|
+
env = cherrypy.tools.jinja2.create_env(
|
|
31
|
+
package_name=__package__,
|
|
32
|
+
globals={
|
|
33
|
+
'const1': 'STATIC VALUE',
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LoginForm(CherryForm):
|
|
39
|
+
login = StringField(
|
|
40
|
+
_('User'),
|
|
41
|
+
validators=[
|
|
42
|
+
InputRequired(),
|
|
43
|
+
Length(max=256, message=_('User too long.')),
|
|
44
|
+
],
|
|
45
|
+
render_kw={
|
|
46
|
+
"placeholder": _('User'),
|
|
47
|
+
"autocorrect": "off",
|
|
48
|
+
"autocapitalize": "none",
|
|
49
|
+
"autocomplete": "off",
|
|
50
|
+
"autofocus": "autofocus",
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
password = PasswordField(
|
|
54
|
+
_('Password'),
|
|
55
|
+
validators=[
|
|
56
|
+
InputRequired(),
|
|
57
|
+
Length(max=256, message=_('Password too long.')),
|
|
58
|
+
],
|
|
59
|
+
render_kw={"placeholder": _("Password")},
|
|
60
|
+
)
|
|
61
|
+
persistent = BooleanField(
|
|
62
|
+
_('Remember me'),
|
|
63
|
+
# All `label-*` are assigned to the label tag.
|
|
64
|
+
render_kw={'container_class': 'col-sm-6', 'label-attr': 'FOO'},
|
|
65
|
+
)
|
|
66
|
+
submit = SubmitField(
|
|
67
|
+
_('Login'),
|
|
68
|
+
# All `container-*` are assigned to the container tag.
|
|
69
|
+
render_kw={"class": "btn-primary float-end", 'container_class': 'col-sm-6', 'container-attr': 'BAR'},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@cherrypy.tools.jinja2(env=env)
|
|
74
|
+
class Root:
|
|
75
|
+
|
|
76
|
+
@cherrypy.expose
|
|
77
|
+
@cherrypy.tools.jinja2(template='test_form.html')
|
|
78
|
+
def index(self):
|
|
79
|
+
form = LoginForm()
|
|
80
|
+
return {'form': form}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@skipUnless(HAS_JINJAX, reason='Required jinjax')
|
|
84
|
+
class FormTest(helper.CPWebCase):
|
|
85
|
+
default_lang = None
|
|
86
|
+
interactive = False
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def setup_server(cls):
|
|
90
|
+
cherrypy.tree.mount(Root(), '/')
|
|
91
|
+
|
|
92
|
+
def test_get_form(self):
|
|
93
|
+
# Given a form
|
|
94
|
+
# When querying the page that include this form
|
|
95
|
+
self.getPage("/")
|
|
96
|
+
self.assertStatus(200)
|
|
97
|
+
# Then each field is render properly.
|
|
98
|
+
# 1. Check title
|
|
99
|
+
self.assertInBody('test-form')
|
|
100
|
+
# 2. Check user field
|
|
101
|
+
self.assertInBody('<label class="form-label" for="login">User</label>')
|
|
102
|
+
self.assertInBody(
|
|
103
|
+
'<input autocapitalize="none" autocomplete="off" autocorrect="off" autofocus="autofocus" class="form-control" id="login" maxlength="256" name="login" placeholder="User" required type="text" value="">'
|
|
104
|
+
)
|
|
105
|
+
# 3. Check password
|
|
106
|
+
self.assertInBody('<label class="form-label" for="password">Password</label>')
|
|
107
|
+
self.assertInBody(
|
|
108
|
+
'<input class="form-control" id="password" maxlength="256" name="password" placeholder="Password" required type="password" value="">'
|
|
109
|
+
)
|
|
110
|
+
# 4 Check remember me
|
|
111
|
+
self.assertInBody(
|
|
112
|
+
'<input class="form-check-input" container-class="col-sm-6" id="persistent" label-attr="FOO" name="persistent" type="checkbox" value="y">'
|
|
113
|
+
)
|
|
114
|
+
self.assertInBody('<label attr="FOO" class="form-check-label" for="persistent">Remember me</label>')
|
|
115
|
+
# 5. check submit button
|
|
116
|
+
self.assertInBody('<div attr="BAR" class="mb-2 form-field col-sm-6">')
|
|
117
|
+
self.assertInBody(
|
|
118
|
+
'<input class="btn-primary float-end btn" container-attr="BAR" container-class="col-sm-6" id="submit" name="submit" type="submit" value="Login">'
|
|
119
|
+
)
|
|
@@ -29,9 +29,6 @@ AUTH_DEFAULT_SESSION_KEY = "_auth_session_key"
|
|
|
29
29
|
AUTH_DEFAULT_REAUTH_TIMEOUT = 60 # minutes
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
logger = logging.getLogger(__name__)
|
|
33
|
-
|
|
34
|
-
|
|
35
32
|
class AuthManager(cherrypy.Tool):
|
|
36
33
|
"""
|
|
37
34
|
CherryPy tool handling authentication.
|
|
@@ -102,7 +99,12 @@ class AuthManager(cherrypy.Tool):
|
|
|
102
99
|
try:
|
|
103
100
|
currentuser = user_from_key_func(user_key)
|
|
104
101
|
except Exception:
|
|
105
|
-
|
|
102
|
+
cherrypy.log(
|
|
103
|
+
f'unexpected error searching for user_key={user_key}',
|
|
104
|
+
context='AUTH',
|
|
105
|
+
severity=logging.ERROR,
|
|
106
|
+
traceback=True,
|
|
107
|
+
)
|
|
106
108
|
currentuser = None
|
|
107
109
|
|
|
108
110
|
if currentuser:
|
|
@@ -122,7 +124,7 @@ class AuthManager(cherrypy.Tool):
|
|
|
122
124
|
Validate credentials with configured checkers; on success, call login_with_result.
|
|
123
125
|
"""
|
|
124
126
|
if not login or not password:
|
|
125
|
-
|
|
127
|
+
cherrypy.log('authentication failed reason=empty_credentials', context='AUTH', severity=logging.WARNING)
|
|
126
128
|
return None
|
|
127
129
|
# Validate credentials using checkpassword function(s).
|
|
128
130
|
conf = self._merged_args()
|
|
@@ -143,15 +145,23 @@ class AuthManager(cherrypy.Tool):
|
|
|
143
145
|
login, user_info = login, None
|
|
144
146
|
# If authentication is successful, initiate login process.
|
|
145
147
|
return self.login_with_result(login=login, user_info=user_info)
|
|
146
|
-
except Exception
|
|
147
|
-
|
|
148
|
-
'unexpected error
|
|
148
|
+
except Exception:
|
|
149
|
+
cherrypy.log(
|
|
150
|
+
f'unexpected error checking password login={login} checkpassword={func.__qualname__} - continue with next function',
|
|
151
|
+
context='AUTH',
|
|
152
|
+
severity=logging.ERROR,
|
|
153
|
+
traceback=True,
|
|
149
154
|
)
|
|
150
155
|
# If we reach here, authentication failed
|
|
151
156
|
if hasattr(cherrypy.serving, 'session'):
|
|
152
157
|
cherrypy.serving.session.regenerate() # Prevent session analysis
|
|
153
158
|
|
|
154
|
-
|
|
159
|
+
remote_ip = cherrypy.serving.request.remote.ip
|
|
160
|
+
cherrypy.log(
|
|
161
|
+
f'authentication failed login={login} ip={remote_ip} reason=wrong_credentials',
|
|
162
|
+
context='AUTH',
|
|
163
|
+
severity=logging.WARNING,
|
|
164
|
+
)
|
|
155
165
|
|
|
156
166
|
return None
|
|
157
167
|
|
|
@@ -166,11 +176,18 @@ class AuthManager(cherrypy.Tool):
|
|
|
166
176
|
try:
|
|
167
177
|
user_key, userobj = user_lookup_func(login=login, user_info=user_info or {})
|
|
168
178
|
except Exception:
|
|
169
|
-
|
|
179
|
+
cherrypy.log(
|
|
180
|
+
f"unexpected error searching user login={login} user_info={user_info}",
|
|
181
|
+
context='AUTH',
|
|
182
|
+
severity=logging.ERROR,
|
|
183
|
+
traceback=True,
|
|
184
|
+
)
|
|
170
185
|
return None
|
|
171
186
|
|
|
172
187
|
if not userobj:
|
|
173
|
-
|
|
188
|
+
cherrypy.log(
|
|
189
|
+
f"authentication failed login={login} reason=not_found", context='AUTH', severity=logging.WARNING
|
|
190
|
+
)
|
|
174
191
|
return None
|
|
175
192
|
|
|
176
193
|
# Notify plugins about user login
|
|
@@ -23,14 +23,12 @@ import cherrypy
|
|
|
23
23
|
|
|
24
24
|
from ..passwd import check_password, hash_password
|
|
25
25
|
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
26
|
MFA_CODE = '_auth_mfa_code'
|
|
29
27
|
MFA_CODE_TIME = '_auth_mfa_code_time'
|
|
30
28
|
MFA_CODE_ATTEMPT = '_auth_mfa_code_attempt'
|
|
31
29
|
MFA_REDIRECT_URL = '_auth_mfa_redirect_url'
|
|
32
30
|
MFA_TRUSTED_IP_LIST = '_auth_mfa_trusted_ip_list'
|
|
33
|
-
|
|
31
|
+
MFA_USER_KEY = '_auth_mfa_user_key'
|
|
34
32
|
MFA_VERIFICATION_TIME = '_auth_mfa_time'
|
|
35
33
|
|
|
36
34
|
MFA_DEFAULT_CODE_TIMEOUT = 10 # minutes
|
|
@@ -86,7 +84,7 @@ class CheckAuthMfa(cherrypy.Tool):
|
|
|
86
84
|
length = self._code_length()
|
|
87
85
|
code = ''.join(secrets.choice(string.digits) for _ in range(length))
|
|
88
86
|
session = cherrypy.serving.session
|
|
89
|
-
session[
|
|
87
|
+
session[MFA_USER_KEY] = cherrypy.request.login
|
|
90
88
|
session[MFA_CODE] = hash_password(code)
|
|
91
89
|
session[MFA_CODE_TIME] = session.now()
|
|
92
90
|
session[MFA_CODE_ATTEMPT] = 0
|
|
@@ -99,7 +97,7 @@ class CheckAuthMfa(cherrypy.Tool):
|
|
|
99
97
|
|
|
100
98
|
# Check if our username match current login user.
|
|
101
99
|
session = cherrypy.serving.session
|
|
102
|
-
if session.get(
|
|
100
|
+
if session.get(MFA_USER_KEY) != cherrypy.request.login:
|
|
103
101
|
return False
|
|
104
102
|
|
|
105
103
|
# Check if the current IP is trusted
|
|
@@ -128,7 +126,7 @@ class CheckAuthMfa(cherrypy.Tool):
|
|
|
128
126
|
return True
|
|
129
127
|
|
|
130
128
|
session = cherrypy.serving.session
|
|
131
|
-
if session.get(
|
|
129
|
+
if session.get(MFA_USER_KEY) != cherrypy.request.login:
|
|
132
130
|
return True
|
|
133
131
|
|
|
134
132
|
# Check if code is defined.
|
|
@@ -208,11 +206,10 @@ class CheckAuthMfa(cherrypy.Tool):
|
|
|
208
206
|
if is_expired or not code_valid:
|
|
209
207
|
if not is_expired: # Only increment if not expired
|
|
210
208
|
session[MFA_CODE_ATTEMPT] = attempts = int(session.get(MFA_CODE_ATTEMPT, 0)) + 1
|
|
211
|
-
|
|
212
|
-
'
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
attempts,
|
|
209
|
+
cherrypy.log(
|
|
210
|
+
f'verification failed user={cherrypy.request.login} ip={cherrypy.request.remote.ip} attempts={attempts}',
|
|
211
|
+
context='MFA',
|
|
212
|
+
severity=logging.WARNING,
|
|
216
213
|
)
|
|
217
214
|
return False
|
|
218
215
|
|
|
@@ -239,8 +236,8 @@ class CheckAuthMfa(cherrypy.Tool):
|
|
|
239
236
|
|
|
240
237
|
# Rotate session id to prevent fixation
|
|
241
238
|
session.regenerate()
|
|
242
|
-
|
|
243
|
-
'
|
|
239
|
+
cherrypy.log(
|
|
240
|
+
f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
|
|
244
241
|
)
|
|
245
242
|
return True
|
|
246
243
|
|
|
@@ -105,6 +105,15 @@ from babel.support import LazyProxy, NullTranslations, Translations
|
|
|
105
105
|
_current = threading.local()
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def _get_config(key, default=None):
|
|
109
|
+
"""
|
|
110
|
+
Lookup configuration from request, if available. Fallback to global config.
|
|
111
|
+
"""
|
|
112
|
+
if getattr(cherrypy, 'request') and getattr(cherrypy.request, 'config') and key in cherrypy.request.config:
|
|
113
|
+
return cherrypy.request.config[key]
|
|
114
|
+
return cherrypy.config.get(key, default)
|
|
115
|
+
|
|
116
|
+
|
|
108
117
|
@contextmanager
|
|
109
118
|
def preferred_lang(lang):
|
|
110
119
|
"""
|
|
@@ -199,7 +208,7 @@ def get_timezone():
|
|
|
199
208
|
return tzinfo
|
|
200
209
|
# Otherwise search for a valid timezone.
|
|
201
210
|
tzinfo = None
|
|
202
|
-
default_timezone =
|
|
211
|
+
default_timezone = _get_config('tools.i18n.default_timezone')
|
|
203
212
|
preferred_timezone = getattr(_current, 'preferred_timezone', [default_timezone])
|
|
204
213
|
for timezone in preferred_timezone:
|
|
205
214
|
try:
|
|
@@ -226,10 +235,10 @@ def get_translation():
|
|
|
226
235
|
# Otherwise, we need to search the translation.
|
|
227
236
|
# `preferred_lang` should always has a sane value within a cherrypy request because of hooks
|
|
228
237
|
# But we also need to support calls outside cherrypy.
|
|
229
|
-
default =
|
|
238
|
+
default = _get_config('tools.i18n.default')
|
|
230
239
|
preferred_lang = getattr(_current, 'preferred_lang', [default])
|
|
231
|
-
mo_dir =
|
|
232
|
-
domain =
|
|
240
|
+
mo_dir = _get_config('tools.i18n.mo_dir')
|
|
241
|
+
domain = _get_config('tools.i18n.domain')
|
|
233
242
|
trans = _search_translation(mo_dir, domain, *preferred_lang)
|
|
234
243
|
if trans is None:
|
|
235
244
|
trans = NullTranslations()
|
|
@@ -242,8 +251,8 @@ def list_available_locales():
|
|
|
242
251
|
"""
|
|
243
252
|
Return a list of available translations.
|
|
244
253
|
"""
|
|
245
|
-
mo_dir =
|
|
246
|
-
domain =
|
|
254
|
+
mo_dir = _get_config('tools.i18n.mo_dir', False)
|
|
255
|
+
domain = _get_config('tools.i18n.domain')
|
|
247
256
|
if not mo_dir:
|
|
248
257
|
return
|
|
249
258
|
for lang in os.listdir(mo_dir):
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
import importlib
|
|
18
|
+
import logging
|
|
18
19
|
import time
|
|
19
20
|
|
|
20
21
|
import cherrypy
|
|
@@ -51,15 +52,15 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
51
52
|
|
|
52
53
|
def wrap(*args, **kwargs):
|
|
53
54
|
# Call original handler
|
|
54
|
-
|
|
55
|
+
context = self.oldhandler(*args, **kwargs)
|
|
55
56
|
# Render template.
|
|
56
|
-
return self.render_request(env=env, template=template,
|
|
57
|
+
return self.render_request(env=env, template=template, context=context, extra_processor=extra_processor)
|
|
57
58
|
|
|
58
59
|
request = cherrypy.serving.request
|
|
59
60
|
if request.handler is not None:
|
|
60
61
|
# Replace request.handler with self
|
|
61
62
|
if debug:
|
|
62
|
-
cherrypy.log('
|
|
63
|
+
cherrypy.log('replacing request handler', context='TOOLS.JINJA2', severity=logging.DEBUG)
|
|
63
64
|
self.oldhandler = request.handler
|
|
64
65
|
request.handler = wrap
|
|
65
66
|
|
|
@@ -79,12 +80,14 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
79
80
|
|
|
80
81
|
# Enable translation if available
|
|
81
82
|
if hasattr(cherrypy.tools, 'i18n'):
|
|
82
|
-
from .i18n import format_datetime, get_language_name, ugettext, ungettext
|
|
83
|
+
from .i18n import format_date, format_datetime, get_language_name, get_translation, ugettext, ungettext
|
|
83
84
|
|
|
84
85
|
env.add_extension('jinja2.ext.i18n')
|
|
85
86
|
env.install_gettext_callables(ugettext, ungettext, newstyle=True)
|
|
87
|
+
env.filters['format_date'] = format_date
|
|
86
88
|
env.filters['format_datetime'] = format_datetime
|
|
87
89
|
env.globals['get_language_name'] = get_language_name
|
|
90
|
+
env.globals['get_translation'] = get_translation
|
|
88
91
|
|
|
89
92
|
# Update globals, filters and tests
|
|
90
93
|
env.globals.update(globals)
|
|
@@ -104,7 +107,7 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
104
107
|
|
|
105
108
|
return env
|
|
106
109
|
|
|
107
|
-
def render_request(self, template,
|
|
110
|
+
def render_request(self, template, context={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
|
|
108
111
|
"""
|
|
109
112
|
Render template for a given cherrypy request.
|
|
110
113
|
"""
|
|
@@ -114,24 +117,24 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
114
117
|
if extra_processor is _UNDEFINED:
|
|
115
118
|
extra_processor = request.config.get('tools.jinja2.extra_processor')
|
|
116
119
|
# Execute extra processor if defined.
|
|
117
|
-
|
|
120
|
+
new_context = {}
|
|
118
121
|
if extra_processor:
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
new_context.update(extra_processor())
|
|
123
|
+
new_context.update(context)
|
|
121
124
|
# Render templates
|
|
122
|
-
return self.render(env=env, template=template,
|
|
125
|
+
return self.render(env=env, template=template, context=new_context)
|
|
123
126
|
|
|
124
|
-
def render(self, env, template,
|
|
127
|
+
def render(self, env, template, context={}):
|
|
125
128
|
"""
|
|
126
129
|
Lower level function used to render a template using the given jinja2 environment, template(s) and variable context.
|
|
127
130
|
"""
|
|
128
131
|
# Get the right templates
|
|
129
132
|
if isinstance(template, (list, tuple)):
|
|
130
|
-
names = [t.format(**
|
|
133
|
+
names = [t.format(**context) for t in template]
|
|
131
134
|
tmpl = env.select_template(names)
|
|
132
135
|
else:
|
|
133
136
|
tmpl = env.get_template(template)
|
|
134
|
-
out = tmpl.render(
|
|
137
|
+
out = tmpl.render(context)
|
|
135
138
|
|
|
136
139
|
# With JinjaX > 0.60 render explicitly here.
|
|
137
140
|
if 'catalog' in env.globals and getattr(env.globals['catalog'], '_emit_assets_later', False):
|
|
@@ -146,6 +146,8 @@ class FileRateLimit(_DataStore):
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
class Ratelimit(cherrypy.Tool):
|
|
149
|
+
CONTEXT = 'TOOLS.RATELIMIT'
|
|
150
|
+
|
|
149
151
|
def __init__(self, priority=60):
|
|
150
152
|
super().__init__('before_handler', self.check_ratelimit, 'ratelimit', priority)
|
|
151
153
|
|
|
@@ -182,10 +184,7 @@ class Ratelimit(cherrypy.Tool):
|
|
|
182
184
|
request = cherrypy.request
|
|
183
185
|
if methods is not None and request.method not in methods:
|
|
184
186
|
if debug:
|
|
185
|
-
cherrypy.log(
|
|
186
|
-
'skip rate limit for HTTP method %s' % (request.method,),
|
|
187
|
-
'TOOLS.RATELIMIT',
|
|
188
|
-
)
|
|
187
|
+
cherrypy.log(f'skip rate limit for HTTP method {request.method}', context=self.CONTEXT)
|
|
189
188
|
return
|
|
190
189
|
|
|
191
190
|
# If datastore is not pass as configuration, create it for the first time.
|
|
@@ -213,10 +212,7 @@ class Ratelimit(cherrypy.Tool):
|
|
|
213
212
|
# Get hits count using datastore.
|
|
214
213
|
hits, timeout = datastore.get_and_increment(token, delay, hit)
|
|
215
214
|
if debug:
|
|
216
|
-
cherrypy.log(
|
|
217
|
-
'check and increase rate limit for scope %s, limit %s, hits %s' % (token, limit, hits),
|
|
218
|
-
'TOOLS.RATELIMIT',
|
|
219
|
-
)
|
|
215
|
+
cherrypy.log(f'check and increase limit token={token} limit={limit} hits={hits}', context=self.CONTEXT)
|
|
220
216
|
|
|
221
217
|
# Verify user has not exceeded rate limit
|
|
222
218
|
remaining = max(0, limit - hits)
|
|
@@ -227,7 +223,7 @@ class Ratelimit(cherrypy.Tool):
|
|
|
227
223
|
cherrypy.response.headers['X-RateLimit-Reset'] = str(timeout)
|
|
228
224
|
|
|
229
225
|
if limit < hits: # block only after 'limit' successful requests
|
|
230
|
-
cherrypy.log('
|
|
226
|
+
cherrypy.log(f'block access to path_info={request.path_info}', context=self.CONTEXT)
|
|
231
227
|
if logout:
|
|
232
228
|
if hasattr(cherrypy.serving, 'session'):
|
|
233
229
|
cherrypy.serving.session.clear()
|
|
@@ -15,13 +15,9 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
import http.cookies
|
|
18
|
-
import logging
|
|
19
18
|
|
|
20
19
|
import cherrypy
|
|
21
20
|
|
|
22
|
-
# Define the logger
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
21
|
DEFAULT_CSP = {
|
|
26
22
|
'default-src': 'self',
|
|
27
23
|
'style-src': ('self', 'unsafe-inline'),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<html lang="{{ get_translation().locale }}">
|
|
2
|
+
<head>
|
|
3
|
+
<title>test-jinja2</title>
|
|
4
|
+
</head>
|
|
5
|
+
<body>
|
|
6
|
+
{{ get_language_name(get_translation().locale) }}<br/>
|
|
7
|
+
{% trans %}Some text to translate{% endtrans %}<br/>
|
|
8
|
+
{{ my_datetime | format_datetime(format='full') }}<br/>
|
|
9
|
+
{{ my_date | format_date(format='full') }}
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -83,7 +83,7 @@ class AuthManagerTest(helper.CPWebCase):
|
|
|
83
83
|
def test_auth_login(self):
|
|
84
84
|
# Given unauthenticated user
|
|
85
85
|
# When posting valid login
|
|
86
|
-
self.getPage('/login/', method='POST', body=urlencode(
|
|
86
|
+
self.getPage('/login/', method='POST', body=urlencode({'username': 'myuser', 'password': 'changeme'}))
|
|
87
87
|
# Then user is redirect to index.
|
|
88
88
|
self.assertStatus(303)
|
|
89
89
|
self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
|