mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-01 18:18:31 +00:00
Merge branch 'master' into react0r
This commit is contained in:
commit
3cba4b32a3
23 changed files with 134 additions and 59 deletions
|
@ -61,7 +61,7 @@ def _get_application_instance(config_path: str, wsgi_errors: types.ErrorStream
|
|||
if not miss and source != "default config":
|
||||
default_config_active = False
|
||||
if default_config_active:
|
||||
logger.warn("%s", "No config file found/readable - only default config is active")
|
||||
logger.warning("%s", "No config file found/readable - only default config is active")
|
||||
_application_instance = Application(configuration)
|
||||
if _application_config_path != config_path:
|
||||
raise ValueError("RADICALE_CONFIG must not change: %r != %r" %
|
||||
|
|
|
@ -175,7 +175,7 @@ def run() -> None:
|
|||
default_config_active = False
|
||||
|
||||
if default_config_active:
|
||||
logger.warn("%s", "No config file found/readable - only default config is active")
|
||||
logger.warning("%s", "No config file found/readable - only default config is active")
|
||||
|
||||
if args_ns.verify_storage:
|
||||
logger.info("Verifying storage")
|
||||
|
@ -183,7 +183,7 @@ def run() -> None:
|
|||
storage_ = storage.load(configuration)
|
||||
with storage_.acquire_lock("r"):
|
||||
if not storage_.verify():
|
||||
logger.critical("Storage verifcation failed")
|
||||
logger.critical("Storage verification failed")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.critical("An exception occurred during storage "
|
||||
|
|
|
@ -232,7 +232,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||
path.rstrip("/").endswith("/.well-known/carddav")):
|
||||
return response(*httputils.redirect(
|
||||
base_prefix + "/", client.MOVED_PERMANENTLY))
|
||||
# Return NOT FOUND for all other paths containing ".well-knwon"
|
||||
# Return NOT FOUND for all other paths containing ".well-known"
|
||||
if path.endswith("/.well-known") or "/.well-known/" in path:
|
||||
return response(*httputils.NOT_FOUND)
|
||||
|
||||
|
|
|
@ -322,13 +322,13 @@ def xml_propfind_response(
|
|||
|
||||
responses[404 if is404 else 200].append(element)
|
||||
|
||||
for status_code, childs in responses.items():
|
||||
if not childs:
|
||||
for status_code, children in responses.items():
|
||||
if not children:
|
||||
continue
|
||||
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
|
||||
response.append(propstat)
|
||||
prop = ET.Element(xmlutils.make_clark("D:prop"))
|
||||
prop.extend(childs)
|
||||
prop.extend(children)
|
||||
propstat.append(prop)
|
||||
status = ET.Element(xmlutils.make_clark("D:status"))
|
||||
status.text = xmlutils.make_response(status_code)
|
||||
|
|
|
@ -363,7 +363,7 @@ def _make_vobject_expanded_item(
|
|||
if hasattr(item.vobject_item.vevent, 'rrule'):
|
||||
rruleset = vevent.getrruleset()
|
||||
|
||||
# There is something strage behavour during serialization native datetime, so converting manualy
|
||||
# There is something strange behaviour during serialization native datetime, so converting manually
|
||||
vevent.dtstart.value = vevent.dtstart.value.strftime(dt_format)
|
||||
if dt_end is not None:
|
||||
vevent.dtend.value = vevent.dtend.value.strftime(dt_format)
|
||||
|
|
|
@ -52,6 +52,7 @@ def load(configuration: "config.Configuration") -> "BaseAuth":
|
|||
class BaseAuth:
|
||||
|
||||
_lc_username: bool
|
||||
_strip_domain: bool
|
||||
|
||||
def __init__(self, configuration: "config.Configuration") -> None:
|
||||
"""Initialize BaseAuth.
|
||||
|
@ -63,6 +64,7 @@ class BaseAuth:
|
|||
"""
|
||||
self.configuration = configuration
|
||||
self._lc_username = configuration.get("auth", "lc_username")
|
||||
self._strip_domain = configuration.get("auth", "strip_domain")
|
||||
|
||||
def get_external_login(self, environ: types.WSGIEnviron) -> Union[
|
||||
Tuple[()], Tuple[str, str]]:
|
||||
|
@ -91,4 +93,8 @@ class BaseAuth:
|
|||
raise NotImplementedError
|
||||
|
||||
def login(self, login: str, password: str) -> str:
|
||||
return self._login(login, password).lower() if self._lc_username else self._login(login, password)
|
||||
if self._lc_username:
|
||||
login = login.lower()
|
||||
if self._strip_domain:
|
||||
login = login.split('@')[0]
|
||||
return self._login(login, password)
|
||||
|
|
|
@ -36,7 +36,7 @@ pointed to by the ``htpasswd_filename`` configuration value while assuming
|
|||
the password encryption method specified via the ``htpasswd_encryption``
|
||||
configuration value.
|
||||
|
||||
The following htpasswd password encrpytion methods are supported by Radicale
|
||||
The following htpasswd password encryption methods are supported by Radicale
|
||||
out-of-the-box:
|
||||
- plain-text (created by htpasswd -p ...) -- INSECURE
|
||||
- MD5-APR1 (htpasswd -m ...) -- htpasswd's default method, INSECURE
|
||||
|
|
|
@ -191,6 +191,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||
"value": "1",
|
||||
"help": "incorrect authentication delay",
|
||||
"type": positive_float}),
|
||||
("strip_domain", {
|
||||
"value": "False",
|
||||
"help": "strip domain from username",
|
||||
"type": bool}),
|
||||
("lc_username", {
|
||||
"value": "False",
|
||||
"help": "convert username to lowercase, must be true for case-insensitive auth providers",
|
||||
|
|
|
@ -3,14 +3,23 @@ from enum import Enum
|
|||
from typing import Sequence
|
||||
|
||||
from radicale import pathutils, utils
|
||||
from radicale.log import logger
|
||||
|
||||
INTERNAL_TYPES: Sequence[str] = ("none", "rabbitmq")
|
||||
|
||||
|
||||
def load(configuration):
|
||||
"""Load the storage module chosen in configuration."""
|
||||
return utils.load_plugin(
|
||||
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
|
||||
try:
|
||||
return utils.load_plugin(
|
||||
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
logger.warn("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type"))
|
||||
configuration = configuration.copy()
|
||||
configuration.update({"hook": {"type": "none"}}, "hook", privileged=True)
|
||||
return utils.load_plugin(
|
||||
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
|
||||
|
||||
|
||||
class BaseHook:
|
||||
|
|
|
@ -49,6 +49,12 @@ def read_components(s: str) -> List[vobject.base.Component]:
|
|||
s = re.sub(r"^(PHOTO(?:;[^:\r\n]*)?;ENCODING=b(?:;[^:\r\n]*)?:)"
|
||||
r"data:[^;,\r\n]*;base64,", r"\1", s,
|
||||
flags=re.MULTILINE | re.IGNORECASE)
|
||||
# Workaround for bug with malformed ICS files containing control codes
|
||||
# Filter out all control codes except those we expect to find:
|
||||
# * 0x09 Horizontal Tab
|
||||
# * 0x0A Line Feed
|
||||
# * 0x0D Carriage Return
|
||||
s = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', s)
|
||||
return list(vobject.readComponents(s, allowQP=True))
|
||||
|
||||
|
||||
|
@ -298,7 +304,7 @@ def find_time_range(vobject_item: vobject.base.Component, tag: str
|
|||
Returns a tuple (``start``, ``end``) where ``start`` and ``end`` are
|
||||
POSIX timestamps.
|
||||
|
||||
This is intened to be used for matching against simplified prefilters.
|
||||
This is intended to be used for matching against simplified prefilters.
|
||||
|
||||
"""
|
||||
if not tag:
|
||||
|
|
|
@ -241,7 +241,7 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
|
|||
|
||||
"""
|
||||
|
||||
# HACK: According to rfc5545-3.8.4.4 an recurrance that is resheduled
|
||||
# HACK: According to rfc5545-3.8.4.4 a recurrence that is rescheduled
|
||||
# with Recurrence ID affects the recurrence itself and all following
|
||||
# recurrences too. This is not respected and client don't seem to bother
|
||||
# either.
|
||||
|
|
|
@ -22,7 +22,7 @@ config (section "rights", key "file").
|
|||
The login is matched against the "user" key, and the collection path
|
||||
is matched against the "collection" key. In the "collection" regex you can use
|
||||
`{user}` and get groups from the "user" regex with `{0}`, `{1}`, etc.
|
||||
In consequence of the parameter subsitution you have to write `{{` and `}}`
|
||||
In consequence of the parameter substitution you have to write `{{` and `}}`
|
||||
if you want to use regular curly braces in the "user" and "collection" regexes.
|
||||
|
||||
For example, for the "user" key, ".+" means "authenticated user" and ".*"
|
||||
|
|
|
@ -291,7 +291,7 @@ def serve(configuration: config.Configuration,
|
|||
try:
|
||||
getaddrinfo = socket.getaddrinfo(address_port[0], address_port[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
|
||||
except OSError as e:
|
||||
logger.warn("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (format_address(address_port), e))
|
||||
logger.warning("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (format_address(address_port), e))
|
||||
continue
|
||||
logger.debug("getaddrinfo of '%s': %s" % (format_address(address_port), getaddrinfo))
|
||||
for (address_family, socket_kind, socket_proto, socket_flags, socket_address) in getaddrinfo:
|
||||
|
@ -299,7 +299,7 @@ def serve(configuration: config.Configuration,
|
|||
try:
|
||||
server = server_class(configuration, address_family, (socket_address[0], socket_address[1]), RequestHandler)
|
||||
except OSError as e:
|
||||
logger.warn("cannot create server socket on '%s': %s" % (format_address(socket_address), e))
|
||||
logger.warning("cannot create server socket on '%s': %s" % (format_address(socket_address), e))
|
||||
continue
|
||||
servers[server.socket] = server
|
||||
server.set_app(application)
|
||||
|
|
|
@ -84,7 +84,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||
cache_content = self._load_item_cache(href, cache_hash)
|
||||
if cache_content is None:
|
||||
with self._acquire_cache_lock("item"):
|
||||
# Lock the item cache to prevent multpile processes from
|
||||
# Lock the item cache to prevent multiple processes from
|
||||
# generating the same data in parallel.
|
||||
# This improves the performance for multiple requests.
|
||||
if self._storage._lock.locked == "r":
|
||||
|
@ -127,7 +127,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||
|
||||
def get_multi(self, hrefs: Iterable[str]
|
||||
) -> Iterator[Tuple[str, Optional[radicale_item.Item]]]:
|
||||
# It's faster to check for file name collissions here, because
|
||||
# It's faster to check for file name collisions here, because
|
||||
# we only need to call os.listdir once.
|
||||
files = None
|
||||
for href in hrefs:
|
||||
|
@ -146,7 +146,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||
|
||||
def get_all(self) -> Iterator[radicale_item.Item]:
|
||||
for href in self._list():
|
||||
# We don't need to check for collissions, because the file names
|
||||
# We don't need to check for collisions, because the file names
|
||||
# are from os.listdir.
|
||||
item = self._get(href, verify_href=False)
|
||||
if item is not None:
|
||||
|
|
|
@ -112,7 +112,7 @@ class BaseTest:
|
|||
for response in xml.findall(xmlutils.make_clark("D:response")):
|
||||
href = response.find(xmlutils.make_clark("D:href"))
|
||||
assert href.text not in path_responses
|
||||
prop_respones: Dict[str, Tuple[int, ET.Element]] = {}
|
||||
prop_responses: Dict[str, Tuple[int, ET.Element]] = {}
|
||||
for propstat in response.findall(
|
||||
xmlutils.make_clark("D:propstat")):
|
||||
status = propstat.find(xmlutils.make_clark("D:status"))
|
||||
|
@ -121,16 +121,16 @@ class BaseTest:
|
|||
for element in propstat.findall(
|
||||
"./%s/*" % xmlutils.make_clark("D:prop")):
|
||||
human_tag = xmlutils.make_human_tag(element.tag)
|
||||
assert human_tag not in prop_respones
|
||||
prop_respones[human_tag] = (status_code, element)
|
||||
assert human_tag not in prop_responses
|
||||
prop_responses[human_tag] = (status_code, element)
|
||||
status = response.find(xmlutils.make_clark("D:status"))
|
||||
if status is not None:
|
||||
assert not prop_respones
|
||||
assert not prop_responses
|
||||
assert status.text.startswith("HTTP/1.1 ")
|
||||
status_code = int(status.text.split(" ")[1])
|
||||
path_responses[href.text] = status_code
|
||||
else:
|
||||
path_responses[href.text] = prop_respones
|
||||
path_responses[href.text] = prop_responses
|
||||
return path_responses
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -115,6 +115,16 @@ class TestBaseAuthRequests(BaseTest):
|
|||
def test_htpasswd_comment(self) -> None:
|
||||
self._test_htpasswd("plain", "#comment\n #comment\n \ntmp:bepo\n\n")
|
||||
|
||||
def test_htpasswd_lc_username(self) -> None:
|
||||
self.configure({"auth": {"lc_username": "True"}})
|
||||
self._test_htpasswd("plain", "tmp:bepo", (
|
||||
("tmp", "bepo", True), ("TMP", "bepo", True), ("tmp1", "bepo", False)))
|
||||
|
||||
def test_htpasswd_strip_domain(self) -> None:
|
||||
self.configure({"auth": {"strip_domain": "True"}})
|
||||
self._test_htpasswd("plain", "tmp:bepo", (
|
||||
("tmp", "bepo", True), ("tmp@domain.example", "bepo", True), ("tmp1", "bepo", False)))
|
||||
|
||||
def test_remote_user(self) -> None:
|
||||
self.configure({"auth": {"type": "remote_user"}})
|
||||
_, responses = self.propfind("/", """\
|
||||
|
|
|
@ -360,7 +360,7 @@ permissions: RrWw""")
|
|||
self.get(path1, check=404)
|
||||
self.get(path2)
|
||||
|
||||
def test_move_between_colections(self) -> None:
|
||||
def test_move_between_collections(self) -> None:
|
||||
"""Move a item."""
|
||||
self.mkcalendar("/calendar1.ics/")
|
||||
self.mkcalendar("/calendar2.ics/")
|
||||
|
@ -373,7 +373,7 @@ permissions: RrWw""")
|
|||
self.get(path1, check=404)
|
||||
self.get(path2)
|
||||
|
||||
def test_move_between_colections_duplicate_uid(self) -> None:
|
||||
def test_move_between_collections_duplicate_uid(self) -> None:
|
||||
"""Move a item to a collection which already contains the UID."""
|
||||
self.mkcalendar("/calendar1.ics/")
|
||||
self.mkcalendar("/calendar2.ics/")
|
||||
|
@ -389,7 +389,7 @@ permissions: RrWw""")
|
|||
assert xml.tag == xmlutils.make_clark("D:error")
|
||||
assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
|
||||
|
||||
def test_move_between_colections_overwrite(self) -> None:
|
||||
def test_move_between_collections_overwrite(self) -> None:
|
||||
"""Move a item to a collection which already contains the item."""
|
||||
self.mkcalendar("/calendar1.ics/")
|
||||
self.mkcalendar("/calendar2.ics/")
|
||||
|
@ -403,8 +403,8 @@ permissions: RrWw""")
|
|||
self.request("MOVE", path1, check=204, HTTP_OVERWRITE="T",
|
||||
HTTP_DESTINATION="http://127.0.0.1/"+path2)
|
||||
|
||||
def test_move_between_colections_overwrite_uid_conflict(self) -> None:
|
||||
"""Move a item to a collection which already contains the item with
|
||||
def test_move_between_collections_overwrite_uid_conflict(self) -> None:
|
||||
"""Move an item to a collection which already contains the item with
|
||||
a different UID."""
|
||||
self.mkcalendar("/calendar1.ics/")
|
||||
self.mkcalendar("/calendar2.ics/")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue