Test internal server

This commit is contained in:
Unrud 2018-09-04 03:33:45 +02:00
parent 49d35cf618
commit 5a433f5476
6 changed files with 176 additions and 34 deletions

View File

@ -24,10 +24,11 @@ This module can be executed from a command line with ``$python -m radicale``.
import argparse
import os
import signal
import socket
from radicale import VERSION, config, log, storage
from radicale import VERSION, config, log, server, storage
from radicale.log import logger
from radicale.server import serve
def run():
@ -125,8 +126,17 @@ def run():
exit(1)
return
# Create a socket pair to notify the server of program shutdown
shutdown_socket, shutdown_socket_out = socket.socketpair()
# SIGTERM and SIGINT (aka KeyboardInterrupt) shutdown the server
def shutdown(*args):
shutdown_socket.sendall(b" ")
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
try:
serve(configuration)
server.serve(configuration, shutdown_socket_out)
except Exception as e:
logger.error("An exception occurred during server startup: %s", e,
exc_info=True)

View File

@ -26,7 +26,6 @@ import contextlib
import multiprocessing
import os
import select
import signal
import socket
import socketserver
import ssl
@ -201,7 +200,7 @@ class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
handler.run(self.server.get_app())
def serve(configuration):
def serve(configuration, shutdown_socket=None):
"""Serve radicale from configuration."""
logger.info("Starting Radicale")
# Copy configuration before modifying
@ -246,8 +245,6 @@ def serve(configuration):
if not configuration.getboolean("server", "dns_lookup"):
RequestHandlerCopy.address_string = lambda self: self.client_address[0]
shutdown_program = False
for host in configuration.get("server", "hosts").split(","):
try:
address, port = host.strip().rsplit(":", 1)
@ -267,41 +264,23 @@ def serve(configuration):
server.server_name, server.server_port, " using SSL"
if configuration.getboolean("server", "ssl") else "")
# Create a socket pair to notify the select syscall of program shutdown
shutdown_program_socket_in, shutdown_program_socket_out = (
socket.socketpair())
# SIGTERM and SIGINT (aka KeyboardInterrupt) should just mark this for
# shutdown
def shutdown(*args):
nonlocal shutdown_program
if shutdown_program:
# Ignore following signals
return
logger.info("Stopping Radicale")
shutdown_program = True
shutdown_program_socket_in.sendall(b" ")
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
# Main loop: wait for requests on any of the servers or program shutdown
sockets = list(servers.keys())
# Use socket pair to get notified of program shutdown
sockets.append(shutdown_program_socket_out)
if shutdown_socket:
sockets.append(shutdown_socket)
select_timeout = None
if os.name == "nt":
# Fallback to busy waiting. (select.select blocks SIGINT on Windows.)
select_timeout = 1.0
logger.info("Radicale server ready")
while not shutdown_program:
try:
rlist, _, xlist = select.select(
sockets, [], sockets, select_timeout)
except (KeyboardInterrupt, select.error):
# SIGINT is handled by signal handler above
rlist, xlist = [], []
while True:
rlist, _, xlist = select.select(sockets, [], sockets, select_timeout)
if xlist:
raise RuntimeError("unhandled socket error")
if shutdown_socket in rlist:
logger.info("Stopping Radicale")
break
if rlist:
server = servers.get(rlist[0])
if server:

View File

