pyweber 1.3.1__tar.gz → 1.5.0.dev0__tar.gz
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.
- {pyweber-1.3.1/pyweber.egg-info → pyweber-1.5.0.dev0}/PKG-INFO +3 -2
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyproject.toml +4 -3
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/__init__.py +49 -3
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/components/form.py +23 -9
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/components/general.py +2 -2
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/components/input.py +13 -13
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/connection/http.py +57 -6
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/connection/websocket.py +37 -12
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/core/__init__.py +9 -1
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/core/element.py +18 -14
- pyweber-1.5.0.dev0/pyweber/core/html_parser.py +213 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/__init__.py +27 -1
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/create_app.py +7 -2
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/element.py +34 -16
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/field_storage.py +13 -11
- pyweber-1.5.0.dev0/pyweber/models/file.py +35 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/middleware.py +104 -27
- pyweber-1.5.0.dev0/pyweber/models/openapi.py +1004 -0
- pyweber-1.5.0.dev0/pyweber/models/rate_limit.py +84 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/request.py +1 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/response.py +36 -6
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/routes.py +189 -44
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/run.py +2 -2
- pyweber-1.5.0.dev0/pyweber/models/security.py +394 -0
- pyweber-1.5.0.dev0/pyweber/models/stream_stats.py +5 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/pyweber/pyweber.py +479 -140
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/config.toml +9 -0
- pyweber-1.5.0.dev0/pyweber/static/docs.html +19 -0
- pyweber-1.5.0.dev0/pyweber/testing/__init__.py +3 -0
- pyweber-1.5.0.dev0/pyweber/testing/client.py +126 -0
- pyweber-1.5.0.dev0/pyweber/utils/deprecation.py +87 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/utils/exceptions.py +22 -3
- pyweber-1.3.1/pyweber/utils/types.py → pyweber-1.5.0.dev0/pyweber/utils/icons.py +2 -705
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/utils/loads.py +20 -2
- pyweber-1.5.0.dev0/pyweber/utils/mime.py +69 -0
- pyweber-1.5.0.dev0/pyweber/utils/security.py +197 -0
- pyweber-1.5.0.dev0/pyweber/utils/types.py +725 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0/pyweber.egg-info}/PKG-INFO +3 -2
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber.egg-info/SOURCES.txt +10 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber.egg-info/requires.txt +3 -1
- pyweber-1.3.1/pyweber/models/file.py +0 -15
- pyweber-1.3.1/pyweber/models/openapi.py +0 -381
- pyweber-1.3.1/pyweber/static/docs.html +0 -28
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/LICENSE +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/MANIFEST.in +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/NOTICE +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/README.md +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/admin/index.html +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/admin/src/script.js +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/admin/src/style.css +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/cli/__init__.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/cli/commands.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/components/__init__.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/config/__init__.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/config/config.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/connection/__init__.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/connection/reload.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/connection/selector.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/connection/session.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/core/events.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/core/template.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/core/window.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/context.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/cookies.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/error_pages.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/field.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/file_stream.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/handoff.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/headers.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/strem_stats.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/task_manager.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/template_diff.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/models/ws_message.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/pyweber/__init__.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/.gitignore +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/css.css +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/apple-touch-icon.png +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/favicon-16x16.png +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/favicon-32x32.png +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/favicon.ico +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/pyweber.png +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/favicon/site.webmanifest +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/handlers.js +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/html.html +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/html401.html +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/html404.html +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/html500.html +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/js.js +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/loading.html +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/main.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/pyweber.css +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/static/update.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/utils/__init__.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/utils/async_utils.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber/utils/utils.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber.egg-info/dependency_links.txt +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber.egg-info/entry_points.txt +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/pyweber.egg-info/top_level.txt +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/setup.cfg +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/tests/test_config.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/tests/test_context.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/tests/test_cookies.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/tests/test_public_api.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/tests/test_response.py +0 -0
- {pyweber-1.3.1 → pyweber-1.5.0.dev0}/tests/test_template_diff.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyweber
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0.dev0
|
|
4
4
|
Summary: A lightweight Python framework for building and managing web applications.
|
|
5
5
|
Author-email: DevPythonMZ <pypi.dev@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -15,7 +15,6 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
License-File: NOTICE
|
|
17
17
|
Requires-Dist: watchdog
|
|
18
|
-
Requires-Dist: lxml
|
|
19
18
|
Requires-Dist: toml
|
|
20
19
|
Requires-Dist: questionary
|
|
21
20
|
Requires-Dist: qrcode[pil]
|
|
@@ -23,6 +22,8 @@ Provides-Extra: dev
|
|
|
23
22
|
Requires-Dist: pytest; extra == "dev"
|
|
24
23
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
25
24
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
25
|
+
Provides-Extra: fast-html
|
|
26
|
+
Requires-Dist: lxml; extra == "fast-html"
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|
# Pyweber Framework
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyweber"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0.dev0"
|
|
8
8
|
description = "A lightweight Python framework for building and managing web applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "DevPythonMZ", email = "pypi.dev@gmail.com" }]
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
|
-
dependencies = ["watchdog", "
|
|
13
|
+
dependencies = ["watchdog", "toml", "questionary", "qrcode[pil]"]
|
|
14
14
|
[project.urls]
|
|
15
15
|
Documentation = "https://pyweber.dev/"
|
|
16
16
|
Source = "https://github.com/pyweber/pyweber"
|
|
@@ -28,6 +28,7 @@ include = ["pyweber*"]
|
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
dev = ["pytest", "pytest-cov", "pytest-asyncio"]
|
|
31
|
+
fast-html = ["lxml"]
|
|
31
32
|
|
|
32
33
|
[tool.setuptools.package-data]
|
|
33
34
|
pyweber = ["static/**/*", "admin/**/*"]
|
|
@@ -36,5 +37,5 @@ pyweber = ["static/**/*", "admin/**/*"]
|
|
|
36
37
|
omit = ["pyweber/cli/commands.py"]
|
|
37
38
|
|
|
38
39
|
[tool.coverage.report]
|
|
39
|
-
fail_under =
|
|
40
|
+
fail_under = 87.5
|
|
40
41
|
show_missing = true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pyweber run
|
|
1
|
+
# pyweber run
|
|
2
2
|
from .models.run import (
|
|
3
3
|
run,
|
|
4
4
|
run_as_asgi
|
|
@@ -39,6 +39,22 @@ from .models.strem_stats import (
|
|
|
39
39
|
AdaptiveController,
|
|
40
40
|
StreamStats
|
|
41
41
|
)
|
|
42
|
+
from .models.openapi import (
|
|
43
|
+
OpenApiProcessor,
|
|
44
|
+
OpenAPIConfig,
|
|
45
|
+
OpenAPIBuilder,
|
|
46
|
+
HTTPBearer,
|
|
47
|
+
HTTPBasic,
|
|
48
|
+
APIKeyHeader,
|
|
49
|
+
APIKeyQuery,
|
|
50
|
+
APIKeyCookie,
|
|
51
|
+
)
|
|
52
|
+
from .models.security import (
|
|
53
|
+
AuthContext,
|
|
54
|
+
SecurityEnforcer,
|
|
55
|
+
SecurityError,
|
|
56
|
+
ForbiddenError,
|
|
57
|
+
)
|
|
42
58
|
|
|
43
59
|
# pyweber events
|
|
44
60
|
from .core.events import (
|
|
@@ -54,6 +70,16 @@ from .utils.utils import (
|
|
|
54
70
|
WriteLine,
|
|
55
71
|
Colors
|
|
56
72
|
)
|
|
73
|
+
from .utils.security import (
|
|
74
|
+
secure_filename,
|
|
75
|
+
safe_join,
|
|
76
|
+
sign_value,
|
|
77
|
+
unsign_value,
|
|
78
|
+
generate_csrf_token,
|
|
79
|
+
)
|
|
80
|
+
from .utils.icons import Icons
|
|
81
|
+
from .utils.deprecation import warn_deprecated
|
|
82
|
+
from .testing import TestClient
|
|
57
83
|
|
|
58
84
|
from .utils.types import (
|
|
59
85
|
ContentTypes,
|
|
@@ -64,7 +90,6 @@ from .utils.types import (
|
|
|
64
90
|
NonSelfClosingHTMLTags,
|
|
65
91
|
WebSocketStatusCode,
|
|
66
92
|
WindowEventType,
|
|
67
|
-
Icons,
|
|
68
93
|
GetBy,
|
|
69
94
|
DateFormat,
|
|
70
95
|
DateTimeFormat,
|
|
@@ -79,7 +104,8 @@ from .utils.types import (
|
|
|
79
104
|
Int32Format,
|
|
80
105
|
Int64Format,
|
|
81
106
|
FloatFormat,
|
|
82
|
-
|
|
107
|
+
DoubleFormat,
|
|
108
|
+
DoubleFormnat,
|
|
83
109
|
)
|
|
84
110
|
from .utils.exceptions import (
|
|
85
111
|
InvalidRouteFormatError,
|
|
@@ -169,6 +195,11 @@ __all__ = [
|
|
|
169
195
|
'PrintLine',
|
|
170
196
|
'WriteLine',
|
|
171
197
|
'LoadStaticFiles',
|
|
198
|
+
'secure_filename',
|
|
199
|
+
'safe_join',
|
|
200
|
+
'sign_value',
|
|
201
|
+
'unsign_value',
|
|
202
|
+
'generate_csrf_token',
|
|
172
203
|
'ContentTypes',
|
|
173
204
|
'EventType',
|
|
174
205
|
'HTMLTag',
|
|
@@ -187,6 +218,7 @@ __all__ = [
|
|
|
187
218
|
'Int32Format',
|
|
188
219
|
'Int64Format',
|
|
189
220
|
'FloatFormat',
|
|
221
|
+
'DoubleFormat',
|
|
190
222
|
'DoubleFormnat',
|
|
191
223
|
'HTTPStatusCode',
|
|
192
224
|
'JWTAlgorithms',
|
|
@@ -198,6 +230,20 @@ __all__ = [
|
|
|
198
230
|
'InvalidTemplateError',
|
|
199
231
|
'RouteNotFoundError',
|
|
200
232
|
'RouterError',
|
|
233
|
+
'TestClient',
|
|
234
|
+
'warn_deprecated',
|
|
235
|
+
'OpenApiProcessor',
|
|
236
|
+
'OpenAPIConfig',
|
|
237
|
+
'OpenAPIBuilder',
|
|
238
|
+
'HTTPBearer',
|
|
239
|
+
'HTTPBasic',
|
|
240
|
+
'APIKeyHeader',
|
|
241
|
+
'APIKeyQuery',
|
|
242
|
+
'APIKeyCookie',
|
|
243
|
+
'AuthContext',
|
|
244
|
+
'SecurityEnforcer',
|
|
245
|
+
'SecurityError',
|
|
246
|
+
'ForbiddenError',
|
|
201
247
|
'Icon',
|
|
202
248
|
'Style',
|
|
203
249
|
'Script',
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
from pyweber.core.element import Element
|
|
2
|
-
from pyweber.components.input import Input
|
|
2
|
+
from pyweber.components.input import Input, InputHidden
|
|
3
3
|
from typing import Callable, Literal
|
|
4
4
|
|
|
5
|
+
from pyweber.utils.security import (
|
|
6
|
+
CSRF_FORM_FIELD,
|
|
7
|
+
csrf_enabled,
|
|
8
|
+
generate_csrf_token,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
5
12
|
class Form(Element):
|
|
6
13
|
def __init__(
|
|
7
14
|
self,
|
|
@@ -24,6 +31,7 @@ class Form(Element):
|
|
|
24
31
|
novalidate: bool = True,
|
|
25
32
|
autocapitalize: bool = False,
|
|
26
33
|
spellcheck: bool = False,
|
|
34
|
+
csrf_token: str = None,
|
|
27
35
|
**kwargs
|
|
28
36
|
):
|
|
29
37
|
super().__init__(tag='form', classes=classes, style=style, id=id, childs=childs, **kwargs)
|
|
@@ -43,16 +51,22 @@ class Form(Element):
|
|
|
43
51
|
self.onsubmit = onsubmit
|
|
44
52
|
self.onreset = onreset
|
|
45
53
|
self.attrs = {}
|
|
46
|
-
|
|
54
|
+
|
|
55
|
+
if csrf_enabled() and method and str(method).upper() == 'POST':
|
|
56
|
+
token = csrf_token or generate_csrf_token()
|
|
57
|
+
self.childs.append(
|
|
58
|
+
InputHidden(name=CSRF_FORM_FIELD, value=token, id=f'{id or "form"}_csrf')
|
|
59
|
+
)
|
|
60
|
+
|
|
47
61
|
@property
|
|
48
62
|
def onclick(self):
|
|
49
63
|
return self.events.__dict__.get('onclick', None)
|
|
50
|
-
|
|
64
|
+
|
|
51
65
|
@onclick.setter
|
|
52
66
|
def onclick(self, value: Callable):
|
|
53
67
|
if value and callable(value):
|
|
54
68
|
setattr(self.events, 'onclick', value)
|
|
55
|
-
|
|
69
|
+
|
|
56
70
|
@property
|
|
57
71
|
def onsubmit(self):
|
|
58
72
|
return self.events.__dict__.get('onsubmit', None)
|
|
@@ -61,7 +75,7 @@ class Form(Element):
|
|
|
61
75
|
def onsubmit(self, value: Callable):
|
|
62
76
|
if value and callable(value):
|
|
63
77
|
setattr(self.events, 'onsubmit', value)
|
|
64
|
-
|
|
78
|
+
|
|
65
79
|
@property
|
|
66
80
|
def onreset(self):
|
|
67
81
|
return self.events.__dict__.get('onreset', None)
|
|
@@ -70,16 +84,16 @@ class Form(Element):
|
|
|
70
84
|
def onreset(self, value):
|
|
71
85
|
if value and callable(value):
|
|
72
86
|
setattr(self.events, 'onreset', value)
|
|
73
|
-
|
|
87
|
+
|
|
74
88
|
@property
|
|
75
89
|
def attrs(self):
|
|
76
90
|
return self.__attrs
|
|
77
|
-
|
|
91
|
+
|
|
78
92
|
@attrs.setter
|
|
79
93
|
def attrs(self, value: dict[str, str]):
|
|
80
94
|
if value:
|
|
81
95
|
raise AttributeError('Subscript not allowed to this attribute')
|
|
82
|
-
|
|
96
|
+
|
|
83
97
|
self.__attrs = {}
|
|
84
98
|
for key, value in self.__dict__.items():
|
|
85
99
|
if key in ['method', 'name', 'action', 'autocomplete', 'spellcheck', 'autocapitalize', 'novalidate', 'target', 'enctype', 'accept_charset', 'rel', 'tabindex'] and value:
|
|
@@ -89,4 +103,4 @@ class Form(Element):
|
|
|
89
103
|
if key == 'accept_charset':
|
|
90
104
|
self.__attrs['accept-charset'] = value
|
|
91
105
|
else:
|
|
92
|
-
self.__attrs[key] = value
|
|
106
|
+
self.__attrs[key] = value
|
|
@@ -95,7 +95,7 @@ class TextArea(Element):
|
|
|
95
95
|
oninput: Callable = None,
|
|
96
96
|
onclick: Callable = None,
|
|
97
97
|
onselect: Callable = None,
|
|
98
|
-
sanitize: bool =
|
|
98
|
+
sanitize: bool = True
|
|
99
99
|
):
|
|
100
100
|
super().__init__(tag='textarea')
|
|
101
101
|
self.sanitize = sanitize
|
|
@@ -200,4 +200,4 @@ class TextArea(Element):
|
|
|
200
200
|
elif key == 'spellcheck':
|
|
201
201
|
self.__attrs[key] = 'true' if value else 'false'
|
|
202
202
|
else:
|
|
203
|
-
self.__attrs[key] = str(value)
|
|
203
|
+
self.__attrs[key] = str(value)
|
|
@@ -120,7 +120,7 @@ class InputColor(Input):
|
|
|
120
120
|
onchange = None,
|
|
121
121
|
oninput = None,
|
|
122
122
|
onclick = None,
|
|
123
|
-
sanitize: bool =
|
|
123
|
+
sanitize: bool = True
|
|
124
124
|
):
|
|
125
125
|
super().__init__('color', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
126
126
|
self.sanitize = sanitize
|
|
@@ -151,7 +151,7 @@ class InputText(Input):
|
|
|
151
151
|
onchange = None,
|
|
152
152
|
oninput = None,
|
|
153
153
|
onclick = None,
|
|
154
|
-
sanitize: bool =
|
|
154
|
+
sanitize: bool = True
|
|
155
155
|
):
|
|
156
156
|
super().__init__('text', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
157
157
|
self.sanitize = sanitize
|
|
@@ -200,7 +200,7 @@ class InputPassword(Input):
|
|
|
200
200
|
onchange: Callable = None,
|
|
201
201
|
oninput: Callable = None,
|
|
202
202
|
onclick: Callable = None,
|
|
203
|
-
sanitize: bool =
|
|
203
|
+
sanitize: bool = True
|
|
204
204
|
):
|
|
205
205
|
super().__init__('password', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
206
206
|
self.sanitize = sanitize
|
|
@@ -259,7 +259,7 @@ class InputEmail(Input):
|
|
|
259
259
|
onchange: Callable = None,
|
|
260
260
|
oninput: Callable = None,
|
|
261
261
|
onclick: Callable = None,
|
|
262
|
-
sanitize: bool =
|
|
262
|
+
sanitize: bool = True
|
|
263
263
|
):
|
|
264
264
|
super().__init__('email', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
265
265
|
self.sanitize = sanitize
|
|
@@ -307,7 +307,7 @@ class InputNumber(Input):
|
|
|
307
307
|
onchange: Callable = None,
|
|
308
308
|
oninput: Callable = None,
|
|
309
309
|
onclick: Callable = None,
|
|
310
|
-
sanitize: bool =
|
|
310
|
+
sanitize: bool = True
|
|
311
311
|
):
|
|
312
312
|
super().__init__('number', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
313
313
|
self.sanitize = sanitize
|
|
@@ -581,7 +581,7 @@ class InputSearch(Input):
|
|
|
581
581
|
onchange: Callable = None,
|
|
582
582
|
oninput: Callable = None,
|
|
583
583
|
onclick: Callable = None,
|
|
584
|
-
sanitize: bool =
|
|
584
|
+
sanitize: bool = True
|
|
585
585
|
):
|
|
586
586
|
super().__init__('search', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
587
587
|
self.sanitize = sanitize
|
|
@@ -630,7 +630,7 @@ class InputTel(Input):
|
|
|
630
630
|
onchange: Callable = None,
|
|
631
631
|
oninput: Callable = None,
|
|
632
632
|
onclick: Callable = None,
|
|
633
|
-
sanitize: bool =
|
|
633
|
+
sanitize: bool = True
|
|
634
634
|
):
|
|
635
635
|
super().__init__('tel', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
636
636
|
self.sanitize = sanitize
|
|
@@ -679,7 +679,7 @@ class InputUrl(Input):
|
|
|
679
679
|
onchange: Callable = None,
|
|
680
680
|
oninput: Callable = None,
|
|
681
681
|
onclick: Callable = None,
|
|
682
|
-
sanitize: bool =
|
|
682
|
+
sanitize: bool = True
|
|
683
683
|
):
|
|
684
684
|
super().__init__('url', name, id, form, tabindex, classes, style, disabled, autofocus, required, onfocus, onblur, onchange, oninput, onclick,)
|
|
685
685
|
self.sanitize = sanitize
|
|
@@ -717,7 +717,7 @@ class InputHidden(Input):
|
|
|
717
717
|
onchange: Callable = None,
|
|
718
718
|
oninput: Callable = None,
|
|
719
719
|
onclick: Callable = None,
|
|
720
|
-
sanitize: bool =
|
|
720
|
+
sanitize: bool = True
|
|
721
721
|
):
|
|
722
722
|
super().__init__('hidden', name, id, form, None, classes, style, None, None, None, onfocus, onblur, onchange, oninput, onclick,)
|
|
723
723
|
self.sanitize = sanitize
|
|
@@ -745,7 +745,7 @@ class InputSubmit(Input):
|
|
|
745
745
|
onchange: Callable = None,
|
|
746
746
|
oninput: Callable = None,
|
|
747
747
|
onclick: Callable = None,
|
|
748
|
-
sanitize: bool =
|
|
748
|
+
sanitize: bool = True
|
|
749
749
|
):
|
|
750
750
|
super().__init__('submit', name, id, form, tabindex, classes, style, disabled, autofocus, None, onfocus, onblur, onchange, oninput, onclick,)
|
|
751
751
|
self.sanitize = sanitize
|
|
@@ -783,7 +783,7 @@ class InputButton(Input):
|
|
|
783
783
|
onchange: Callable = None,
|
|
784
784
|
oninput: Callable = None,
|
|
785
785
|
onclick: Callable = None,
|
|
786
|
-
sanitize: bool =
|
|
786
|
+
sanitize: bool = True
|
|
787
787
|
):
|
|
788
788
|
super().__init__('button', name, id, form, tabindex, classes, style, disabled, autofocus, None, onfocus, onblur, onchange, oninput, onclick,)
|
|
789
789
|
self.sanitize = sanitize
|
|
@@ -807,8 +807,8 @@ class InputReset(Input):
|
|
|
807
807
|
onchange: Callable = None,
|
|
808
808
|
oninput: Callable = None,
|
|
809
809
|
onclick: Callable = None,
|
|
810
|
-
sanitize: bool =
|
|
810
|
+
sanitize: bool = True
|
|
811
811
|
):
|
|
812
812
|
super().__init__('reset', name, id, form, tabindex, classes, style, disabled, autofocus, None, onfocus, onblur, onchange, oninput, onclick,)
|
|
813
813
|
self.sanitize = sanitize
|
|
814
|
-
self.value = value
|
|
814
|
+
self.value = value
|
|
@@ -13,9 +13,11 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
13
13
|
from pyweber.pyweber.pyweber import Pyweber
|
|
14
14
|
from pyweber.utils.async_utils import async_timeout
|
|
15
15
|
from pyweber.utils.utils import Colors, PrintLine
|
|
16
|
+
from pyweber.utils.security import get_max_body_size
|
|
16
17
|
from pyweber.models.request import Request, ClientInfo
|
|
17
18
|
from pyweber.connection.websocket import WebsocketUpgrade, WebsocketServer
|
|
18
19
|
from pyweber.connection.selector import IOSelector
|
|
20
|
+
from pyweber.utils.types import ContentTypes, HTTPStatusCode
|
|
19
21
|
|
|
20
22
|
class HttpServer:
|
|
21
23
|
def __init__(self, *args, **kwargs):
|
|
@@ -48,6 +50,28 @@ class HttpServer:
|
|
|
48
50
|
loop = asyncio.get_event_loop()
|
|
49
51
|
return await loop.run_in_executor(None, client.recv, length)
|
|
50
52
|
|
|
53
|
+
def _parse_cookies(self, header_text: str) -> dict[str, str]:
|
|
54
|
+
cookies = {}
|
|
55
|
+
match = re.search(r'(?im)^Cookie:\s*(.+)$', header_text)
|
|
56
|
+
if not match:
|
|
57
|
+
return cookies
|
|
58
|
+
for part in match.group(1).split(';'):
|
|
59
|
+
if '=' not in part:
|
|
60
|
+
continue
|
|
61
|
+
name, value = part.split('=', 1)
|
|
62
|
+
cookies[name.strip()] = value.strip()
|
|
63
|
+
return cookies
|
|
64
|
+
|
|
65
|
+
async def _send_simple_error(self, client, code: int, body: bytes, content_type: str = 'text/plain'):
|
|
66
|
+
reason = HTTPStatusCode.search_by_code(code) if hasattr(HTTPStatusCode, 'search_by_code') else f'{code}'
|
|
67
|
+
response = (
|
|
68
|
+
f'HTTP/1.1 {reason}\r\n'
|
|
69
|
+
f'Content-Type: {content_type}; charset=UTF-8\r\n'
|
|
70
|
+
f'Content-Length: {len(body)}\r\n'
|
|
71
|
+
'Connection: close\r\n\r\n'
|
|
72
|
+
).encode() + body
|
|
73
|
+
await self.send_data(client, response)
|
|
74
|
+
|
|
51
75
|
async def process_request(self, client: Union[socket.socket, ssl.SSLSocket]):
|
|
52
76
|
try:
|
|
53
77
|
request_data = bytearray()
|
|
@@ -65,14 +89,30 @@ class HttpServer:
|
|
|
65
89
|
if content_match:
|
|
66
90
|
content_length = int(content_match.group(1))
|
|
67
91
|
|
|
92
|
+
max_body = get_max_body_size()
|
|
93
|
+
if content_length > max_body:
|
|
94
|
+
await self._send_simple_error(
|
|
95
|
+
client,
|
|
96
|
+
413,
|
|
97
|
+
b'Payload Too Large',
|
|
98
|
+
)
|
|
99
|
+
return None, None
|
|
100
|
+
|
|
68
101
|
body = bytearray(body_start)
|
|
102
|
+
if len(body) > max_body:
|
|
103
|
+
await self._send_simple_error(client, 413, b'Payload Too Large')
|
|
104
|
+
return None, None
|
|
105
|
+
|
|
69
106
|
async with async_timeout(self.timeout):
|
|
70
107
|
while len(body) < content_length:
|
|
71
|
-
remaining = content_length - len(body)
|
|
108
|
+
remaining = min(content_length - len(body), max_body - len(body) + 1)
|
|
72
109
|
chunk = await self.read_data(client, remaining)
|
|
73
110
|
if not chunk:
|
|
74
111
|
break
|
|
75
112
|
body.extend(chunk)
|
|
113
|
+
if len(body) > max_body:
|
|
114
|
+
await self._send_simple_error(client, 413, b'Payload Too Large')
|
|
115
|
+
return None, None
|
|
76
116
|
|
|
77
117
|
return header_bytes, bytes(body)
|
|
78
118
|
|
|
@@ -85,7 +125,7 @@ class HttpServer:
|
|
|
85
125
|
try:
|
|
86
126
|
headers, body = await self.process_request(client)
|
|
87
127
|
|
|
88
|
-
if not headers:
|
|
128
|
+
if headers is None or not headers:
|
|
89
129
|
return
|
|
90
130
|
|
|
91
131
|
client_details = client.getpeername()
|
|
@@ -115,7 +155,7 @@ class HttpServer:
|
|
|
115
155
|
try:
|
|
116
156
|
headers, body = await self.process_request(client)
|
|
117
157
|
|
|
118
|
-
if not headers:
|
|
158
|
+
if headers is None or not headers:
|
|
119
159
|
return
|
|
120
160
|
|
|
121
161
|
request = Request(
|
|
@@ -127,7 +167,7 @@ class HttpServer:
|
|
|
127
167
|
upgrade = WebsocketUpgrade(headers=headers)
|
|
128
168
|
await self.send_data(client, upgrade.upgrade_response.encode('utf-8'))
|
|
129
169
|
|
|
130
|
-
ws_connection = WebsocketServer(client)
|
|
170
|
+
ws_connection = WebsocketServer(client, cookies=dict(request.cookies))
|
|
131
171
|
await self.app.ws_server.connect_wsgi(ws_connection=ws_connection)
|
|
132
172
|
|
|
133
173
|
except TypeError:
|
|
@@ -186,15 +226,24 @@ class HttpServer:
|
|
|
186
226
|
if match:
|
|
187
227
|
content_length = int(match.group(1))
|
|
188
228
|
|
|
229
|
+
max_body = get_max_body_size()
|
|
230
|
+
if content_length > max_body:
|
|
231
|
+
await self._send_simple_error(client, 413, b'Payload Too Large')
|
|
232
|
+
return
|
|
233
|
+
|
|
189
234
|
body = bytearray(body_start)
|
|
190
235
|
async with async_timeout(self.timeout):
|
|
191
236
|
while len(body) < content_length:
|
|
237
|
+
remaining = min(content_length - len(body), max_body - len(body) + 1)
|
|
192
238
|
chunk = await asyncio.get_event_loop().run_in_executor(
|
|
193
|
-
None, client.recv,
|
|
239
|
+
None, client.recv, remaining
|
|
194
240
|
)
|
|
195
241
|
if not chunk:
|
|
196
242
|
break
|
|
197
243
|
body.extend(chunk)
|
|
244
|
+
if len(body) > max_body:
|
|
245
|
+
await self._send_simple_error(client, 413, b'Payload Too Large')
|
|
246
|
+
return
|
|
198
247
|
|
|
199
248
|
client_details = client.getpeername()
|
|
200
249
|
client_info = ClientInfo(
|
|
@@ -222,11 +271,13 @@ class HttpServer:
|
|
|
222
271
|
"""WebSocket com headers já lidos."""
|
|
223
272
|
try:
|
|
224
273
|
header_bytes = raw.partition(b'\r\n\r\n')[0]
|
|
274
|
+
header_text = header_bytes.decode('iso-8859-1')
|
|
275
|
+
cookies = self._parse_cookies(header_text)
|
|
225
276
|
|
|
226
277
|
upgrade = WebsocketUpgrade(headers=header_bytes)
|
|
227
278
|
client.sendall(upgrade.upgrade_response.encode('utf-8'))
|
|
228
279
|
|
|
229
|
-
ws_connection = WebsocketServer(client)
|
|
280
|
+
ws_connection = WebsocketServer(client, cookies=cookies)
|
|
230
281
|
await self.app.ws_server.connect_wsgi(ws_connection=ws_connection)
|
|
231
282
|
|
|
232
283
|
except TypeError:
|
|
@@ -12,6 +12,11 @@ from time import time
|
|
|
12
12
|
|
|
13
13
|
from pyweber.models.ws_message import wsMessage
|
|
14
14
|
from pyweber.utils.utils import PrintLine
|
|
15
|
+
from pyweber.utils.security import (
|
|
16
|
+
SESSION_COOKIE_NAME,
|
|
17
|
+
generate_session_id,
|
|
18
|
+
unsign_value,
|
|
19
|
+
)
|
|
15
20
|
from pyweber.connection.session import sessions, Session
|
|
16
21
|
from pyweber.models.template_diff import TemplateDiff
|
|
17
22
|
from pyweber.models.task_manager import TaskManager
|
|
@@ -85,9 +90,10 @@ class WebsocketUpgrade:
|
|
|
85
90
|
|
|
86
91
|
class WebsocketServer:
|
|
87
92
|
|
|
88
|
-
def __init__(self, client: Union[socket.socket, ssl.SSLSocket]):
|
|
93
|
+
def __init__(self, client: Union[socket.socket, ssl.SSLSocket], cookies: dict[str, str] | None = None):
|
|
89
94
|
self.id = None
|
|
90
95
|
self.client = client
|
|
96
|
+
self.cookies = cookies or {}
|
|
91
97
|
self.__all_message: bytes = b''
|
|
92
98
|
self.__messages: list[bytes] = []
|
|
93
99
|
|
|
@@ -491,8 +497,15 @@ class WebsocketManager(BaseWebsockets):
|
|
|
491
497
|
group, route = self.app.get_group_and_route(route=route)
|
|
492
498
|
self.app.update_route(route=route, group=group, template=new_template)
|
|
493
499
|
|
|
494
|
-
def get_session_id(self, session_id: str = None):
|
|
495
|
-
|
|
500
|
+
def get_session_id(self, session_id: str = None, cookies: dict[str, str] | None = None):
|
|
501
|
+
"""Resolve session id from signed HttpOnly cookie; never trust client id alone."""
|
|
502
|
+
cookie_jar = cookies or {}
|
|
503
|
+
signed = cookie_jar.get(SESSION_COOKIE_NAME)
|
|
504
|
+
cookie_sid = unsign_value(signed) if signed else None
|
|
505
|
+
if cookie_sid:
|
|
506
|
+
return cookie_sid
|
|
507
|
+
# Ignore client-supplied session_id when cookie is missing/invalid
|
|
508
|
+
return generate_session_id()
|
|
496
509
|
|
|
497
510
|
async def get_sync_template(self, message: wsMessage):
|
|
498
511
|
assert isinstance(message, wsMessage)
|
|
@@ -502,11 +515,6 @@ class WebsocketManager(BaseWebsockets):
|
|
|
502
515
|
session.old_template = sync_template.clone()
|
|
503
516
|
return sync_template
|
|
504
517
|
|
|
505
|
-
def update_app_template(self, new_template: 'Template', route: str):
|
|
506
|
-
if route in self.app.list_routes:
|
|
507
|
-
group, route = self.app.get_group_and_route(route=route)
|
|
508
|
-
self.app.update_route(route=route, group=group, template=new_template)
|
|
509
|
-
|
|
510
518
|
def add_session(self, session_id: str, template: 'Template', window: 'Window', route: str):
|
|
511
519
|
sessions.add_session(
|
|
512
520
|
session_id=session_id,
|
|
@@ -593,7 +601,10 @@ class WebsocketManager(BaseWebsockets):
|
|
|
593
601
|
)
|
|
594
602
|
|
|
595
603
|
if is_new_session:
|
|
596
|
-
ws_server.id = self.get_session_id(
|
|
604
|
+
ws_server.id = self.get_session_id(
|
|
605
|
+
session_id=message.session_id,
|
|
606
|
+
cookies=getattr(ws_server, 'cookies', {}) or {},
|
|
607
|
+
)
|
|
597
608
|
self.ws_connections[ws_server.id] = ws_server
|
|
598
609
|
|
|
599
610
|
self.add_session(
|
|
@@ -633,8 +644,9 @@ class WebsocketManager(BaseWebsockets):
|
|
|
633
644
|
)
|
|
634
645
|
await self.message_handler(message=message)
|
|
635
646
|
|
|
636
|
-
async def ws_handler_asgi(self, receive: Callable, send: Callable):
|
|
647
|
+
async def ws_handler_asgi(self, receive: Callable, send: Callable, cookies: dict[str, str] | None = None):
|
|
637
648
|
ws_connection: str = None
|
|
649
|
+
handshake_cookies = cookies or {}
|
|
638
650
|
|
|
639
651
|
try:
|
|
640
652
|
while True:
|
|
@@ -665,7 +677,10 @@ class WebsocketManager(BaseWebsockets):
|
|
|
665
677
|
)
|
|
666
678
|
|
|
667
679
|
if is_new_session:
|
|
668
|
-
ws_connection = self.get_session_id(
|
|
680
|
+
ws_connection = self.get_session_id(
|
|
681
|
+
session_id=message.session_id,
|
|
682
|
+
cookies=handshake_cookies,
|
|
683
|
+
)
|
|
669
684
|
|
|
670
685
|
self.add_session(
|
|
671
686
|
session_id=ws_connection,
|
|
@@ -720,7 +735,17 @@ class WebsocketManager(BaseWebsockets):
|
|
|
720
735
|
|
|
721
736
|
async def __call__(self, scope, receive, send):
|
|
722
737
|
assert scope.get('type', None) == 'websocket'
|
|
723
|
-
|
|
738
|
+
headers = {
|
|
739
|
+
(k.decode() if isinstance(k, bytes) else k).lower():
|
|
740
|
+
(v.decode() if isinstance(v, bytes) else v)
|
|
741
|
+
for k, v in scope.get('headers', [])
|
|
742
|
+
}
|
|
743
|
+
cookie_header = headers.get('cookie', '')
|
|
744
|
+
cookies = {
|
|
745
|
+
part.split('=', 1)[0].strip(): part.split('=', 1)[-1].strip()
|
|
746
|
+
for part in cookie_header.split(';') if part.strip() and '=' in part
|
|
747
|
+
}
|
|
748
|
+
await self.ws_handler_asgi(receive=receive, send=send, cookies=cookies)
|
|
724
749
|
|
|
725
750
|
for id, handler in self.ws_connections.items():
|
|
726
751
|
if handler == send:
|
|
@@ -6,6 +6,11 @@ from .events import (
|
|
|
6
6
|
WindowEvents,
|
|
7
7
|
TemplateEvents
|
|
8
8
|
)
|
|
9
|
+
from .html_parser import (
|
|
10
|
+
get_html_parser_backend,
|
|
11
|
+
set_html_parser_backend,
|
|
12
|
+
reset_html_parser_backend,
|
|
13
|
+
)
|
|
9
14
|
|
|
10
15
|
__all__ = [
|
|
11
16
|
'Element',
|
|
@@ -13,5 +18,8 @@ __all__ = [
|
|
|
13
18
|
'window',
|
|
14
19
|
'EventHandler',
|
|
15
20
|
'WindowEvents',
|
|
16
|
-
'TemplateEvents'
|
|
21
|
+
'TemplateEvents',
|
|
22
|
+
'get_html_parser_backend',
|
|
23
|
+
'set_html_parser_backend',
|
|
24
|
+
'reset_html_parser_backend',
|
|
17
25
|
]
|