commit
5c50cae9f2
@ -23,10 +23,30 @@ Give a configparser-like interface to read and write configuration.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from configparser import RawConfigParser as ConfigParser
|
from configparser import RawConfigParser as ConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
def positive_int(value):
|
||||||
|
value = int(value)
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("value is negative: %d" % value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def positive_float(value):
|
||||||
|
value = float(value)
|
||||||
|
if not math.isfinite(value):
|
||||||
|
raise ValueError("value is infinite")
|
||||||
|
if math.isnan(value):
|
||||||
|
raise ValueError("value is not a number")
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("value is negative: %f" % value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Default configuration
|
# Default configuration
|
||||||
INITIAL_CONFIG = OrderedDict([
|
INITIAL_CONFIG = OrderedDict([
|
||||||
("server", OrderedDict([
|
("server", OrderedDict([
|
||||||
@ -49,15 +69,15 @@ INITIAL_CONFIG = OrderedDict([
|
|||||||
("max_connections", {
|
("max_connections", {
|
||||||
"value": "20",
|
"value": "20",
|
||||||
"help": "maximum number of parallel connections",
|
"help": "maximum number of parallel connections",
|
||||||
"type": int}),
|
"type": positive_int}),
|
||||||
("max_content_length", {
|
("max_content_length", {
|
||||||
"value": "10000000",
|
"value": "10000000",
|
||||||
"help": "maximum size of request body in bytes",
|
"help": "maximum size of request body in bytes",
|
||||||
"type": int}),
|
"type": positive_int}),
|
||||||
("timeout", {
|
("timeout", {
|
||||||
"value": "10",
|
"value": "10",
|
||||||
"help": "socket timeout",
|
"help": "socket timeout",
|
||||||
"type": int}),
|
"type": positive_int}),
|
||||||
("ssl", {
|
("ssl", {
|
||||||
"value": "False",
|
"value": "False",
|
||||||
"help": "use SSL connection",
|
"help": "use SSL connection",
|
||||||
@ -101,7 +121,7 @@ INITIAL_CONFIG = OrderedDict([
|
|||||||
"type": str})])),
|
"type": str})])),
|
||||||
("auth", OrderedDict([
|
("auth", OrderedDict([
|
||||||
("type", {
|
("type", {
|
||||||
"value": "None",
|
"value": "none",
|
||||||
"help": "authentication method",
|
"help": "authentication method",
|
||||||
"type": str}),
|
"type": str}),
|
||||||
("htpasswd_filename", {
|
("htpasswd_filename", {
|
||||||
@ -115,7 +135,7 @@ INITIAL_CONFIG = OrderedDict([
|
|||||||
("delay", {
|
("delay", {
|
||||||
"value": "1",
|
"value": "1",
|
||||||
"help": "incorrect authentication delay",
|
"help": "incorrect authentication delay",
|
||||||
"type": float})])),
|
"type": positive_float})])),
|
||||||
("rights", OrderedDict([
|
("rights", OrderedDict([
|
||||||
("type", {
|
("type", {
|
||||||
"value": "owner_only",
|
"value": "owner_only",
|
||||||
@ -212,6 +232,7 @@ def load(paths=(), extra_config=None, ignore_missing_paths=True):
|
|||||||
type_(config.get(section, option))
|
type_(config.get(section, option))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Invalid value %r for option %r in section %r in config" %
|
"Invalid %s value for option %r in section %r in config: "
|
||||||
(config.get(section, option), option, section)) from e
|
"%r" % (type_.__name__, option, section,
|
||||||
|
config.get(section, option))) from e
|
||||||
return config
|
return config
|
||||||
|
@ -24,7 +24,6 @@ http://docs.python.org/library/logging.config.html
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -47,26 +46,30 @@ class RemoveTracebackFilter(logging.Filter):
|
|||||||
def start(name="radicale", filename=None, debug=False):
|
def start(name="radicale", filename=None, debug=False):
|
||||||
"""Start the logging according to the configuration."""
|
"""Start the logging according to the configuration."""
|
||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
if filename and os.path.exists(filename):
|
|
||||||
# Configuration taken from file
|
|
||||||
configure_from_file(logger, filename, debug)
|
|
||||||
# Reload config on SIGHUP (UNIX only)
|
|
||||||
if hasattr(signal, "SIGHUP"):
|
|
||||||
def handler(signum, frame):
|
|
||||||
configure_from_file(logger, filename, debug)
|
|
||||||
signal.signal(signal.SIGHUP, handler)
|
|
||||||
else:
|
|
||||||
# Default configuration, standard output
|
|
||||||
if filename:
|
|
||||||
logger.warning(
|
|
||||||
"WARNING: Logging configuration file %r not found, using "
|
|
||||||
"stderr" % filename)
|
|
||||||
handler = logging.StreamHandler(sys.stderr)
|
|
||||||
handler.setFormatter(
|
|
||||||
logging.Formatter("[%(thread)x] %(levelname)s: %(message)s"))
|
|
||||||
logger.addHandler(handler)
|
|
||||||
if debug:
|
if debug:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
logger.addFilter(RemoveTracebackFilter())
|
logger.addFilter(RemoveTracebackFilter())
|
||||||
|
if filename:
|
||||||
|
# Configuration taken from file
|
||||||
|
try:
|
||||||
|
configure_from_file(logger, filename, debug)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError("Failed to load logging configuration file %r: "
|
||||||
|
"%s" % (filename, e)) from e
|
||||||
|
# Reload config on SIGHUP (UNIX only)
|
||||||
|
if hasattr(signal, "SIGHUP"):
|
||||||
|
def handler(signum, frame):
|
||||||
|
try:
|
||||||
|
configure_from_file(logger, filename, debug)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to reload logging configuration file "
|
||||||
|
"%r: %s", filename, e, exc_info=True)
|
||||||
|
signal.signal(signal.SIGHUP, handler)
|
||||||
|
else:
|
||||||
|
# Default configuration, standard output
|
||||||
|
handler = logging.StreamHandler(sys.stderr)
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("[%(thread)x] %(levelname)s: %(message)s"))
|
||||||
|
logger.addHandler(handler)
|
||||||
return logger
|
return logger
|
||||||
|
@ -40,13 +40,13 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
def setup(self):
|
def setup(self):
|
||||||
self.configuration = config.load()
|
self.configuration = config.load()
|
||||||
self.colpath = tempfile.mkdtemp()
|
self.colpath = tempfile.mkdtemp()
|
||||||
self.configuration.set("storage", "filesystem_folder", self.colpath)
|
self.configuration["storage"]["filesystem_folder"] = self.colpath
|
||||||
# Disable syncing to disk for better performance
|
# Disable syncing to disk for better performance
|
||||||
self.configuration.set("storage", "filesystem_fsync", "False")
|
self.configuration["storage"]["filesystem_fsync"] = "False"
|
||||||
# Required on Windows, doesn't matter on Unix
|
# Required on Windows, doesn't matter on Unix
|
||||||
self.configuration.set("storage", "filesystem_close_lock_file", "True")
|
self.configuration["storage"]["filesystem_close_lock_file"] = "True"
|
||||||
# Set incorrect authentication delay to a very low value
|
# Set incorrect authentication delay to a very low value
|
||||||
self.configuration.set("auth", "delay", "0.002")
|
self.configuration["auth"]["delay"] = "0.002"
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
shutil.rmtree(self.colpath)
|
shutil.rmtree(self.colpath)
|
||||||
@ -56,10 +56,9 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
htpasswd_file_path = os.path.join(self.colpath, ".htpasswd")
|
htpasswd_file_path = os.path.join(self.colpath, ".htpasswd")
|
||||||
with open(htpasswd_file_path, "w") as f:
|
with open(htpasswd_file_path, "w") as f:
|
||||||
f.write(htpasswd_content)
|
f.write(htpasswd_content)
|
||||||
self.configuration.set("auth", "type", "htpasswd")
|
self.configuration["auth"]["type"] = "htpasswd"
|
||||||
self.configuration.set("auth", "htpasswd_filename", htpasswd_file_path)
|
self.configuration["auth"]["htpasswd_filename"] = htpasswd_file_path
|
||||||
self.configuration.set("auth", "htpasswd_encryption",
|
self.configuration["auth"]["htpasswd_encryption"] = htpasswd_encryption
|
||||||
htpasswd_encryption)
|
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
for user, password, expected_status in (
|
for user, password, expected_status in (
|
||||||
("tmp", "bepo", 207), ("tmp", "tmp", 401), ("tmp", "", 401),
|
("tmp", "bepo", 207), ("tmp", "tmp", 401), ("tmp", "", 401),
|
||||||
@ -108,7 +107,7 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
"tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq")
|
"tmp:$2y$05$oD7hbiQFQlvCM7zoalo/T.MssV3VNTRI3w5KDnj8NTUKJNWfVpvRq")
|
||||||
|
|
||||||
def test_remote_user(self):
|
def test_remote_user(self):
|
||||||
self.configuration.set("auth", "type", "remote_user")
|
self.configuration["auth"]["type"] = "remote_user"
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
status, _, answer = self.request(
|
status, _, answer = self.request(
|
||||||
"PROPFIND", "/",
|
"PROPFIND", "/",
|
||||||
@ -122,7 +121,7 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
assert ">/test/<" in answer
|
assert ">/test/<" in answer
|
||||||
|
|
||||||
def test_http_x_remote_user(self):
|
def test_http_x_remote_user(self):
|
||||||
self.configuration.set("auth", "type", "http_x_remote_user")
|
self.configuration["auth"]["type"] = "http_x_remote_user"
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
status, _, answer = self.request(
|
status, _, answer = self.request(
|
||||||
"PROPFIND", "/",
|
"PROPFIND", "/",
|
||||||
@ -137,7 +136,7 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
|
|
||||||
def test_custom(self):
|
def test_custom(self):
|
||||||
"""Custom authentication."""
|
"""Custom authentication."""
|
||||||
self.configuration.set("auth", "type", "tests.custom.auth")
|
self.configuration["auth"]["type"] = "tests.custom.auth"
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
status, headers, answer = self.request(
|
status, headers, answer = self.request(
|
||||||
"PROPFIND", "/tmp", HTTP_AUTHORIZATION="Basic %s" %
|
"PROPFIND", "/tmp", HTTP_AUTHORIZATION="Basic %s" %
|
||||||
|
@ -779,10 +779,10 @@ class BaseRequestsMixIn:
|
|||||||
|
|
||||||
def test_authentication(self):
|
def test_authentication(self):
|
||||||
"""Test if server sends authentication request."""
|
"""Test if server sends authentication request."""
|
||||||
self.configuration.set("auth", "type", "htpasswd")
|
self.configuration["auth"]["type"] = "htpasswd"
|
||||||
self.configuration.set("auth", "htpasswd_filename", os.devnull)
|
self.configuration["auth"]["htpasswd_filename"] = os.devnull
|
||||||
self.configuration.set("auth", "htpasswd_encryption", "plain")
|
self.configuration["auth"]["htpasswd_encryption"] = "plain"
|
||||||
self.configuration.set("rights", "type", "owner_only")
|
self.configuration["rights"]["type"] = "owner_only"
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
status, headers, answer = self.request("MKCOL", "/user/")
|
status, headers, answer = self.request("MKCOL", "/user/")
|
||||||
assert status in (401, 403)
|
assert status in (401, 403)
|
||||||
@ -791,7 +791,8 @@ class BaseRequestsMixIn:
|
|||||||
def test_principal_collection_creation(self):
|
def test_principal_collection_creation(self):
|
||||||
"""Verify existence of the principal collection."""
|
"""Verify existence of the principal collection."""
|
||||||
status, headers, answer = self.request(
|
status, headers, answer = self.request(
|
||||||
"PROPFIND", "/user/", REMOTE_USER="user")
|
"PROPFIND", "/user/", HTTP_AUTHORIZATION=(
|
||||||
|
"Basic " + base64.b64encode(b"user:").decode()))
|
||||||
assert status == 207
|
assert status == 207
|
||||||
|
|
||||||
def test_existence_of_root_collections(self):
|
def test_existence_of_root_collections(self):
|
||||||
@ -806,15 +807,14 @@ class BaseRequestsMixIn:
|
|||||||
|
|
||||||
def test_fsync(self):
|
def test_fsync(self):
|
||||||
"""Create a directory and file with syncing enabled."""
|
"""Create a directory and file with syncing enabled."""
|
||||||
self.configuration.set("storage", "filesystem_fsync", "True")
|
self.configuration["storage"]["filesystem_fsync"] = "True"
|
||||||
status, headers, answer = self.request("MKCALENDAR", "/calendar.ics/")
|
status, headers, answer = self.request("MKCALENDAR", "/calendar.ics/")
|
||||||
assert status == 201
|
assert status == 201
|
||||||
|
|
||||||
def test_hook(self):
|
def test_hook(self):
|
||||||
"""Run hook."""
|
"""Run hook."""
|
||||||
self.configuration.set(
|
self.configuration["storage"]["hook"] = (
|
||||||
"storage", "hook", "mkdir %s" % os.path.join(
|
"mkdir %s" % os.path.join("collection-root", "created_by_hook"))
|
||||||
"collection-root", "created_by_hook"))
|
|
||||||
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
||||||
assert status == 201
|
assert status == 201
|
||||||
status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
|
status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
|
||||||
@ -822,9 +822,8 @@ class BaseRequestsMixIn:
|
|||||||
|
|
||||||
def test_hook_read_access(self):
|
def test_hook_read_access(self):
|
||||||
"""Verify that hook is not run for read accesses."""
|
"""Verify that hook is not run for read accesses."""
|
||||||
self.configuration.set(
|
self.configuration["storage"]["hook"] = (
|
||||||
"storage", "hook", "mkdir %s" % os.path.join(
|
"mkdir %s" % os.path.join("collection-root", "created_by_hook"))
|
||||||
"collection-root", "created_by_hook"))
|
|
||||||
status, headers, answer = self.request("GET", "/")
|
status, headers, answer = self.request("GET", "/")
|
||||||
assert status == 303
|
assert status == 303
|
||||||
status, headers, answer = self.request("GET", "/created_by_hook/")
|
status, headers, answer = self.request("GET", "/created_by_hook/")
|
||||||
@ -834,24 +833,25 @@ class BaseRequestsMixIn:
|
|||||||
reason="flock command not found")
|
reason="flock command not found")
|
||||||
def test_hook_storage_locked(self):
|
def test_hook_storage_locked(self):
|
||||||
"""Verify that the storage is locked when the hook runs."""
|
"""Verify that the storage is locked when the hook runs."""
|
||||||
self.configuration.set(
|
self.configuration["storage"]["hook"] = (
|
||||||
"storage", "hook", "flock -n .Radicale.lock || exit 0; exit 1")
|
"flock -n .Radicale.lock || exit 0; exit 1")
|
||||||
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
||||||
assert status == 201
|
assert status == 201
|
||||||
|
|
||||||
def test_hook_principal_collection_creation(self):
|
def test_hook_principal_collection_creation(self):
|
||||||
"""Verify that the hooks runs when a new user is created."""
|
"""Verify that the hooks runs when a new user is created."""
|
||||||
self.configuration.set(
|
self.configuration["storage"]["hook"] = (
|
||||||
"storage", "hook", "mkdir %s" % os.path.join(
|
"mkdir %s" % os.path.join("collection-root", "created_by_hook"))
|
||||||
"collection-root", "created_by_hook"))
|
status, headers, answer = self.request(
|
||||||
status, headers, answer = self.request("GET", "/", REMOTE_USER="user")
|
"GET", "/", HTTP_AUTHORIZATION=(
|
||||||
|
"Basic " + base64.b64encode(b"user:").decode()))
|
||||||
assert status == 303
|
assert status == 303
|
||||||
status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
|
status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
|
||||||
assert status == 207
|
assert status == 207
|
||||||
|
|
||||||
def test_hook_fail(self):
|
def test_hook_fail(self):
|
||||||
"""Verify that a request fails if the hook fails."""
|
"""Verify that a request fails if the hook fails."""
|
||||||
self.configuration.set("storage", "hook", "exit 1")
|
self.configuration["storage"]["hook"] = "exit 1"
|
||||||
try:
|
try:
|
||||||
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
||||||
assert status != 201
|
assert status != 201
|
||||||
@ -877,13 +877,13 @@ class BaseFileSystemTest(BaseTest):
|
|||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
self.configuration = config.load()
|
self.configuration = config.load()
|
||||||
self.configuration.set("storage", "type", self.storage_type)
|
self.configuration["storage"]["type"] = self.storage_type
|
||||||
self.colpath = tempfile.mkdtemp()
|
self.colpath = tempfile.mkdtemp()
|
||||||
self.configuration.set("storage", "filesystem_folder", self.colpath)
|
self.configuration["storage"]["filesystem_folder"] = self.colpath
|
||||||
# Disable syncing to disk for better performance
|
# Disable syncing to disk for better performance
|
||||||
self.configuration.set("storage", "filesystem_fsync", "False")
|
self.configuration["storage"]["filesystem_fsync"] = "False"
|
||||||
# Required on Windows, doesn't matter on Unix
|
# Required on Windows, doesn't matter on Unix
|
||||||
self.configuration.set("storage", "filesystem_close_lock_file", "True")
|
self.configuration["storage"]["filesystem_close_lock_file"] = "True"
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
|
@ -34,11 +34,11 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
def setup(self):
|
def setup(self):
|
||||||
self.configuration = config.load()
|
self.configuration = config.load()
|
||||||
self.colpath = tempfile.mkdtemp()
|
self.colpath = tempfile.mkdtemp()
|
||||||
self.configuration.set("storage", "filesystem_folder", self.colpath)
|
self.configuration["storage"]["filesystem_folder"] = self.colpath
|
||||||
# Disable syncing to disk for better performance
|
# Disable syncing to disk for better performance
|
||||||
self.configuration.set("storage", "filesystem_fsync", "False")
|
self.configuration["storage"]["filesystem_fsync"] = "False"
|
||||||
# Required on Windows, doesn't matter on Unix
|
# Required on Windows, doesn't matter on Unix
|
||||||
self.configuration.set("storage", "filesystem_close_lock_file", "True")
|
self.configuration["storage"]["filesystem_close_lock_file"] = "True"
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
shutil.rmtree(self.colpath)
|
shutil.rmtree(self.colpath)
|
||||||
@ -49,10 +49,10 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
htpasswd_file_path = os.path.join(self.colpath, ".htpasswd")
|
htpasswd_file_path = os.path.join(self.colpath, ".htpasswd")
|
||||||
with open(htpasswd_file_path, "w") as f:
|
with open(htpasswd_file_path, "w") as f:
|
||||||
f.write("tmp:bepo\nother:bepo")
|
f.write("tmp:bepo\nother:bepo")
|
||||||
self.configuration.set("rights", "type", rights_type)
|
self.configuration["rights"]["type"] = rights_type
|
||||||
self.configuration.set("auth", "type", "htpasswd")
|
self.configuration["auth"]["type"] = "htpasswd"
|
||||||
self.configuration.set("auth", "htpasswd_filename", htpasswd_file_path)
|
self.configuration["auth"]["htpasswd_filename"] = htpasswd_file_path
|
||||||
self.configuration.set("auth", "htpasswd_encryption", "plain")
|
self.configuration["auth"]["htpasswd_encryption"] = "plain"
|
||||||
self.application = Application(self.configuration, self.logger)
|
self.application = Application(self.configuration, self.logger)
|
||||||
for u in ("tmp", "other"):
|
for u in ("tmp", "other"):
|
||||||
status, _, _ = self.request(
|
status, _, _ = self.request(
|
||||||
@ -125,7 +125,7 @@ permission: rw
|
|||||||
user: .*
|
user: .*
|
||||||
collection: custom(/.*)?
|
collection: custom(/.*)?
|
||||||
permission: r""")
|
permission: r""")
|
||||||
self.configuration.set("rights", "file", rights_file_path)
|
self.configuration["rights"]["file"] = rights_file_path
|
||||||
self._test_rights("from_file", "", "/other", "r", 401)
|
self._test_rights("from_file", "", "/other", "r", 401)
|
||||||
self._test_rights("from_file", "tmp", "/other", "r", 403)
|
self._test_rights("from_file", "tmp", "/other", "r", 403)
|
||||||
self._test_rights("from_file", "", "/custom/sub", "r", 404)
|
self._test_rights("from_file", "", "/custom/sub", "r", 404)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user