@ -29,10 +29,13 @@ import os
EXAMPLES_FOLDER = os.path.join(os.path.dirname(__file__), "static")
def get_file_path(file_name):
return os.path.join(EXAMPLES_FOLDER, file_name)
def get_file_content(file_name):
try:
with open(os.path.join(EXAMPLES_FOLDER, file_name),
encoding="utf-8") as fd:
with open(get_file_path(file_name), encoding="utf-8") as fd:
return fd.read()
except IOError:
print("Couldn't open the file %s" % file_name)

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDXDCCAkSgAwIBAgIJAKBsA+sXwPtuMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV
BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg
Q29tcGFueSBMdGQwIBcNMTgwOTAzMjAyNDE2WhgPMjExODA4MTAyMDI0MTZaMEIx
CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl
ZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQDMEBfr6oEk/t1Op9fSRRRrReQOZqx+gC1jHONSDXudDyfZBFSQx1QY9EtFqMUr
lvY3uI+rohujMTfXih6AEXTHHJmRIk80hDR/ovDMDiC5+z6EuKwbKPtjDMKqn7Hb
YoA4pyRWwzPydrZRVeG9+z4YY5uMRCmpzLqWcm04kgCEeJqKpb9ZQMKL/8fq8a9p
v5rfOXqtneje4yJAOF/L2EXk/MjdqvYR/cu2kTP8IDocTYZj6xjA9GVb37Xga+YG
u/SbGSU9vU8rmXJqqAFR/im97bz960Q/Q2VN2y9nTLEPCjGeyxcatxDw6vc1s2GE
5ttuu6aPmRc392T3kFV9ZnYdAgMBAAGjUzBRMB0GA1UdDgQWBBRKPvGgdpsYK/ma
3l+FMUIngO9xGTAfBgNVHSMEGDAWgBRKPvGgdpsYK/ma3l+FMUIngO9xGTAPBgNV
HRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCID4FTrX6DJKQzvDTg6ejP
ziSeoea7+nqtVogEBfmzm8YY4pu6qbNM8EHwbP9cnbZ6V48PmZUV4hQibGy33C6E
EIvqNBHcO/WqjbL2IWKcuZH7pMQVedR3GAV8sJMMwBOTtdopcTbnYFRZYwXV2dKe
reo5ukDZo8KyQHS9lloi5IPhsTufPBK3n9EtMa/Ch7bqmXEiSkKFU04o2kuj0Urk
hG8lnX1Ff2xWjG5N9Hp7xaEWk3LO/nDxlF/AmF3pDuWkZXpzNpUk70KlNx8xSKYR
cHmp2Z1hrA7PvUrG46I2dwC+y09hRXFSqYBT2po9Uzwj8aSNXGr1vKBzebqi9Sxc
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMEBfr6oEk/t1O
p9fSRRRrReQOZqx+gC1jHONSDXudDyfZBFSQx1QY9EtFqMUrlvY3uI+rohujMTfX
ih6AEXTHHJmRIk80hDR/ovDMDiC5+z6EuKwbKPtjDMKqn7HbYoA4pyRWwzPydrZR
VeG9+z4YY5uMRCmpzLqWcm04kgCEeJqKpb9ZQMKL/8fq8a9pv5rfOXqtneje4yJA
OF/L2EXk/MjdqvYR/cu2kTP8IDocTYZj6xjA9GVb37Xga+YGu/SbGSU9vU8rmXJq
qAFR/im97bz960Q/Q2VN2y9nTLEPCjGeyxcatxDw6vc1s2GE5ttuu6aPmRc392T3
kFV9ZnYdAgMBAAECggEAeQ7HEjbBPJBR+9qIp35Buc3xmDWC+VzTECxQExpajfcy
vYTbIjSOCGvMx9tydQSOtsmvubNmz+5f4WdX5sP0Ujb+R2JiOJaBioLAdV2gPpT1
JsljmI08bSthxNUOL0cFKBbH8QzGoX2ZdTEMxabp1JAq9BBv4wLIYn4pm1jKI8tU
bzqgx6OjS9bd/su0EPjksLs3pQUN/+f2O7ta6jgXnk68akDtICUq8ELiv2q2+zM1
pZ3npjR/Nc6CLcp9jCYnlQ5hwqJK1ZFXzMUGxpbMXc0rcppVCjR9Tu5ThC4qIPEE
tvDeXhy+j1XX1LV1dL2Nt4vTpLpd4xPthvfjxyJUgQKBgQD2x1kZvR3FJZMjXwpt
G4MUtVp2VUcGm6Q1790HruHrHFqD2zZpsfcLhyCcGlVt2lVrhVjUeZ1jwKuxAAfE
dO1KdTQF0cdMsHAoAkGairfwi4VGIL7PqIHBZXNUiSWY9p61ybZ8tABRv5edxwvK
qRdbId9x4ooeTK76H3+gWB19IQKBgQDTsCGkrgLMaiTBAc7Wf8xnpz9x6P2IGCgo
0jg7MKnHEE+Mx/MPn8TwEmB5a4Ldp5LlJ2mSkxm8BohtHvCVYyNZnilmIgXeZhbx
mEwKPe/carqGk36DozlZqhrx1n87jWmwO3kCNNyTv1aODwubdA0rO+hzpZXA7zi+
ADBLlr+9fQKBgEVH/BTEyjnR7bgNc6DkC23h6C62jEUnpvdZiuUgTN6zzBmejm0o
AGJlIluQ7RD1LewMuL6WEgCyU8FSb9vQs9mmg99qYJiAJEynLYHUlgVbNiRVBxzH
gv4nnDRMeJi0DCSfJ7Nk2X4Z2tf5zK6twBfer5uKbRpKjwk7lJoQgt7hAoGBALDm
fIbw/9exT/uWtjHcZIWuZz+a89v6S/0pB+K23PpEcCX2pfFFk78HrGVradYvhntH
P1tE4HmXgASomWZNjaoDmRcHkZ3z9HJ60fixH7Qz4KI7ubrp+TAsDg5RMMwkddDX
Ml2crUQu3ncirZGAHs0laDDUjFvJzcJByBoy5RLFAoGBAKFmic8xdYzHeQLurU/Z
8LPBHTLw1z/o4y5GK+kBGZArpENJTd89/y4FlCboLp5bPYtL2k85KYYGtXKgLN48
GZSFVGVGEir3q6lxUHFq49oj1uywQBSxrhe0ZByngP/0pwvcjqzg0hd8Oz+TmVrK
C3zzE6uYw/gVocCTX9xXIzoN
-----END PRIVATE KEY-----

