Merge pull request #2063 from pbiering/align-delay
Align delay on missing auth or server error
This commit is contained in:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -223,7 +223,7 @@ jobs:
|
|||||||
integ-test:
|
integ-test:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test-ubuntu-python-newest
|
needs: [test-ubuntu-python-newest, htmlvalidator, js-test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
|
|||||||
@@ -838,9 +838,9 @@ Default: `8`
|
|||||||
|
|
||||||
_(>= 3.7.0)_
|
_(>= 3.7.0)_
|
||||||
|
|
||||||
Base delay in case of error response (seconds)
|
Base delay in case of error 5xx response (seconds)
|
||||||
|
|
||||||
Default: `0.01`
|
Default: `1`
|
||||||
|
|
||||||
##### max_content_length
|
##### max_content_length
|
||||||
|
|
||||||
@@ -1078,9 +1078,7 @@ Default: `False`
|
|||||||
|
|
||||||
##### delay
|
##### delay
|
||||||
|
|
||||||
Average delay (in seconds) after failed login attempts.
|
Average delay (in seconds) after failed or missing login attempts or denied access.
|
||||||
|
|
||||||
Also used for invalid/not-existing/not-enabled share-by-token. _(>= 3.7.0)_
|
|
||||||
|
|
||||||
Default: `1`
|
Default: `1`
|
||||||
|
|
||||||
|
|||||||
7
config
7
config
@@ -21,8 +21,8 @@
|
|||||||
# Max parallel connections
|
# Max parallel connections
|
||||||
#max_connections = 8
|
#max_connections = 8
|
||||||
|
|
||||||
# Base delay in case of error response (seconds)
|
# Base delay in case of error 5xx response (seconds)
|
||||||
#delay_on_error = 0.01
|
#delay_on_error = 1
|
||||||
|
|
||||||
# Max size of request body (bytes), default: 100 Mbyte
|
# Max size of request body (bytes), default: 100 Mbyte
|
||||||
# In case of using a reverse proxy in front of check also there related option
|
# In case of using a reverse proxy in front of check also there related option
|
||||||
@@ -180,8 +180,7 @@
|
|||||||
# Enable caching of htpasswd file based on size and mtime_ns
|
# Enable caching of htpasswd file based on size and mtime_ns
|
||||||
#htpasswd_cache = False
|
#htpasswd_cache = False
|
||||||
|
|
||||||
# Incorrect authentication delay (seconds)
|
# Incorrect or missing authentication or denied access average delay (seconds)
|
||||||
# Also used for invalid/not-existing/not-enabled share-by-token (>= 3.7.0)
|
|
||||||
#delay = 1
|
#delay = 1
|
||||||
|
|
||||||
# Message displayed in the client when a password is needed
|
# Message displayed in the client when a password is needed
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import cProfile
|
|||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
import pstats
|
import pstats
|
||||||
import random
|
import random
|
||||||
@@ -292,6 +293,26 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||||||
logger.debug("Response header: suppressed by config/option [logging] response_header_on_debug")
|
logger.debug("Response header: suppressed by config/option [logging] response_header_on_debug")
|
||||||
|
|
||||||
# Start response
|
# Start response
|
||||||
|
# delay on error
|
||||||
|
delay: float = 0.0
|
||||||
|
if (status == 401 or status == 403) and (not login or user):
|
||||||
|
# delay for required but missing authentication or denied access
|
||||||
|
if self._auth_delay > 0:
|
||||||
|
if 'PYTEST_VERSION' in os.environ:
|
||||||
|
# no random during tests
|
||||||
|
delay = self._auth_delay
|
||||||
|
else:
|
||||||
|
delay = self._auth_delay * (0.5 + random.random())
|
||||||
|
if status >= 500 and status <= 599:
|
||||||
|
if self._delay_on_error > 0:
|
||||||
|
delay = self._delay_on_error
|
||||||
|
if delay > 0:
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
if 'PYTEST_VERSION' in os.environ:
|
||||||
|
logger.debug("Response fixed(pytest) delay triggered by result code: %d -> %0.3f seconds", status, delay)
|
||||||
|
else:
|
||||||
|
logger.debug("Response random delay triggered by result code: %d -> %0.3f seconds", status, delay)
|
||||||
|
time.sleep(delay)
|
||||||
time_end = datetime.datetime.now()
|
time_end = datetime.datetime.now()
|
||||||
time_delta_seconds = (time_end - time_begin).total_seconds()
|
time_delta_seconds = (time_end - time_begin).total_seconds()
|
||||||
status_text = "%d %s" % (
|
status_text = "%d %s" % (
|
||||||
@@ -310,19 +331,10 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||||||
flags_text = " (" + " ".join(flags) + ")"
|
flags_text = " (" + " ".join(flags) + ")"
|
||||||
else:
|
else:
|
||||||
flags_text = ""
|
flags_text = ""
|
||||||
# delay on error
|
|
||||||
if status >= 400:
|
|
||||||
if self._delay_on_error > 0:
|
|
||||||
random_delay = self._delay_on_error * (1 + random.random())
|
|
||||||
if status >= 500:
|
|
||||||
random_delay = 2 * random_delay
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("Response delay triggered by result code: %d -> %0.3f seconds", status, random_delay)
|
|
||||||
time.sleep(random_delay)
|
|
||||||
if answer is not None:
|
if answer is not None:
|
||||||
logger.info("%s response status for %r%s in %.3f seconds %s %s bytes%s: %s",
|
logger.info("%s response status for %r%s in %.3f seconds %s %s bytes%s: %s",
|
||||||
request_method, unsafe_path, depthinfo,
|
request_method, unsafe_path, depthinfo,
|
||||||
(time_end - time_begin).total_seconds(), content_encoding, str(len(answer)),
|
time_delta_seconds, content_encoding, str(len(answer)),
|
||||||
flags_text,
|
flags_text,
|
||||||
status_text)
|
status_text)
|
||||||
else:
|
else:
|
||||||
@@ -487,8 +499,15 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||||||
remote_host, login, info)
|
remote_host, login, info)
|
||||||
# Random delay to avoid timing oracles and bruteforce attacks
|
# Random delay to avoid timing oracles and bruteforce attacks
|
||||||
if self._auth_delay > 0:
|
if self._auth_delay > 0:
|
||||||
random_delay = self._auth_delay * (0.5 + random.random())
|
if 'PYTEST_VERSION' in os.environ:
|
||||||
logger.debug("Failed login, sleeping random: %.3f sec", random_delay)
|
# no random during tests
|
||||||
|
random_delay = self._auth_delay
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("Failed login, sleeping fixed(pytest): %.3f sec", random_delay)
|
||||||
|
else:
|
||||||
|
random_delay = self._auth_delay * (0.5 + random.random())
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("Failed login, sleeping random: %.3f sec", random_delay)
|
||||||
time.sleep(random_delay)
|
time.sleep(random_delay)
|
||||||
|
|
||||||
if user and not pathutils.is_safe_path_component(user):
|
if user and not pathutils.is_safe_path_component(user):
|
||||||
@@ -566,12 +585,15 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||||||
self.profiler_per_request_method[request_method].disable()
|
self.profiler_per_request_method[request_method].disable()
|
||||||
|
|
||||||
if (status, headers, answer, xml_request) == httputils.NOT_ALLOWED:
|
if (status, headers, answer, xml_request) == httputils.NOT_ALLOWED:
|
||||||
logger.info("Access to %r denied for %s", path,
|
if path.startswith("/.token"):
|
||||||
repr(user) if user else "anonymous user")
|
logger.info("Access to %r denied", path)
|
||||||
|
else:
|
||||||
|
logger.info("Access to %r denied for %s", path, repr(user) if user else "anonymous user")
|
||||||
else:
|
else:
|
||||||
status, headers, answer, xml_request = httputils.NOT_ALLOWED
|
status, headers, answer, xml_request = httputils.NOT_ALLOWED
|
||||||
|
|
||||||
if ((status, headers, answer, xml_request) == httputils.NOT_ALLOWED and not user and
|
if ((status, headers, answer, xml_request) == httputils.NOT_ALLOWED and not user and
|
||||||
|
not path.startswith("/.token") and
|
||||||
not external_login):
|
not external_login):
|
||||||
# Unknown or unauthorized user
|
# Unknown or unauthorized user
|
||||||
logger.debug("Asking client for authentication")
|
logger.debug("Asking client for authentication")
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||||||
"help": "maximum size of request body in bytes (default: 100 Mbyte)",
|
"help": "maximum size of request body in bytes (default: 100 Mbyte)",
|
||||||
"type": positive_int}),
|
"type": positive_int}),
|
||||||
("delay_on_error", {
|
("delay_on_error", {
|
||||||
"value": "0.01",
|
"value": "1",
|
||||||
"help": "base delay in case of error response (seconds)",
|
"help": "base delay in case of error 5xx response (seconds)",
|
||||||
"type": positive_float}),
|
"type": positive_float}),
|
||||||
("max_resource_size", {
|
("max_resource_size", {
|
||||||
"value": "10000000",
|
"value": "10000000",
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import time
|
|
||||||
import uuid
|
import uuid
|
||||||
from csv import DictWriter
|
from csv import DictWriter
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -394,10 +392,6 @@ class BaseSharing:
|
|||||||
if share is None:
|
if share is None:
|
||||||
share = self.sharing_collection_by_token_resolver(path)
|
share = self.sharing_collection_by_token_resolver(path)
|
||||||
if share is not None and 'error' in share:
|
if share is not None and 'error' in share:
|
||||||
if self._auth_delay > 0:
|
|
||||||
random_delay = self._auth_delay * (0.5 + random.random())
|
|
||||||
logger.debug("Failed shared-by-token resolver, sleeping random: %.3f sec", random_delay)
|
|
||||||
time.sleep(random_delay)
|
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ Radicale tests with simple requests and authentication.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
from typing import Iterable, Tuple, Union
|
from typing import Iterable, Tuple, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -61,7 +61,7 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
|
|
||||||
def _test_htpasswd(self, htpasswd_encryption: str, htpasswd_content: str,
|
def _test_htpasswd(self, htpasswd_encryption: str, htpasswd_content: str,
|
||||||
test_matrix: Union[str, Iterable[Tuple[str, str, bool]]]
|
test_matrix: Union[str, Iterable[Tuple[str, str, bool]]]
|
||||||
= "ascii", delay: int = 0) -> None:
|
= "ascii", delay: float = 0) -> None:
|
||||||
"""Test htpasswd authentication with user "tmp" and password "bepo" for
|
"""Test htpasswd authentication with user "tmp" and password "bepo" for
|
||||||
``test_matrix`` "ascii" or user "😀" and password "🔑" for
|
``test_matrix`` "ascii" or user "😀" and password "🔑" for
|
||||||
``test_matrix`` "unicode"."""
|
``test_matrix`` "unicode"."""
|
||||||
@@ -223,16 +223,25 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
def test_htpasswd_login_cache_failed_delay_plain(self, caplog) -> None:
|
def test_htpasswd_login_cache_failed_delay_plain(self, caplog) -> None:
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
self.configure({"auth": {"cache_logins": "True"}})
|
self.configure({"auth": {"cache_logins": "True"}})
|
||||||
delay = 1
|
delay = .3
|
||||||
delay_ns = delay * 10**9 * 0.5 # delay minimum jitter
|
delay_min = delay * 0.9 # no random jitter during test
|
||||||
time_ns_begin1 = time.time_ns()
|
delay_max = delay + 0.2 # no random jitter during test
|
||||||
|
if sys.platform == "darwin": # no reliable sleep times
|
||||||
|
delay_max = delay_max * 1.5
|
||||||
|
|
||||||
|
time_begin = datetime.datetime.now()
|
||||||
self._test_htpasswd("plain", "tmp:bepo", [("tmp", "bepo1", False)], delay=delay)
|
self._test_htpasswd("plain", "tmp:bepo", [("tmp", "bepo1", False)], delay=delay)
|
||||||
time_ns_end1 = time.time_ns()
|
time_end = datetime.datetime.now()
|
||||||
time_ns_begin2 = time.time_ns()
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
|
time_begin = datetime.datetime.now()
|
||||||
self._test_htpasswd("plain", "tmp:bepo", [("tmp", "bepo1", False)], delay=delay)
|
self._test_htpasswd("plain", "tmp:bepo", [("tmp", "bepo1", False)], delay=delay)
|
||||||
time_ns_end2 = time.time_ns()
|
time_end = datetime.datetime.now()
|
||||||
assert (time_ns_end1 - time_ns_begin1) > delay_ns
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
assert (time_ns_end2 - time_ns_begin2) > delay_ns
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
# htpasswd file cache
|
# htpasswd file cache
|
||||||
def test_htpasswd_file_cache(self, caplog) -> None:
|
def test_htpasswd_file_cache(self, caplog) -> None:
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ Radicale tests with simple requests.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
|
import sys
|
||||||
import urllib
|
import urllib
|
||||||
from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple
|
from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
@@ -727,7 +729,7 @@ permissions: RrWw""")
|
|||||||
assert responses["/calendar.ics/"] == 200
|
assert responses["/calendar.ics/"] == 200
|
||||||
self.get("/calendar.ics/", check=404)
|
self.get("/calendar.ics/", check=404)
|
||||||
|
|
||||||
def test_delete_collection_global_forbid(self) -> None:
|
def test_delete_collection_global_forbid_base(self) -> None:
|
||||||
"""Delete a collection (expect forbidden)."""
|
"""Delete a collection (expect forbidden)."""
|
||||||
self.configure({"rights": {"permit_delete_collection": False}})
|
self.configure({"rights": {"permit_delete_collection": False}})
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
@@ -736,6 +738,23 @@ permissions: RrWw""")
|
|||||||
_, responses = self.delete("/calendar.ics/", check=401)
|
_, responses = self.delete("/calendar.ics/", check=401)
|
||||||
self.get("/calendar.ics/", check=200)
|
self.get("/calendar.ics/", check=200)
|
||||||
|
|
||||||
|
def test_delete_collection_global_forbid_delay(self) -> None:
|
||||||
|
"""Delete a collection (expect forbidden, check delay)."""
|
||||||
|
delay = .3
|
||||||
|
delay_min = delay * 0.9 # no random jitter during test
|
||||||
|
delay_max = delay + 0.2 # no random jitter during test
|
||||||
|
if sys.platform == "darwin": # no reliable sleep times
|
||||||
|
delay_max = delay_max * 1.5
|
||||||
|
|
||||||
|
self.configure({"rights": {"permit_delete_collection": False}, "auth": {"delay": delay}})
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
time_begin = datetime.datetime.now()
|
||||||
|
_, responses = self.delete("/calendar.ics/", check=401)
|
||||||
|
time_end = datetime.datetime.now()
|
||||||
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
def test_delete_collection_global_forbid_explicit_permit(self) -> None:
|
def test_delete_collection_global_forbid_explicit_permit(self) -> None:
|
||||||
"""Delete a collection with permitted path (expect permit)."""
|
"""Delete a collection with permitted path (expect permit)."""
|
||||||
self.configure({"rights": {"permit_delete_collection": False}})
|
self.configure({"rights": {"permit_delete_collection": False}})
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ Radicale tests related to sharing.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import sys
|
||||||
from typing import Dict, Sequence, Tuple, Union
|
from typing import Dict, Sequence, Tuple, Union
|
||||||
|
|
||||||
from radicale import sharing, xmlutils
|
from radicale import sharing, xmlutils
|
||||||
@@ -202,7 +203,7 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"collection_by_token": "False"}
|
"collection_by_token": "False"}
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_sharing_api_base_no_auth(self) -> None:
|
def test_sharing_api_base_no_auth_basic(self) -> None:
|
||||||
"""POST request at '/.sharing' without authentication."""
|
"""POST request at '/.sharing' without authentication."""
|
||||||
# disabled
|
# disabled
|
||||||
for path in ["/.sharing", "/.sharing/"]:
|
for path in ["/.sharing", "/.sharing/"]:
|
||||||
@@ -258,6 +259,39 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
})
|
})
|
||||||
_, headers, _ = self.request("POST", path, check=401)
|
_, headers, _ = self.request("POST", path, check=401)
|
||||||
|
|
||||||
|
def test_sharing_api_base_no_auth_delay(self) -> None:
|
||||||
|
delay = .3
|
||||||
|
delay_min = delay * 0.9 # no random jitter during test
|
||||||
|
delay_max = delay + 0.2 # no random jitter during test
|
||||||
|
if sys.platform == "darwin": # no reliable sleep times
|
||||||
|
delay_max = delay_max * 1.5
|
||||||
|
|
||||||
|
for path in ["/.sharing", "/.sharing/"]:
|
||||||
|
time_begin = datetime.datetime.now()
|
||||||
|
_, headers, _ = self.request("POST", path, check=404)
|
||||||
|
time_end = datetime.datetime.now()
|
||||||
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta < delay_min # 404 should have no delay
|
||||||
|
|
||||||
|
path = "/.sharing/"
|
||||||
|
|
||||||
|
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
|
||||||
|
logging.info("\n*** test: %s", db_type)
|
||||||
|
self.configure({"sharing": {"type": db_type}})
|
||||||
|
|
||||||
|
# no database is active
|
||||||
|
logging.info("\n*** check API hook base: map=True token=False (incl. delay)")
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"collection_by_map": "True",
|
||||||
|
"collection_by_token": "False"},
|
||||||
|
"auth": {"delay": delay}})
|
||||||
|
time_begin = datetime.datetime.now()
|
||||||
|
_, headers, _ = self.request("POST", path, check=401)
|
||||||
|
time_end = datetime.datetime.now()
|
||||||
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
def test_sharing_api_base_with_auth(self) -> None:
|
def test_sharing_api_base_with_auth(self) -> None:
|
||||||
"""POST request at '/.sharing' with authentication."""
|
"""POST request at '/.sharing' with authentication."""
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
@@ -834,7 +868,7 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert "Status='success'" in answer
|
assert "Status='success'" in answer
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using invalid token")
|
logging.info("\n*** fetch collection using invalid token")
|
||||||
_, headers, answer = self.request("GET", "/.token/v1/invalidtoken/", check=401)
|
_, headers, answer = self.request("GET", "/.token/v1/invalidtoken/", check=403)
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using token")
|
logging.info("\n*** fetch collection using token")
|
||||||
_, headers, answer = self.request("GET", token, check=200)
|
_, headers, answer = self.request("GET", token, check=200)
|
||||||
@@ -846,7 +880,7 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert "Status='success'" in answer
|
assert "Status='success'" in answer
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using disabled token")
|
logging.info("\n*** fetch collection using disabled token")
|
||||||
_, headers, answer = self.request("GET", token, check=401)
|
_, headers, answer = self.request("GET", token, check=403)
|
||||||
|
|
||||||
logging.info("\n*** enable token (form->text)")
|
logging.info("\n*** enable token (form->text)")
|
||||||
form_array = ["PathOrToken=" + token]
|
form_array = ["PathOrToken=" + token]
|
||||||
@@ -877,12 +911,15 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
_, headers, answer = self._sharing_api_form("token", "delete", check=404, login="owner:ownerpw", form_array=form_array)
|
_, headers, answer = self._sharing_api_form("token", "delete", check=404, login="owner:ownerpw", form_array=form_array)
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using deleted token")
|
logging.info("\n*** fetch collection using deleted token")
|
||||||
_, headers, answer = self.request("GET", token, check=401)
|
_, headers, answer = self.request("GET", token, check=403)
|
||||||
|
|
||||||
def test_sharing_api_token_usage_delay(self) -> None:
|
def test_sharing_api_token_usage_delay(self) -> None:
|
||||||
"""share-by-token API tests - real usage."""
|
"""share-by-token API tests - real usage."""
|
||||||
delay = .3
|
delay = .3
|
||||||
delay_ns = delay * 10**9 * 0.5 # delay minimum jitter
|
delay_min = delay * 0.9 # no random jitter during test
|
||||||
|
delay_max = delay + 0.2 # no random jitter during test
|
||||||
|
if sys.platform == "darwin": # no reliable sleep times
|
||||||
|
delay_max = delay_max * 1.5
|
||||||
|
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
"delay": delay,
|
"delay": delay,
|
||||||
@@ -930,16 +967,19 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert False
|
assert False
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using invalid token")
|
logging.info("\n*** fetch collection using invalid token")
|
||||||
time_ns_begin = time.time_ns()
|
time_begin = datetime.datetime.now()
|
||||||
_, headers, answer = self.request("GET", "/.token/v1/invalidtoken/", check=401)
|
_, headers, answer = self.request("GET", "/.token/v1/invalidtoken/", check=403)
|
||||||
time_ns_end = time.time_ns()
|
time_end = datetime.datetime.now()
|
||||||
assert (time_ns_end - time_ns_begin) > delay_ns
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using token")
|
logging.info("\n*** fetch collection using token")
|
||||||
time_ns_begin = time.time_ns()
|
time_begin = datetime.datetime.now()
|
||||||
_, headers, answer = self.request("GET", token, check=200)
|
_, headers, answer = self.request("GET", token, check=200)
|
||||||
time_ns_end = time.time_ns()
|
time_end = datetime.datetime.now()
|
||||||
assert (time_ns_end - time_ns_begin) < delay_ns
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta < delay_min # no delay
|
||||||
assert "UID:event" in answer
|
assert "UID:event" in answer
|
||||||
|
|
||||||
logging.info("\n*** disable token (form->text)")
|
logging.info("\n*** disable token (form->text)")
|
||||||
@@ -948,10 +988,12 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert "Status='success'" in answer
|
assert "Status='success'" in answer
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using disabled token")
|
logging.info("\n*** fetch collection using disabled token")
|
||||||
time_ns_begin = time.time_ns()
|
time_begin = datetime.datetime.now()
|
||||||
_, headers, answer = self.request("GET", token, check=401)
|
_, headers, answer = self.request("GET", token, check=403)
|
||||||
time_ns_end = time.time_ns()
|
time_end = datetime.datetime.now()
|
||||||
assert (time_ns_end - time_ns_begin) > delay_ns
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
logging.info("\n*** delete token (json->json)")
|
logging.info("\n*** delete token (json->json)")
|
||||||
json_dict = {'PathOrToken': token}
|
json_dict = {'PathOrToken': token}
|
||||||
@@ -961,10 +1003,12 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert answer_dict['Status'] == "success"
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
logging.info("\n*** fetch collection using deleted token with delay")
|
logging.info("\n*** fetch collection using deleted token with delay")
|
||||||
time_ns_begin = time.time_ns()
|
time_begin = datetime.datetime.now()
|
||||||
_, headers, answer = self.request("GET", token, check=401)
|
_, headers, answer = self.request("GET", token, check=403)
|
||||||
time_ns_end = time.time_ns()
|
time_end = datetime.datetime.now()
|
||||||
assert (time_ns_end - time_ns_begin) > delay_ns
|
time_delta = (time_end - time_begin).total_seconds()
|
||||||
|
assert time_delta > delay_min
|
||||||
|
assert time_delta < delay_max
|
||||||
|
|
||||||
def test_sharing_api_map_basic(self) -> None:
|
def test_sharing_api_map_basic(self) -> None:
|
||||||
"""share-by-map API basic tests."""
|
"""share-by-map API basic tests."""
|
||||||
|
|||||||
Reference in New Issue
Block a user