1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00
Radicale/radicale/server.py

288 lines
10 KiB
Python
Raw Normal View History

2018-08-16 08:00:00 +02:00
# This file is part of Radicale Server - Calendar Server
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
2018-09-04 03:33:47 +02:00
# Copyright © 2017-2018 Unrud<unrud@outlook.com>
2018-08-16 08:00:00 +02:00
#
# 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/>.
"""
Radicale WSGI server.
"""
import contextlib
import multiprocessing
2018-08-16 08:00:00 +02:00
import os
import select
import socket
import socketserver
import ssl
import sys
import wsgiref.simple_server
2018-09-04 03:33:36 +02:00
from configparser import ConfigParser
2018-08-16 08:00:00 +02:00
from urllib.parse import unquote
from radicale import Application
from radicale.log import logger
if hasattr(socketserver, "ForkingMixIn"):
ParallelizationMixIn = socketserver.ForkingMixIn
else:
ParallelizationMixIn = socketserver.ThreadingMixIn
2018-08-16 08:00:00 +02:00
class ParallelHTTPServer(ParallelizationMixIn,
wsgiref.simple_server.WSGIServer):
2018-08-16 08:00:00 +02:00
# These class attributes must be set before creating instance
client_timeout = None
max_connections = None
def __init__(self, address, handler, bind_and_activate=True):
"""Create server."""
ipv6 = ":" in address[0]
if ipv6:
self.address_family = socket.AF_INET6
# Do not bind and activate, as we might change socket options
super().__init__(address, handler, False)
if ipv6:
# Only allow IPv6 connections to the IPv6 socket
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
if self.max_connections:
self.connections_guard = multiprocessing.BoundedSemaphore(
2018-08-16 08:00:00 +02:00
self.max_connections)
else:
# use dummy context manager
self.connections_guard = contextlib.ExitStack()
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except BaseException:
self.server_close()
raise
def get_request(self):
# Set timeout for client
socket_, address = super().get_request()
2018-08-16 08:00:00 +02:00
if self.client_timeout:
socket_.settimeout(self.client_timeout)
return socket_, address
2018-09-04 03:33:43 +02:00
def finish_request_locked(self, request, client_address):
return super().finish_request(request, client_address)
def finish_request(self, request, client_address):
with self.connections_guard:
2018-09-04 03:33:43 +02:00
return self.finish_request_locked(request, client_address)
2018-08-16 08:00:00 +02:00
def handle_error(self, request, client_address):
if issubclass(sys.exc_info()[0], socket.timeout):
logger.info("client timed out", exc_info=True)
else:
logger.error("An exception occurred during request: %s",
sys.exc_info()[1], exc_info=True)
class ParallelHTTPSServer(ParallelHTTPServer):
2018-08-16 08:00:00 +02:00
# These class attributes must be set before creating instance
certificate = None
key = None
protocol = None
ciphers = None
certificate_authority = None
def __init__(self, address, handler, bind_and_activate=True):
2018-08-16 08:00:00 +02:00
"""Create server by wrapping HTTP socket in an SSL socket."""
# Do not bind and activate, as we change the socket
super().__init__(address, handler, False)
2018-08-16 08:00:00 +02:00
self.socket = ssl.wrap_socket(
self.socket, self.key, self.certificate, server_side=True,
cert_reqs=ssl.CERT_REQUIRED if self.certificate_authority else
ssl.CERT_NONE,
ca_certs=self.certificate_authority or None,
ssl_version=self.protocol, ciphers=self.ciphers,
do_handshake_on_connect=False)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except BaseException:
self.server_close()
raise
2018-08-16 08:00:00 +02:00
def finish_request(self, request, client_address):
2018-09-04 03:33:43 +02:00
with self.connections_guard:
2018-08-16 08:00:00 +02:00
try:
2018-09-04 03:33:43 +02:00
try:
request.do_handshake()
except socket.timeout:
raise
except Exception as e:
raise RuntimeError("SSL handshake failed: %s" % e) from e
except Exception:
try:
self.handle_error(request, client_address)
finally:
self.shutdown_request(request)
return
return super().finish_request_locked(request, client_address)
2018-08-16 08:00:00 +02:00
class ServerHandler(wsgiref.simple_server.ServerHandler):
# Don't pollute WSGI environ with OS environment
os_environ = {}
def log_exception(self, exc_info):
logger.error("An exception occurred during request: %s",
exc_info[1], exc_info=exc_info)
class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
"""HTTP requests handler."""
def log_request(self, code="-", size="-"):
"""Disable request logging."""
def log_error(self, format, *args):
msg = format % args
logger.error("An error occurred during request: %s" % msg)
def get_environ(self):
env = super().get_environ()
if hasattr(self.connection, "getpeercert"):
# The certificate can be evaluated by the auth module
env["REMOTE_CERTIFICATE"] = self.connection.getpeercert()
# Parent class only tries latin1 encoding
env["PATH_INFO"] = unquote(self.path.split("?", 1)[0])
return env
def handle(self):
"""Copy of WSGIRequestHandler.handle with different ServerHandler"""
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
2018-08-18 12:56:40 +02:00
self.requestline = ""
self.request_version = ""
self.command = ""
2018-08-16 08:00:00 +02:00
self.send_error(414)
return
if not self.parse_request():
return
handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
handler.request_handler = self
handler.run(self.server.get_app())
2018-09-04 03:33:45 +02:00
def serve(configuration, shutdown_socket=None):
2018-08-16 08:00:00 +02:00
"""Serve radicale from configuration."""
logger.info("Starting Radicale")
2018-09-04 03:33:36 +02:00
# Copy configuration before modifying
config_copy = ConfigParser()
config_copy.read_dict(configuration)
configuration = config_copy
2018-08-18 12:56:38 +02:00
configuration["internal"]["internal_server"] = "True"
2018-08-16 08:00:00 +02:00
# Create collection servers
servers = {}
if configuration.getboolean("server", "ssl"):
server_class = ParallelHTTPSServer
2018-08-28 16:19:49 +02:00
else:
server_class = ParallelHTTPServer
class ServerCopy(server_class):
"""Copy, avoids overriding the original class attributes."""
ServerCopy.client_timeout = configuration.getint("server", "timeout")
ServerCopy.max_connections = configuration.getint(
"server", "max_connections")
if configuration.getboolean("server", "ssl"):
ServerCopy.certificate = configuration.get("server", "certificate")
ServerCopy.key = configuration.get("server", "key")
ServerCopy.certificate_authority = configuration.get(
2018-08-16 08:00:00 +02:00
"server", "certificate_authority")
2018-08-28 16:19:49 +02:00
ServerCopy.ciphers = configuration.get("server", "ciphers")
ServerCopy.protocol = getattr(
2018-08-16 08:00:00 +02:00
ssl, configuration.get("server", "protocol"), ssl.PROTOCOL_SSLv23)
# Test if the SSL files can be read
for name in ["certificate", "key"] + (
["certificate_authority"]
2018-08-28 16:19:49 +02:00
if ServerCopy.certificate_authority else []):
filename = getattr(ServerCopy, name)
2018-08-16 08:00:00 +02:00
try:
open(filename, "r").close()
except OSError as e:
raise RuntimeError("Failed to read SSL %s %r: %s" %
(name, filename, e)) from e
2018-08-28 16:19:49 +02:00
class RequestHandlerCopy(RequestHandler):
"""Copy, avoids overriding the original class attributes."""
2018-08-16 08:00:00 +02:00
if not configuration.getboolean("server", "dns_lookup"):
2018-08-28 16:19:49 +02:00
RequestHandlerCopy.address_string = lambda self: self.client_address[0]
2018-08-16 08:00:00 +02:00
for host in configuration.get("server", "hosts").split(","):
try:
address, port = host.strip().rsplit(":", 1)
address, port = address.strip("[] "), int(port)
except ValueError as e:
raise RuntimeError(
"Failed to parse address %r: %s" % (host, e)) from e
2018-08-18 12:56:38 +02:00
application = Application(configuration)
2018-08-16 08:00:00 +02:00
try:
server = wsgiref.simple_server.make_server(
2018-08-28 16:19:49 +02:00
address, port, application, ServerCopy, RequestHandlerCopy)
2018-08-16 08:00:00 +02:00
except OSError as e:
raise RuntimeError(
"Failed to start server %r: %s" % (host, e)) from e
servers[server.socket] = server
logger.info("Listening to %r on port %d%s",
server.server_name, server.server_port, " using SSL"
if configuration.getboolean("server", "ssl") else "")
# Main loop: wait for requests on any of the servers or program shutdown
sockets = list(servers.keys())
2018-08-16 08:00:02 +02:00
# Use socket pair to get notified of program shutdown
2018-09-04 03:33:45 +02:00
if shutdown_socket:
sockets.append(shutdown_socket)
2018-08-16 08:00:00 +02:00
select_timeout = None
2018-08-16 08:00:02 +02:00
if os.name == "nt":
2018-08-16 08:00:00 +02:00
# Fallback to busy waiting. (select.select blocks SIGINT on Windows.)
select_timeout = 1.0
logger.info("Radicale server ready")
2018-09-04 03:33:45 +02:00
while True:
rlist, _, xlist = select.select(sockets, [], sockets, select_timeout)
2018-08-16 08:00:00 +02:00
if xlist:
raise RuntimeError("unhandled socket error")
2018-09-04 03:33:45 +02:00
if shutdown_socket in rlist:
logger.info("Stopping Radicale")
break
2018-08-16 08:00:00 +02:00
if rlist:
server = servers.get(rlist[0])
if server:
server.handle_request()