tina4-python 0.2.194__tar.gz → 0.2.197__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.
Files changed (66) hide show
  1. {tina4_python-0.2.194 → tina4_python-0.2.197}/PKG-INFO +1 -1
  2. {tina4_python-0.2.194 → tina4_python-0.2.197}/pyproject.toml +2 -1
  3. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Response.py +10 -3
  4. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Router.py +7 -4
  5. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Session.py +7 -2
  6. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Webserver.py +34 -10
  7. {tina4_python-0.2.194 → tina4_python-0.2.197}/.gitignore +0 -0
  8. {tina4_python-0.2.194 → tina4_python-0.2.197}/README.md +0 -0
  9. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Api.py +0 -0
  10. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Auth.py +0 -0
  11. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/CLAUDE.md +0 -0
  12. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/CRUD.py +0 -0
  13. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Constant.py +0 -0
  14. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Database.py +0 -0
  15. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/DatabaseResult.py +0 -0
  16. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/DatabaseTypes.py +0 -0
  17. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Debug.py +0 -0
  18. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/DevReload.py +0 -0
  19. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Env.py +0 -0
  20. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/FieldTypes.py +0 -0
  21. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/HtmlElement.py +0 -0
  22. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Localization.py +0 -0
  23. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Messages.py +0 -0
  24. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/MiddleWare.py +0 -0
  25. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Migration.py +0 -0
  26. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/ORM.py +0 -0
  27. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Queue.py +0 -0
  28. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Request.py +0 -0
  29. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/ShellColors.py +0 -0
  30. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Swagger.py +0 -0
  31. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Template.py +0 -0
  32. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Testing.py +0 -0
  33. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/WSDL.py +0 -0
  34. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/Websocket.py +0 -0
  35. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/__init__.py +0 -0
  36. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/cli.py +0 -0
  37. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/messages.pot +0 -0
  38. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/css/readme.md +0 -0
  39. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/favicon.ico +0 -0
  40. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/images/403.png +0 -0
  41. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/images/404.png +0 -0
  42. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/images/500.png +0 -0
  43. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/images/logo.png +0 -0
  44. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/images/readme.md +0 -0
  45. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/js/readme.md +0 -0
  46. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  47. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/js/tina4helper.js +0 -0
  48. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/swagger/index.html +0 -0
  49. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  50. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/templates/components/crud.twig +0 -0
  51. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/templates/errors/403.twig +0 -0
  52. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/templates/errors/404.twig +0 -0
  53. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/templates/errors/500.twig +0 -0
  54. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/templates/readme.md +0 -0
  55. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  56. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  57. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  58. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  59. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  60. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  61. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  62. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  63. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  64. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  65. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  66. {tina4_python-0.2.194 → tina4_python-0.2.197}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 0.2.194
3
+ Version: 0.2.197
4
4
  Summary: Tina4Python - This is not another framework for Python
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  Requires-Python: <4.0,>=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "0.2.194"
3
+ version = "0.2.197"
4
4
  description = "Tina4Python - This is not another framework for Python"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
