|
42 | 42 | from typing import Iterable, List, Mapping, Tuple, Union |
43 | 43 |
|
44 | 44 | from radicale import config, httputils, log, pathutils, types, utils |
| 45 | +from radicale.app import base as app_base |
45 | 46 | from radicale.app.base import ApplicationBase |
46 | 47 | from radicale.app.delete import ApplicationPartDelete |
47 | 48 | from radicale.app.get import ApplicationPartGet |
@@ -86,6 +87,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead, |
86 | 87 | _profiling_per_request: bool = False |
87 | 88 | _profiling_per_request_method: bool = False |
88 | 89 | _limit_content: int |
| 90 | + _validate_user_value: str |
| 91 | + _validate_path_value: str |
89 | 92 | profiler_per_request_method: dict[str, cProfile.Profile] = {} |
90 | 93 | profiler_per_request_method_counter: dict[str, int] = {} |
91 | 94 | profiler_per_request_method_starttime: datetime.datetime |
@@ -158,6 +161,19 @@ def __init__(self, configuration: config.Configuration) -> None: |
158 | 161 | self._extra_headers[key] = configuration.get("headers", key) |
159 | 162 | self._strict_preconditions = configuration.get("storage", "strict_preconditions") |
160 | 163 | logger.info("strict preconditions check: %s", self._strict_preconditions) |
| 164 | + # Format checks |
| 165 | + self._validate_user_value = configuration.get("server", "validate_user_value") |
| 166 | + self._validate_path_value = configuration.get("server", "validate_path_value") |
| 167 | + if not self._storage._supports_unicode: |
| 168 | + if self._validate_user_value not in ["strict", "no-unicode"]: |
| 169 | + self._validate_user_value = "no-unicode" |
| 170 | + if self._validate_path_value not in ["strict", "no-unicode"]: |
| 171 | + self._validate_path_value = "no-unicode" |
| 172 | + logger.notice("validate user value: %r (enforced by limited support of collection storage)", self._validate_user_value) |
| 173 | + logger.notice("validate path value: %r (enforced by limited support of collection storage)", self._validate_path_value) |
| 174 | + else: |
| 175 | + logger.info("validate user value: %r", self._validate_user_value) |
| 176 | + logger.info("validate path value: %r", self._validate_path_value) |
161 | 177 | # Profiling options |
162 | 178 | self._profiling = configuration.get("logging", "profiling") |
163 | 179 | self._profiling_per_request_min_duration = configuration.get("logging", "profiling_per_request_min_duration") |
@@ -338,15 +354,23 @@ def response(status: int, headers: types.WSGIResponseHeaders, |
338 | 354 | else: |
339 | 355 | flags_text = "" |
340 | 356 | if answer is not None: |
341 | | - logger.info("%s response status for %r%s in %.3f seconds %s %s bytes%s: %s", |
| 357 | + message = "%s response status for %r%s in %.3f seconds %s %s bytes%s: %s" % ( |
342 | 358 | request_method, unsafe_path, depthinfo, |
343 | 359 | time_delta_seconds, content_encoding, str(len(answer)), |
344 | 360 | flags_text, |
345 | 361 | status_text) |
346 | 362 | else: |
347 | | - logger.info("%s response status for %r%s in %.3f seconds: %s", |
| 363 | + message = "%s response status for %r%s in %.3f seconds: %s" % ( |
348 | 364 | request_method, unsafe_path, depthinfo, |
349 | 365 | time_delta_seconds, status_text) |
| 366 | + logger_method = logger.info # default |
| 367 | + if status == 401 or status == 404 or status == 412 or status == 409 or request_method in ["PROPPATCH", "MKCALENDAR", "MKCOL"]: |
| 368 | + logger_method = logger.notice |
| 369 | + elif status >= 500 and status < 500: |
| 370 | + logger_method = logger.error |
| 371 | + elif status >= 500: |
| 372 | + logger_method = logger.critical |
| 373 | + logger_method(message) |
350 | 374 |
|
351 | 375 | # Profiling end |
352 | 376 | if self._profiling_per_request: |
@@ -459,6 +483,9 @@ def response(status: int, headers: types.WSGIResponseHeaders, |
459 | 483 | logger.warning("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching (may cause authentication issues using internal WebUI)", base_prefix, path) |
460 | 484 | else: |
461 | 485 | logger.debug("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching", base_prefix, path) |
| 486 | + if not app_base._check_path_format(self._storage, path, self._validate_path_value): |
| 487 | + logger.error("request contains invalid path: %r (not compliant to %r)", path, self._validate_path_value) |
| 488 | + return response(*httputils.BAD_REQUEST) |
462 | 489 |
|
463 | 490 | # Get function corresponding to method |
464 | 491 | function = getattr(self, "do_%s" % request_method, None) |
@@ -489,7 +516,11 @@ def response(status: int, headers: types.WSGIResponseHeaders, |
489 | 516 | self.configuration, environ, base64.b64decode( |
490 | 517 | authorization.encode("ascii"))).split(":", 1) |
491 | 518 |
|
492 | | - (user, info) = self._auth.login(login, password, context) or ("", "") if login else ("", "") |
| 519 | + if login and not app_base._check_user_format(self._storage, login, self._validate_user_value): |
| 520 | + info = "not compliant to %r" % self._validate_user_value |
| 521 | + user = "" |
| 522 | + else: |
| 523 | + (user, info) = self._auth.login(login, password, context) or ("", "") if login else ("", "") |
493 | 524 | if self.configuration.get("auth", "type") == "ldap": |
494 | 525 | try: |
495 | 526 | logger.debug("Groups received from LDAP: %r", ",".join(self._auth._ldap_groups)) |
@@ -600,9 +631,9 @@ def response(status: int, headers: types.WSGIResponseHeaders, |
600 | 631 |
|
601 | 632 | if (status, headers, answer, xml_request) == httputils.NOT_ALLOWED: |
602 | 633 | if path.startswith("/.token"): |
603 | | - logger.info("Access to %r denied", path) |
| 634 | + logger.notice("Access to %r denied", path) |
604 | 635 | else: |
605 | | - logger.info("Access to %r denied for %s", path, repr(user) if user else "anonymous user") |
| 636 | + logger.notice("Access to %r denied for %s", path, repr(user) if user else "anonymous user") |
606 | 637 | else: |
607 | 638 | status, headers, answer, xml_request = httputils.NOT_ALLOWED |
608 | 639 |
|
|
0 commit comments