Merge pull request #2190 from pbiering/issue-2188

max options extension
This commit is contained in:
Peter Bieringer
2026-08-03 08:23:18 +03:00
committed by GitHub
20 changed files with 461 additions and 22 deletions

View File

@@ -3,6 +3,8 @@
## 3.7.8.dev
* Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions
* Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync")
* Add: [report] max_expand_occurrence option to separate from max_freebusy_occurrence
* Add: [system] max_vevent_rrule_occurrence option to catch DoS by problematic RRULE early enough (workaround for missing protection in current vobject version)
## 3.7.7
* Fix: web plugin helpers httputils.serve_resource/serve_folder ignored their mimetypes and fallback_mimetype parameters and always used the built-in mapping, so custom web plugins could not serve additional file types with a correct Content-Type

View File

@@ -866,6 +866,19 @@ Limited to 80% of max_content_length to cover plain base64 encoded payload.
Announced to clients requesting "max-resource-size" via PROPFIND.
##### max_vevent_rrule_occurrence
_(>= 3.7.8)_
The maximum of occurrence by an rrule of a vevent.
Large time frames defined in RRULE by COUNT or UNTIL could
generate a lot of occurrences based on the time frame supplied. This
setting limits the lookup to prevent potential denial of service
attacks on large time frames. If the limit is reached, an HTTP error
is thrown instead of accepting the item.
Default: `10000`
##### timeout
Socket timeout. (seconds)
@@ -2191,6 +2204,19 @@ This is an automated message. Please do not reply.
#### [reporting]
##### max_expand_occurrence
_(>= 3.7.8)_
When returning an expanded report, a list of occurrences are
generated based on a given time frame. Large time frames could
generate a lot of occurrences based on the time frame supplied. This
setting limits the lookup to prevent potential denial of service
attacks on large time frames. If the limit is reached, an HTTP error
is thrown instead of returning the results.
Default: 10000
##### max_freebusy_occurrence
_(>= 3.2.3)_

7
config
View File

@@ -33,6 +33,9 @@
# Announced to clients requesting "max-resource-size" via PROPFIND
#max_resource_size = 10000000
# Max occurrence by an RRULE, limit the number to prevent DoS attacks.
#max_vevent_rrule_occurrence = 10000
# Socket timeout (seconds)
#timeout = 30
@@ -499,6 +502,10 @@ Content-Security-Policy = default-src 'self'; object-src 'none'
[reporting]
# When returning an expanded report, limit the number of returned
# occurences per event to prevent DoS attacks.
#max_expand_occurrence = 10000
# When returning a free-busy report, limit the number of returned
# occurences per event to prevent DoS attacks.
#max_freebusy_occurrence = 10000

View File

@@ -200,9 +200,10 @@ def run() -> None:
if args_ns.verify_item:
encoding = configuration.get("encoding", "stock")
max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence")
logger.info("Item verification start using 'stock' encoding: %s", encoding)
try:
if not item.verify(args_ns.verify_item[0], encoding):
if not item.verify(args_ns.verify_item[0], encoding, max_vevent_rrule_occurrence):
logger.critical("Item verification failed")
sys.exit(1)
except Exception as e:

View File

@@ -79,6 +79,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
_internal_server: bool
_max_content_length: int
_max_resource_size: int
_max_vevent_rrule_occurrence: int
_auth_realm: str
_auth_type: str
_web_type: str
@@ -120,6 +121,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
self._max_resource_size = max_resource_size_limited
else:
logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True))
self._max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence")
logger.info("max_vevent_rrule_occurrence set to: %d", self._max_vevent_rrule_occurrence)
self._bad_put_request_content = configuration.get("logging", "bad_put_request_content")
logger.info("log bad put request content: %s", self._bad_put_request_content)
self._request_header_on_debug = configuration.get("logging", "request_header_on_debug")

View File

@@ -122,6 +122,7 @@ class ApplicationBase:
_sharing: sharing.BaseSharing
_encoding: str
_max_resource_size: int
_max_vevent_rrule_occurrence: int
_permit_delete_collection: bool
_permit_overwrite_collection: bool
_strict_preconditions: bool