@@ -32,6 +32,7 @@ dev = [
32
32
  "pydoc-markdown>=4.8.2",
33
33
  "pytest>=8.3.5",
34
34
  "pytest-asyncio>=1.3.0",
35
+ "pytest-cov>=7.0.0",
35
36
  "python-keycloak>=5.8.1",
36
37
  "ruff>=0.11.9",
37
38
  "safety>=3.5.0",
@@ -124,7 +124,9 @@ class Response:
124
124
  :param redirect_url:
125
125
  :return:
126
126
  """
127
- headers = {"Location": redirect_url}
127
+ # Strip CR/LF to prevent HTTP response splitting (header injection)
128
+ safe_url = redirect_url.replace("\r", "").replace("\n", "")
129
+ headers = {"Location": safe_url}
128
130
  from tina4_python import Messages
129
131
  return Response(Messages.MSG_REDIRECTING, http_code_in, Constant.TEXT_HTML, headers)
130
132
 
@@ -193,16 +195,21 @@ class Response:
193
195
  @staticmethod
194
196
  def add_header(key, value):
195
197
  """
196
- Adds a header for the response (concurrency-safe via contextvars)
198
+ Adds a header for the response (concurrency-safe via contextvars).
199
+ CR/LF characters are stripped from both key and value to prevent
200
+ HTTP response splitting.
197
201
  :param key:
198
202
  :param value:
199
203
  :return:
200
204
  """
205
+ # Sanitise to prevent header injection
206
+ safe_key = str(key).replace("\r", "").replace("\n", "")
207
+ safe_value = str(value).replace("\r", "").replace("\n", "")
201
208
  h = _pending_headers.get()
202
209
  if h is None:
203
210
  h = {}
204
211
  _pending_headers.set(h)
205
- h[key] = value
212
+ h[safe_key] = safe_value
206
213
 
207
214
  @staticmethod
208
215
  def wsdl(wsdl_instance):
@@ -373,6 +373,7 @@ class Router:
373
373
  token = headers["authorization"].replace("Bearer", "").strip()
374
374
  if tina4_python.tina4_auth.valid(token):
375
375
  validated = True
376
+ has_form_token = True
376
377
 
377
378
  if request["params"] is not None and "formToken" in request["params"]:
378
379
  token = request["params"]["formToken"]
@@ -528,15 +529,17 @@ class Router:
528
529
  result.headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
529
530
  if "cache" in route and route["cache"] is not None:
530
531
  if not route["cache"]["cached"]:
531
- result.headers["Cache-Control"] = "max-age=1, must-revalidate"
532
+ result.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
532
533
  result.headers["Pragma"] = "no-cache"
533
534
  else:
534
535
  result.headers["Cache-Control"] = "max-age=" + str(
535
536
  route["cache"]["max_age"]) + ", must-revalidate"
536
537
  result.headers["Pragma"] = "cache"
537
538
  else:
538
- result.headers["Cache-Control"] = "max-age=-1, must-revalidate"
539
- result.headers["Pragma"] = "cache"
539
+ # Default: don't cache — prevents sensitive data leaking
540
+ # via shared proxies or browser disk cache
541
+ result.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
542
+ result.headers["Pragma"] = "no-cache"
540
543
  finally:
541
544
  sys.stdout = old_stdout
542
545
 
@@ -572,7 +575,7 @@ class Router:
572
575
  )
573
576
 
574
577
  twig_headers = {
575
- "Cache-Control": "max-age=-1, public",
578
+ "Cache-Control": "no-store, no-cache, must-revalidate",
576
579
  "Pragma": "no-cache"
577
580
  }
578
581
  if has_form_token:
@@ -745,10 +745,15 @@ class LazySession:
745
745
  self._activate().session_hash = value
746
746
 
747
747
  def start(self, session_hash=None):
748
- return self._activate().start(session_hash)
748
+ result = self._activate().start(session_hash)
749
+ # Keep cookie dict in sync with the (possibly new) session hash
750
+ self._cookies[self._name] = result
751
+ return result
749
752
 
750
753
  def load(self, session_hash):
751
- return self._activate().load(session_hash)
754
+ self._activate().load(session_hash)
755
+ # Keep cookie dict in sync with loaded session hash
756
+ self._cookies[self._name] = self._real.session_hash
752
757
 
753
758
  def set(self, key, value):
754
759
  return self._activate().set(key, value)
@@ -239,22 +239,33 @@ class Webserver:
239
239
 
240
240
  async def send_basic_headers(self, headers_list: list):
241
241
  """
242
- Add permissive CORS and keep-alive headers (used for most responses).
242
+ Add CORS, keep-alive, and security headers (used for most responses).
243
243
 
244
244
  Args:
245
245
  headers_list (list): List to which headers are appended.
246
246
  """
247
- self.send_header("Access-Control-Allow-Origin", "*", headers_list)
247
+ # CORS — reflect the request Origin when credentials are needed,
248
+ # otherwise fall back to * for simple cross-origin requests.
249
+ origin = self.lowercase_headers.get("origin", "")
250
+ if origin:
251
+ self.send_header("Access-Control-Allow-Origin", origin, headers_list)
252
+ self.send_header("Access-Control-Allow-Credentials", "true", headers_list)
253
+ else:
254
+ self.send_header("Access-Control-Allow-Origin", "*", headers_list)
255
+
248
256
  self.send_header(
249
257
  "Access-Control-Allow-Headers",
250
258
  "Origin, X-Requested-With, Content-Type, Accept, Authorization",
251
259
  headers_list,
252
260
  )
253
- self.send_header("Access-Control-Allow-Credentials", "true", headers_list)
254
261
  self.send_header("Connection", "Keep-Alive", headers_list)
255
262
  self.send_header("Keep-Alive", "timeout=5, max=30", headers_list)
256
263
  self.send_header("Timing-Allow-Origin", "*", headers_list)
257
264
 
265
+ # Security headers
266
+ self.send_header("X-Content-Type-Options", "nosniff", headers_list)
267
+ self.send_header("X-Frame-Options", "SAMEORIGIN", headers_list)
268
+
258
269
  @staticmethod
259
270
  async def get_headers(header_lines: list, protocol: str, status_code: int) -> bytes:
260
271
  """
@@ -307,14 +318,19 @@ class Webserver:
307
318
  # ------------------------------------------------------------------
308
319
  if method == "OPTIONS":
309
320
  headers = []
310
- self.send_header("Access-Control-Allow-Origin", "*", headers)
321
+ # Reflect Origin for credentialed preflight, otherwise wildcard
322
+ origin = self.lowercase_headers.get("origin", "")
323
+ if origin:
324
+ self.send_header("Access-Control-Allow-Origin", origin, headers)
325
+ self.send_header("Access-Control-Allow-Credentials", "true", headers)
326
+ else:
327
+ self.send_header("Access-Control-Allow-Origin", "*", headers)
311
328
  self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS", headers)
312
329
  self.send_header(
313
330
  "Access-Control-Allow-Headers",
314
331
  "Origin, X-Requested-With, Content-Type, Accept, Authorization",
315
332
  headers,
316
333
  )
317
- self.send_header("Access-Control-Allow-Credentials", "true", headers)
318
334
 
319
335
  headers_bytes = await self.get_headers(headers, self.response_protocol, HTTP_OK)
320
336
  class _Tina4Response:
@@ -434,11 +450,19 @@ class Webserver:
434
450
  self.send_header("Content-Type", response.content_type or "text/html", headers)
435
451
  await self.send_basic_headers(headers)
436
452
 
437
- # Preserve session cookie (only if session was used)
438
- session_name = os.getenv("TINA4_SESSION", "PY_SESS")
439
- session_activated = not hasattr(self.session, 'activated') or self.session.activated
440
- if session_activated and session_name in self.cookies:
441
- self.send_header("Set-Cookie", f"{session_name}={self.cookies[session_name]}", headers)
453
+ # Preserve session cookie (only if session was used) — must be sent
454
+ # even on redirects so the browser keeps the session across the redirect
455
+ session_name = os.getenv("TINA4_SESSION", "PY_SESS")
456
+ session_activated = not hasattr(self.session, 'activated') or self.session.activated
457
+ if session_activated and session_name in self.cookies:
458
+ # Add Secure flag when behind TLS (reverse proxy or direct HTTPS)
459
+ proto = self.lowercase_headers.get("x-forwarded-proto", "http")
460
+ secure_flag = "; Secure" if proto == "https" else ""
461
+ self.send_header(
462
+ "Set-Cookie",
463
+ f"{session_name}={self.cookies[session_name]}; Path=/; HttpOnly; SameSite=Lax{secure_flag}",
464
+ headers,
465
+ )
442
466
 
443
467
  # Custom headers from route
444
468
  for name, value in response.headers.items():
File without changes