View File

@ -0,0 +1,102 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2018 Unrud <unrud@outlook.com>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Test the internal server.
"""
import shutil
import socket
import ssl
import tempfile
import threading
import time
from urllib import request
from urllib.error import HTTPError, URLError
from radicale import config, server
from .helpers import get_file_path
class DisabledRedirectHandler(request.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
raise HTTPError(req.full_url, code, msg, headers, fp)
http_error_301 = http_error_303 = http_error_307 = http_error_302
class TestBaseServerRequests:
"""Test the internal server."""
def setup(self):
self.configuration = config.load()
self.colpath = tempfile.mkdtemp()
self.configuration["storage"]["filesystem_folder"] = self.colpath
# Disable syncing to disk for better performance
self.configuration["internal"]["filesystem_fsync"] = "False"
self.shutdown_socket, shutdown_socket_out = socket.socketpair()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# Find available port
sock.bind(("localhost", 0))
self.sockname = sock.getsockname()
self.configuration["server"]["hosts"] = "[%s]:%d" % self.sockname
self.thread = threading.Thread(target=server.serve, args=(
self.configuration, shutdown_socket_out))
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
self.opener = request.build_opener(
request.HTTPSHandler(context=ssl_context),
DisabledRedirectHandler)
def teardown(self):
self.shutdown_socket.sendall(b" ")
self.thread.join()
shutil.rmtree(self.colpath)
def request(self, method, path, data=None, **headers):
"""Send a request."""
scheme = ("https" if self.configuration.getboolean("server", "ssl")
else "http")
req = request.Request(
"%s://[%s]:%d%s" % (scheme, *self.sockname, path),
data=data, headers=headers, method=method)
while True:
assert self.thread.is_alive()
try:
with self.opener.open(req) as f:
return f.getcode(), f.info(), f.read().decode()
except HTTPError as e:
return e.code, e.headers, e.read().decode()
except URLError as e:
if not isinstance(e.reason, ConnectionRefusedError):
raise
time.sleep(0.1)
def test_root(self):
self.thread.start()
status, _, _ = self.request("GET", "/")
assert status == 302
def test_ssl(self):
self.configuration["server"]["ssl"] = "True"
self.configuration["server"]["certificate"] = get_file_path("cert.pem")
self.configuration["server"]["key"] = get_file_path("key.pem")
self.thread.start()
status, _, _ = self.request("GET", "/")
assert status == 302