View File

@@ -47,6 +47,7 @@ PRODID = u"-//Radicale//NONSGML Version " + utils.package_version("radicale") +
def prepare(vobject_items: List[vobject.base.Component], path: str,
content_type: str, permission: bool, parent_permission: bool, max_resource_size: int,
max_vevent_rrule_occurrence: int,
tag: Optional[str] = None,
write_whole_collection: Optional[bool] = None) -> Tuple[
Iterator[radicale_item.Item], # items
@@ -73,7 +74,9 @@ def prepare(vobject_items: List[vobject.base.Component], path: str,
try:
if tag and write_whole_collection is not None:
radicale_item.check_and_sanitize_items(
vobject_items, is_collection=write_whole_collection, tag=tag)
vobject_items,
max_vevent_rrule_occurrence=max_vevent_rrule_occurrence,
is_collection=write_whole_collection, tag=tag)
if write_whole_collection and tag == "VCALENDAR":
vobject_components: List[vobject.base.Component] = []
vobject_item, = vobject_items
@@ -224,7 +227,9 @@ class ApplicationPartPut(ApplicationBase):
vobject_items, path, content_type,
bool(rights.intersect(access.permissions, "Ww")),
bool(rights.intersect(access.parent_permissions, "w")),
self._max_resource_size)
self._max_resource_size,
self._max_vevent_rrule_occurrence,
)
with self._storage.acquire_lock("w", user, path=path, request="PUT"):
item = next(iter(self._storage.discover(path)), None)
@@ -289,6 +294,7 @@ class ApplicationPartPut(ApplicationBase):
bool(rights.intersect(access.permissions, "Ww")),
bool(rights.intersect(access.parent_permissions, "w")),
self._max_resource_size,
self._max_vevent_rrule_occurrence,
tag, write_whole_collection)
props = prepared_props
if prepared_exc_info:

View File

