test_server.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2018-2019 Unrud <unrud@outlook.com>
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Test the internal server.
  18. """
  19. import errno
  20. import os
  21. import shutil
  22. import socket
  23. import ssl
  24. import subprocess
  25. import sys
  26. import tempfile
  27. import threading
  28. import time
  29. from configparser import RawConfigParser
  30. from urllib import request
  31. from urllib.error import HTTPError, URLError
  32. import pytest
  33. from radicale import config, server
  34. from radicale.tests import BaseTest
  35. from radicale.tests.helpers import configuration_to_dict, get_file_path
  36. class DisabledRedirectHandler(request.HTTPRedirectHandler):
  37. def http_error_302(self, req, fp, code, msg, headers):
  38. raise HTTPError(req.full_url, code, msg, headers, fp)
  39. http_error_301 = http_error_303 = http_error_307 = http_error_302
  40. class TestBaseServerRequests(BaseTest):
  41. """Test the internal server."""
  42. def setup(self):
  43. self.configuration = config.load()
  44. self.colpath = tempfile.mkdtemp()
  45. self.shutdown_socket, shutdown_socket_out = socket.socketpair()
  46. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
  47. # Find available port
  48. sock.bind(("127.0.0.1", 0))
  49. self.sockname = sock.getsockname()
  50. self.configuration.update({
  51. "storage": {"filesystem_folder": self.colpath,
  52. # Disable syncing to disk for better performance
  53. "_filesystem_fsync": "False"},
  54. "server": {"hosts": "[%s]:%d" % self.sockname},
  55. # Enable debugging for new processes
  56. "logging": {"level": "debug"}},
  57. "test", privileged=True)
  58. self.thread = threading.Thread(target=server.serve, args=(
  59. self.configuration, shutdown_socket_out))
  60. ssl_context = ssl.create_default_context()
  61. ssl_context.check_hostname = False
  62. ssl_context.verify_mode = ssl.CERT_NONE
  63. self.opener = request.build_opener(
  64. request.HTTPSHandler(context=ssl_context),
  65. DisabledRedirectHandler)
  66. def teardown(self):
  67. self.shutdown_socket.close()
  68. try:
  69. self.thread.join()
  70. except RuntimeError: # Thread never started
  71. pass
  72. shutil.rmtree(self.colpath)
  73. def request(self, method, path, data=None, is_alive_fn=None, **headers):
  74. """Send a request."""
  75. if is_alive_fn is None:
  76. is_alive_fn = self.thread.is_alive
  77. scheme = ("https" if self.configuration.get("server", "ssl") else
  78. "http")
  79. req = request.Request(
  80. "%s://[%s]:%d%s" % (scheme, *self.sockname, path),
  81. data=data, headers=headers, method=method)
  82. while True:
  83. assert is_alive_fn()
  84. try:
  85. with self.opener.open(req) as f:
  86. return f.getcode(), f.info(), f.read().decode()
  87. except HTTPError as e:
  88. return e.code, e.headers, e.read().decode()
  89. except URLError as e:
  90. if not isinstance(e.reason, ConnectionRefusedError):
  91. raise
  92. time.sleep(0.1)
  93. def test_root(self):
  94. self.thread.start()
  95. self.get("/", check=302)
  96. def test_ssl(self):
  97. self.configuration.update({
  98. "server": {"ssl": "True",
  99. "certificate": get_file_path("cert.pem"),
  100. "key": get_file_path("key.pem")}}, "test")
  101. self.thread.start()
  102. self.get("/", check=302)
  103. def test_bind_fail(self):
  104. for address_family, address in [(socket.AF_INET, "::1"),
  105. (socket.AF_INET6, "127.0.0.1")]:
  106. with socket.socket(address_family, socket.SOCK_STREAM) as sock:
  107. if address_family == socket.AF_INET6:
  108. # Only allow IPv6 connections to the IPv6 socket
  109. sock.setsockopt(server.COMPAT_IPPROTO_IPV6,
  110. socket.IPV6_V6ONLY, 1)
  111. with pytest.raises(OSError) as exc_info:
  112. sock.bind((address, 0))
  113. # See ``radicale.server.serve``
  114. assert (isinstance(exc_info.value, socket.gaierror) and
  115. exc_info.value.errno in (socket.EAI_NONAME,
  116. server.COMPAT_EAI_ADDRFAMILY) or
  117. str(exc_info.value) == "address family mismatched" or
  118. exc_info.value.errno == errno.EADDRNOTAVAIL)
  119. def test_ipv6(self):
  120. with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
  121. # Only allow IPv6 connections to the IPv6 socket
  122. sock.setsockopt(server.COMPAT_IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  123. try:
  124. # Find available port
  125. sock.bind(("::1", 0))
  126. except OSError as e:
  127. if e.errno == errno.EADDRNOTAVAIL:
  128. pytest.skip("IPv6 not supported")
  129. raise
  130. self.sockname = sock.getsockname()[:2]
  131. self.configuration.update({
  132. "server": {"hosts": "[%s]:%d" % self.sockname}}, "test")
  133. self.thread.start()
  134. self.get("/", check=302)
  135. def test_command_line_interface(self):
  136. config_args = []
  137. for section, values in config.DEFAULT_CONFIG_SCHEMA.items():
  138. if section.startswith("_"):
  139. continue
  140. for option, data in values.items():
  141. if option.startswith("_"):
  142. continue
  143. long_name = "--%s-%s" % (section, option.replace("_", "-"))
  144. if data["type"] == bool:
  145. if not self.configuration.get(section, option):
  146. long_name = "--no%s" % long_name[1:]
  147. config_args.append(long_name)
  148. else:
  149. config_args.append(long_name)
  150. config_args.append(
  151. self.configuration.get_raw(section, option))
  152. env = os.environ.copy()
  153. env["PYTHONPATH"] = os.pathsep.join(sys.path)
  154. p = subprocess.Popen(
  155. [sys.executable, "-m", "radicale"] + config_args, env=env)
  156. try:
  157. self.get("/", is_alive_fn=lambda: p.poll() is None, check=302)
  158. finally:
  159. p.terminate()
  160. p.wait()
  161. if os.name == "posix":
  162. assert p.returncode == 0
  163. def test_wsgi_server(self):
  164. config_path = os.path.join(self.colpath, "config")
  165. parser = RawConfigParser()
  166. parser.read_dict(configuration_to_dict(self.configuration))
  167. with open(config_path, "w") as f:
  168. parser.write(f)
  169. env = os.environ.copy()
  170. env["PYTHONPATH"] = os.pathsep.join(sys.path)
  171. env["RADICALE_CONFIG"] = config_path
  172. p = subprocess.Popen([
  173. sys.executable, "-m", "waitress",
  174. "--listen", self.configuration.get_raw("server", "hosts"),
  175. "radicale:application"], env=env)
  176. try:
  177. self.get("/", is_alive_fn=lambda: p.poll() is None, check=302)
  178. finally:
  179. p.terminate()
  180. p.wait()