@@ -895,9 +895,9 @@ class ApplicationPartReport(ApplicationBase):
assert item.collection is not None
collection = item.collection
max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence")
if xml_content is not None and \
xml_content.tag == xmlutils.make_clark("C:free-busy-query"):
max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence")
try:
status, body = free_busy_report(
base_prefix, path, xml_content, collection, self._encoding,
@@ -909,6 +909,7 @@ class ApplicationPartReport(ApplicationBase):
headers = {"Content-Type": "text/calendar; charset=%s" % self._encoding}
return status, headers, str(body), xmlutils.pretty_xml(xml_content)
else:
max_occurrence = self.configuration.get("reporting", "max_expand_occurrence")
try:
status, xml_answer = xml_report(
base_prefix, path, xml_content, collection, self._encoding,

View File

@@ -254,6 +254,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"value": "10000000",
"help": "maximum size of resource (default: 10 Mbyte)",
"type": positive_int}),
("max_vevent_rrule_occurrence", {
"value": "10000",
"help": "maximum occurrence by an RRULE (default: 10000)",
"type": positive_int}),
("timeout", {
"value": "30",
"help": "socket timeout",
@@ -813,9 +817,13 @@ This is an automated message. Please do not reply.""",
("headers", OrderedDict([
("_allow_extra", str)])),
("reporting", OrderedDict([
("max_expand_occurrence", {
"value": "10000",
"help": "number of expand occurrences per event when reporting",
"type": positive_int}),
("max_freebusy_occurrence", {
"value": "10000",
"help": "number of occurrences per event when reporting",
"help": "number of free-busy occurrences per event when reporting",
"type": positive_int})]))
])

View File

@@ -53,6 +53,17 @@ VCF_TO_ICS_SUPPORTED_PLACEHOLDERS: list = ["fn", "n:f", "n:g", "n:a", "age", "ni
# List of BDAY years acting as flag for "no year specified"
VCF_TO_ICS_BDAY_NO_YEAR: list = ["1604"]
# List of RRULE frequencies and their interval in seconds
RRULE_FREQUENCIES_TO_INTERVAL: dict[str, float] = {
"YEARLY": 60*60*24*365,
"MONTHLY": 60*60*24*365/12,
"WEEKLY": 60*60*24*7,
"DAILY": 60*60*24,
"HOURLY": 60*60,
"MINUTELY": 60,
"SECONDLY": 1,
}
def read_components(s: str) -> List[vobject.base.Component]:
"""Wrapper for vobject.readComponents"""
@@ -103,6 +114,7 @@ def predict_tag_of_whole_collection(
def check_and_sanitize_items(
vobject_items: List[vobject.base.Component],
max_vevent_rrule_occurrence: int,
is_collection: bool = False, tag: str = "") -> None:
"""Check vobject items for common errors and add missing UIDs.
@@ -232,11 +244,77 @@ def check_and_sanitize_items(
if ref_value_param is not None:
dates.params["VALUE"] = ref_value_param
# vobject interprets recurrence rules on demand
try:
component.rruleset
except Exception as e:
raise ValueError("Invalid recurrence rules in %s in object %r"
% (component.name, component_uid)) from e
if hasattr(component, "rrule"):
# workaround for vobject < 1.0.0 as it has no limiter in "getrruleset"
logger.trace("Recurrence rule found in %s in object %r: %r", component.name, component_uid, component.rrule.value)
if not hasattr(component, "dtstart"):
# e.g. VTODO
rrule = vobject.icalendar.rrule.rrulestr(component.rrule.value)
else:
dtstart = radicale_filter.date_to_datetime(component.dtstart.value)
ignoretz = (
not isinstance(dtstart, datetime.datetime)
or dtstart.tzinfo is None
)
rrule = vobject.icalendar.rrule.rrulestr(component.rrule.value, ignoretz=ignoretz)
# early check of maximum of COUNT to avoid DoS (workaround)
if hasattr(rrule, "_count") and rrule._count is not None:
logger.trace("Recurrence rule %r in %s in object %r contains: COUNT=%d", component.rrule.value, component.name, component_uid, rrule._count)
if max_vevent_rrule_occurrence > 0 and rrule._count > max_vevent_rrule_occurrence:
logger.error("Recurrence rule %r count in %s in object %r: %d (REJECTED/limit: %d)", component.rrule.value, component.name, component_uid, rrule._count, max_vevent_rrule_occurrence)
raise ValueError("Too many recurrence rule entries in %s in object %r: %d (limit: %d)"
% (component.name, component_uid, rrule._count, max_vevent_rrule_occurrence))
else:
logger.debug("Recurrence rule count in %s in object %r: %d (PASSED/limit: %d)" % (component.name, component_uid, rrule._count, max_vevent_rrule_occurrence))
else:
logger.trace("Recurrence rule %r in %s in object %r doesn't contain: COUNT", component.rrule.value, component.name, component_uid)
# early check of maximum of (UNTIL-DTSTART)/interval(FREQ) to avoid DoS (ugly workaround with some guessing)
if hasattr(rrule, "_freq") and rrule._freq is not None and hasattr(rrule, "_until") and rrule._until is not None and hasattr(component, "dtstart"):
if vobject.icalendar.FREQUENCIES[rrule._freq] not in RRULE_FREQUENCIES_TO_INTERVAL:
raise ValueError("Unsupported FREQ in recurrence rule in %s in object %r: %r"
% (component.name, component_uid, rrule._freq))
# RRULE has known FREQ+UNTIL+DTSTART
# TZ code taken from vobject/icalendar.py/getrruleset
if rrule._until.tzinfo is None:
rrule._until = rrule._until.replace(tzinfo=dtstart.tzinfo)
if dtstart.tzinfo is not None:
rrule._until = rrule._until.astimezone(dtstart.tzinfo)
delta = rrule._until - dtstart
seconds = delta.total_seconds()
if seconds < 0:
# UNTIL < DTSTART
logger.error("Recurrence rule %r in %s in object %r REJECTED, UNTIL < DTSTART", component.rrule.value, component.name, component_uid)
raise ValueError("Recurrence rule in %s in object %r has UNTIL < DTSTART"
% (component.name, component_uid))
rrule_entries = seconds / RRULE_FREQUENCIES_TO_INTERVAL[vobject.icalendar.FREQUENCIES[rrule._freq]]
if max_vevent_rrule_occurrence > 0 and rrule_entries > max_vevent_rrule_occurrence:
logger.warning("Recurrence rule %r entries in %s in object %r: %d (estimated/REJECTED/limit: %d)", component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)
raise ValueError("Too many recurrence rule entries in %s in object %r: %d (limit: %d)"
% (component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence))
else:
logger.debug("Recurrence rule %r entries in %s in object %r: %d (estimated/PASSED/limit: %d)" % (component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence))
else:
logger.trace("Recurrence rule %r in %s in object %r doesn't contain: FREQ+UNTIL", component.rrule.value, component.name, component_uid)
# generic check by vobject
try:
rruleset = component.rruleset
except Exception as e:
raise ValueError("Invalid recurrence rules in %s in object %r"
% (component.name, component_uid)) from e
# check limit (last resort)
infinite = False
if (";UNTIL=" not in component.rrule.value and
";COUNT=" not in component.rrule.value):
infinite = True
if infinite is False:
rrule_entries = len(list(rruleset))
if max_vevent_rrule_occurrence > 0 and rrule_entries > max_vevent_rrule_occurrence:
logger.warning("Recurrence rule %r entries in %s in object %r: %d (calculated/REJECTED/limit: %d)", component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)
raise ValueError("Too many recurrence rule entries in %s in object %r: %d (limit: %d)"
% (component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence))
else:
logger.debug("Recurrence rule %r entries in %s in object %r: %d (calculated/PASSED/limit: %d)", component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)
elif tag == "VADDRESSBOOK":
# https://tools.ietf.org/html/rfc6352#section-5.1
object_uids = set()
@@ -387,7 +465,7 @@ def find_time_range(vobject_item: vobject.base.Component, tag: str
return math.floor(start.timestamp()), math.ceil(end.timestamp())
def verify(file: str, encoding: str):
def verify(file: str, encoding: str, max_vevent_rrule_occurrence: int):
logger.info("Verifying item: %s", file)
with open(file, "rb") as f:
content_raw = f.read()
@@ -407,7 +485,7 @@ def verify(file: str, encoding: str):
try:
tag = radicale_item.predict_tag_of_whole_collection(vobject_items)
if tag is not None:
radicale_item.check_and_sanitize_items(vobject_items, tag=tag)
radicale_item.check_and_sanitize_items(vobject_items, tag=tag, max_vevent_rrule_occurrence=max_vevent_rrule_occurrence)
else:
raise ValueError("collection tag cannot be predicted")
except Exception as e:

View File

@@ -106,6 +106,7 @@ class StorageBase(storage.BaseStorage):
"logging", "storage_cache_actions_on_debug")
self._max_resource_size = configuration.get(
"server", "max_resource_size")
self._max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence")
def _get_collection_root_folder(self) -> str:
return os.path.join(self._filesystem_folder, "collection-root")

View File

@@ -124,7 +124,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
vobject_items = radicale_item.read_components(
raw_text.decode(self._encoding))
radicale_item.check_and_sanitize_items(
vobject_items, tag=self.tag)
vobject_items, tag=self.tag, max_vevent_rrule_occurrence=self._storage._max_vevent_rrule_occurrence)
vobject_item, = vobject_items
temp_item = radicale_item.Item(
collection=self, vobject_item=vobject_item)

View File

@@ -0,0 +1,35 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Paris
X-LIC-LOCATION:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20130902T150157Z
LAST-MODIFIED:20130902T150158Z
DTSTAMP:20130902T150158Z
UID:event1-Y2040
SUMMARY:Event in year 2040
CATEGORIES:some_category1,another_category2
ORGANIZER:mailto:unclesam@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Jane Doe:MAILTO:janedoe@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="MAILTO:bob@host.com";PARTSTAT=ACCEPTED;CN=John Doe:MAILTO:johndoe@example.com
DTSTART;TZID=Europe/Paris:20400901T180000
DTEND;TZID=Europe/Paris:20400901T190000
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,31 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102
DTEND;TZID=US/Eastern:20060103
RRULE:FREQ=DAILY;COUNT=500
SUMMARY:Recurring event with count 500
UID:event_full_day_rrule_count_500
DTSTAMP:20060102T094829Z
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,31 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102
DTEND;TZID=US/Eastern:20060103
RRULE:FREQ=DAILY;UNTIL=20080101
SUMMARY:Recurring event with until +2y
UID:event_full_day_rrule_until_2y
DTSTAMP:20060102T094829Z
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,31 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102
DTEND;TZID=US/Eastern:20060103
RRULE:FREQ=DAILY;UNTIL=70060101
SUMMARY:Recurring event with until +5000y
UID:event_full_day_rrule_until_5000y
DTSTAMP:20060102T094829Z
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,31 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102
DTEND;TZID=US/Eastern:20060103
RRULE:FREQ=DAILY;UNTIL=20560101
SUMMARY:Recurring event with until +50y
UID:event_full_day_rrule_until_50y
DTSTAMP:20060102T094829Z
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,31 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102
DTEND;TZID=US/Eastern:20060103
RRULE:FREQ=DAILY;UNTIL=20050101
SUMMARY:Recurring event with until in the past
UID:event_full_day_rrule_until_in_the_past
DTSTAMP:20060102T094829Z
END:VEVENT
END:VCALENDAR

View File

@@ -151,6 +151,19 @@ permissions: RrWw""")
assert "Event" in answer
assert "UID:event" in answer
def test_add_event_y2040(self) -> None:
"""Add an event with year 2040."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1_y2040.ics")
path = "/calendar.ics/event1_y2040.ics"
self.put(path, event)
_, headers, answer = self.request("GET", path, check=200)
assert "ETag" in headers
assert headers["Content-Type"] == "text/calendar; charset=utf-8"
assert "VEVENT" in answer
assert "Event" in answer
assert "UID:event" in answer
@pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
def test_add_event_with_rdate_period_start_duration_single(self) -> None:
"""Add an event with RDATE/PERIOD with start+duration."""
@@ -328,6 +341,72 @@ permissions: RrWw""")
event = get_file_content("event_mixed_datetime_and_date.ics")
self.put("/calendar.ics/event.ics", event)
def test_add_event_with_rrule_count_500_limit_100(self) -> None:
"""Test event with RRULE COUNT=500 and limit 100."""
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_count_500.ics")
self.put("/calendar.ics/event_full_day_rrule_count_500.ics", event, check=400)
def test_add_event_with_rrule_count_500_limit_600(self) -> None:
"""Test event with RRULE COUNT=500 and limit 600."""
self.configure({"server": {"max_vevent_rrule_occurrence": 600}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_count_500.ics")
self.put("/calendar.ics/event_full_day_rrule_count_500.ics", event)
def test_add_event_with_rrule_until_2y_limit_100(self) -> None:
"""Test event with RRULE UNTIL=+2y and limit 100."""
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_2y.ics")
self.put("/calendar.ics/event_full_day_rrule_until_2y.ics", event, check=400)
def test_add_event_with_rrule_until_2y_limit_800(self) -> None:
"""Test event with RRULE UNTIL=+2y and limit 800."""
self.configure({"server": {"max_vevent_rrule_occurrence": 800}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_2y.ics")
self.put("/calendar.ics/event_full_day_rrule_until_2y.ics", event)
def test_add_event_with_rrule_until_before_dtstart(self) -> None:
"""Test event with RRULE UNTIL < DTSTART."""
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_before_dtstart.ics")
self.put("/calendar.ics/event_full_day_rrule_until_before_dtstart.ics", event, check=400)
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform")
def test_add_event_with_rrule_until_50y_limit_100(self) -> None:
"""Test event with RRULE UNTIL=+50y and limit 100."""
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_50y.ics")
self.put("/calendar.ics/event_full_day_rrule_until_50y.ics", event, check=400)
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform")
def test_add_event_with_rrule_until_50y_limit_20000(self) -> None:
"""Test event with RRULE UNTIL=+50y and limit 20000."""
self.configure({"server": {"max_vevent_rrule_occurrence": 20000}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_50y.ics")
self.put("/calendar.ics/event_full_day_rrule_until_50y.ics", event)
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform")
def test_add_event_with_rrule_until_5000y_limit_100(self) -> None:
"""Test event with RRULE UNTIL=+5000y and limit 100."""
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_5000y.ics")
self.put("/calendar.ics/event_full_day_rrule_until_5000y.ics", event, check=400)
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform")
def test_add_event_with_rrule_until_5000y_limit_2000000(self) -> None:
"""Test event with RRULE UNTIL=+5000y and limit 2000000."""
self.configure({"server": {"max_vevent_rrule_occurrence": 2000000}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event_full_day_rrule_until_5000y.ics")
self.put("/calendar.ics/event_full_day_rrule_until_5000y.ics", event)
def test_add_event_with_exdate_without_rrule(self) -> None:
"""Test event with EXDATE but not having RRULE."""
self.mkcalendar("/calendar.ics/")

View File

@@ -128,7 +128,8 @@ permissions: RrWw""")
expected_start_times: List[str],
expected_end_times: List[str],
only_dates: bool,
nr_uids: int) -> None:
nr_uids: int,
check: int = 207) -> None:
_, responses = self.report("/calendar.ics/",
self._req_without_expand(expected_uid, start, end))
assert len(responses) == 1
@@ -154,7 +155,9 @@ permissions: RrWw""")
assert len(uids) == nr_uids
_, responses = self.report("/calendar.ics/",
self._req_with_expand(expected_uid, start, end))
self._req_with_expand(expected_uid, start, end), check=check)
if check != 207:
return
assert len(responses) == 1
@@ -176,17 +179,21 @@ permissions: RrWw""")
uids.append(line)
if line.startswith("RECURRENCE-ID:"):
assert line in expected_recurrence_ids
if expected_recurrence_ids:
assert line in expected_recurrence_ids
recurrence_ids.append(line)
if line.startswith("DTSTART:"):
assert line in expected_start_times
if expected_start_times:
assert line in expected_start_times
if line.startswith("DTEND:"):
assert line in expected_end_times
if expected_end_times:
assert line in expected_end_times
assert len(uids) == len(expected_recurrence_ids)
assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
if expected_recurrence_ids:
assert len(uids) == len(expected_recurrence_ids)
assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
def _test_expand_max(self,
expected_uid: str,
@@ -289,6 +296,35 @@ permissions: RrWw""")
1
)
def test_report_with_expand_property_all_day_count_500_event_pass(self) -> None:
"""Test report with expand property for all day count 500 events"""
self.configure({"reporting": {"max_expand_occurrence": 501}})
self._test_expand(
"event_full_day_rrule_count_500",
"20060103T000000Z",
"20080105T000000Z",
[],
[],
[],
ONLY_DATES,
1
)
def test_report_with_expand_property_all_day_count_500_event_reject(self) -> None:
"""Test report with expand property for all day count 500 events"""
self.configure({"reporting": {"max_expand_occurrence": 10}})
self._test_expand(
"event_full_day_rrule_count_500",
"20060103T000000Z",
"20080105T000000Z",
[],
[],
[],
ONLY_DATES,
1,
400
)
def test_report_with_expand_property_overridden(self) -> None:
"""Test report with expand property with overridden events"""
self._test_expand(
@@ -326,7 +362,7 @@ permissions: RrWw""")
def test_report_with_expand_property_max_occur(self) -> None:
"""Test report with expand property too many vevents"""
self.configure({"reporting": {"max_freebusy_occurrence": 100}})
self.configure({"reporting": {"max_expand_occurrence": 100}})
self._test_expand_max(
"event_daily_rrule_forever",
"20060103T000000Z",
@@ -336,7 +372,7 @@ permissions: RrWw""")
def test_report_with_max_occur(self) -> None:
"""Test report with too many vevents"""
self.configure({"reporting": {"max_freebusy_occurrence": 10}})
self.configure({"reporting": {"max_expand_occurrence": 10}})
uid = "event_multiple_too_many"
start = "20130901T000000Z"