#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Management Console
#  UMC web server
#
# Copyright 2011-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

from __future__ import division

import os
import re
import sys
import time
import uuid
import json
import zlib
import base64
import signal
import hashlib
import resource
import tempfile
import binascii
import datetime
import traceback
import functools
import threading
from errno import ESRCH
from optparse import OptionParser
from six.moves.urllib_parse import urlparse, urlunsplit
from six.moves.http_client import REQUEST_ENTITY_TOO_LARGE, LENGTH_REQUIRED, NOT_FOUND, BAD_REQUEST, UNAUTHORIZED, SERVICE_UNAVAILABLE

from defusedxml.common import DefusedXmlException
import six
from six.moves import queue as Queue
import notifier
import cherrypy
from ipaddress import ip_address
from cherrypy import HTTPError, HTTPRedirect, NotFound
from cherrypy.lib.httputil import valid_status
from daemon.runner import DaemonRunner, DaemonRunnerStopFailureError, DaemonRunnerStartFailureError

import univention.debug as ud
from univention.management.console.protocol import Request, Response, Client, NoSocketError, TEMPUPLOADDIR
from univention.management.console.log import CORE, log_init, log_reopen
from univention.management.console.config import ucr, get_int

from saml2 import BINDING_HTTP_POST, BINDING_HTTP_ARTIFACT, BINDING_HTTP_REDIRECT
from saml2.client import Saml2Client
from saml2.metadata import create_metadata_string
from saml2.response import VerificationError, UnsolicitedResponse, StatusError
from saml2.s_utils import UnknownPrincipal, UnsupportedBinding, rndstr
from saml2.sigver import MissingKey, SignatureError
from saml2.ident import code as encode_name_id, decode as decode_name_id
from saml2.validate import ResponseLifetimeExceed

from univention.lib.i18n import NullTranslation

try:
	from html import escape, unescape
except ImportError:  # Python 2
	import HTMLParser
	html_parser = HTMLParser.HTMLParser()
	unescape = html_parser.unescape
	from cgi import escape

try:
	from time import monotonic
except ImportError:
	from monotonic import monotonic

# the SameSite cookie attribute is only available from Python 3.8
from six.moves.http_cookies import Morsel
Morsel._reserved['samesite'] = 'SameSite'

_ = NullTranslation('univention-management-console-frontend').translate

_session_timeout = get_int('umc/http/session/timeout', 300)

PORT = None
REQUEST_ENTITY_TOO_LARGE, LENGTH_REQUIRED, NOT_FOUND, BAD_REQUEST, UNAUTHORIZED, SERVICE_UNAVAILABLE = int(REQUEST_ENTITY_TOO_LARGE), int(LENGTH_REQUIRED), int(NOT_FOUND), int(BAD_REQUEST), int(UNAUTHORIZED), int(SERVICE_UNAVAILABLE)


def sessionidhash():
	session = u'%s%s%s%s' % (cherrypy.request.headers.get('Authorization', ''), cherrypy.request.headers.get('Accept-Language', ''), get_ip_address(), sessionidhash.salt)
	return hashlib.sha256(session.encode('UTF-8')).hexdigest()[:36]
	# TODO: the following is more secure (real random) but also much slower
	return binascii.hexlify(hashlib.pbkdf2_hmac('sha256', session, sessionidhash.salt, 100000))[:36]


sessionidhash.salt = rndstr()


def log_exceptions(func):
	@functools.wraps(func)
	def _decorated(*args, **kwargs):
		try:
			return func(*args, **kwargs)
		except (HTTPError, HTTPRedirect, NotFound, KeyboardInterrupt, SystemExit):
			raise  # ignore system and common cherrypy exceptions
		except Exception:
			CORE.error('Traceback in %s(%r, %r):\n%s' % (func.__name__, args, kwargs, traceback.format_exc()))
			raise
	return _decorated


def _proxy_uri():
	scheme, _, base = cherrypy.request.base.partition('://')
	if cherrypy.request.headers.get('X-UMC-HTTPS') == 'on':
		scheme = 'https'

	cherrypy.request.scheme = scheme
	cherrypy.request.base = '%s://%s/univention' % (scheme, base)
	cherrypy.request.uri = ('%s%s?%s' % (cherrypy.request.base, cherrypy.request.path_info, cherrypy.request.query_string)).rstrip('?')
	foo = cherrypy.request.params
	try:
		cherrypy.request.params = {}
		cherrypy.request.process_query_string()
		cherrypy.request.query = cherrypy.request.params
	finally:
		cherrypy.request.params = foo


cherrypy.tools.fix_uri = cherrypy.Tool('before_request_body', _proxy_uri, priority=30)


class SessionClient(object):

	__slots__ = ('authenticated', 'client', '_requestid2response_queue', '_lock', 'ip')

	def __init__(self, ip=None):
		CORE.info('SessionClient(0x%x): creating new session' % (id(self),))
		self.authenticated = False
		self.client = Client(servername=None, port=None, unix='/var/run/univention-management-console/server.socket', ssl=False)
		self.client.signal_connect('authenticated', self._authenticated)
		self.client.signal_connect('response', self._response)
		self.client.signal_connect('closed', self._closed)
		try:
			self.client.connect()
			CORE.info('SessionClient(0x%x): connected to UMC server' % (id(self),))
		except NoSocketError:
			CORE.warn('SessionClient(0x%x): connection to UMC server failed' % (id(self),))
			raise NoSocketError('Connection failed')
		self._requestid2response_queue = {}
		self._lock = threading.Lock()
		self.ip = ip

	def _authenticated(self, success, response):
		"""Callback function for 'authenticated' from UMCP-Server."""
		CORE.process('SessionClient(0x%x): _authenticated: success=%s  status=%s  message=%s result=%r' % (id(self), success, response.status, response.message, response.result))
		self.authenticated = success
		self._response(response)

	def _response(self, response):
		"""Queue response from UMC server."""
		self._lock.acquire()
		try:
			try:
				# get and remove queue for response
				queue = self._requestid2response_queue.pop(response.id)[0]
			except KeyError:
				CORE.process('SessionClient(0x%x): no request(%s) found: status=%s' % (id(self), response.id, response.status))
			else:
				CORE.info('SessionClient(0x%x): got response(%s): status=%s queue=0x%x' % (id(self), response.id, response.status, id(queue)))
				queue.put(response)
		finally:
			self._lock.release()

	def send_request(self, request, response_queue):
		"""Send request to UMC server."""
		CORE.info('SessionClient(0x%x): sending request(%s)' % (id(self), request.id))
		self._lock.acquire()
		try:
			self._requestid2response_queue[request.id] = (response_queue, request)
		finally:
			self._lock.release()
		try:
			self.client.request(request)
		except IOError:
			CORE.error(traceback.format_exc())
			self.client.disconnect()
			return

	def _closed(self):
		message = '\n'.join((
			'The connection to the Univention Management Console Server broke up unexpectedly. ',
			'If you have root permissions on the system you can restart UMC by executing the following commands:',
			' * service univention-management-console-server restart',
			' * service univention-management-console-web-server restart',
			'Otherwise please contact an administrator or try again later.'
		))
		for id, (queue, request) in list(self._requestid2response_queue.items()):
			self._requestid2response_queue.pop(id)
			response = Response(request)
			response.status = SERVICE_UNAVAILABLE
			response.reason = 'UMC Service restarting'
			response.message = message
			queue.put(response)

	def cleanup_request(self, request):
		"""Remove request from mapping."""
		self._lock.acquire()
		try:
			del self._requestid2response_queue[request.id]
		finally:
			self._lock.release()


class UMCP_Dispatcher(object):

	"""Dispatcher used to exchange the requests between CherryPy and UMC"""
	# sessionid ==> SessionClient
	sessions = {}
	_queue_send = Queue.Queue()

	@classmethod
	@log_exceptions
	def check_queue(cls):
		while True:
			try:
				queuerequest = cls._queue_send.get_nowait()
			except Queue.Empty:
				# Queue is empty - nothing to do (for now)
				return True
			try:
				cls.dispatch(queuerequest)
			except Exception as exc:
				CORE.process('Failed to create UMC connection: %s' % (exc,))
				response = Response(queuerequest.request)
				response.status = 500
				response.message = traceback.format_exc()
				queuerequest.response_queue.put(response)

	@classmethod
	def dispatch(cls, queuerequest):
		CORE.info('UMCP_Dispatcher: check_queue: new request: 0x%x' % (id(queuerequest),))

		client = cls.sessions.get(queuerequest.sessionid)
		if client is None:
			try:
				client = SessionClient(ip=queuerequest.ip)
			except NoSocketError as exc:
				CORE.process('Failed to create UMC connection: %s' % (exc,))
				response = Response(queuerequest.request)
				response.status = SERVICE_UNAVAILABLE
				response.reason = 'UMC Service Unavailable'
				response.message = '\n'.join((
					'The Univention Management Console Server is currently not running. ',
					'If you have root permissions on the system you can restart it by executing the following command:',
					' * service univention-management-console-server restart',
					'The following logfile may contain information why the server is not running:',
					' * /var/log/univention/management-console-server.log',
					'Otherwise please contact an administrator or try again later.'
				))
				queuerequest.response_queue.put(response)
				return

			cls.sessions[queuerequest.sessionid] = client
			callback = notifier.Callback(cls.cleanup_session, queuerequest.sessionid)
			client.client.signal_connect('closed', callback)

		# make sure a lost connection to the UMC-Server does not bind the session to ::1
		if client.ip in ('127.0.0.1', '::1') and queuerequest.ip != client.ip:
			CORE.warn('Switching session IP from=%r to=%r' % (client.ip, queuerequest.ip))
			client.ip = queuerequest.ip

		# bind session to IP (allow requests from localhost)
		if queuerequest.ip not in (client.ip, '127.0.0.1', '::1'):
			CORE.warn('The sessionid (ip=%s) is not valid for this IP address (%s)' % (client.ip, queuerequest.ip))
			response = Response(queuerequest.request)
			response.status = UNAUTHORIZED
			response.message = 'The current session is not valid with your IP address for security reasons. This might happen after switching the network. Please login again.'
			# very important! We must expire the session cookie, with the same path, otherwise one ends up in a infinite redirection loop after changing the IP address (e.g. because switching from VPN to regular network)
			for name in queuerequest.request.cookies:
				if name.startswith('UMCSessionId'):
					response.cookies[name] = {
						'expires': datetime.datetime.fromtimestamp(0),
						'path': '/univention/',
						'version': 1,
						'value': '',
					}
			queuerequest.response_queue.put(response)
			return

		if queuerequest.request.command == 'AUTH':
			CORE.info('Sending authentication request for user %r' % (queuerequest.request.body.get('username'),))

		client.send_request(queuerequest.request, queuerequest.response_queue)

	@classmethod
	def cleanup_session(cls, sessionid):
		"""Removes a session when the connection to the UMC server has died or the session is expired"""
		try:
			del cls.sessions[sessionid]
			CORE.info('Cleaning up session %r' % (sessionid,))
		except KeyError:
			CORE.info('Session %r not found' % (sessionid,))


class UploadManager(dict):

	def add(self, request_id, store):
		tmpfile = tempfile.NamedTemporaryFile(prefix=request_id, dir=TEMPUPLOADDIR, delete=False)
		if hasattr(store, 'file') and store.file is None:
			tmpfile.write(store.value)
		else:
			tmpfile.write(store.file.read())
		tmpfile.close()
		if request_id in self:
			self[request_id].append(tmpfile.name)
		else:
			self[request_id] = [tmpfile.name]

		return tmpfile.name

	def cleanup(self, request_id):
		if request_id in self:
			filenames = self[request_id]
			for filename in filenames:
				if os.path.isfile(filename):
					os.unlink(filename)
			del self[request_id]
			return True

		return False


_upload_manager = UploadManager()


class QueueRequest(object):

	"""Element for the request queue containing the assoziated session
	ID, the request object, a response queue and the request ip address.

	:param sessionid: str
	:param request: ´univention.management.console.protocol.message.Request´
	:param response_queue: ´Queue.Queue´
	:param ip: str
	"""

	__slots__ = ('sessionid', 'request', 'response_queue', 'ip')

	def __init__(self, sessionid, request, response_queue, ip):
		self.sessionid = sessionid
		self.request = request
		self.response_queue = response_queue
		self.ip = ip


class User(object):

	__slots__ = ('sessionid', 'username', 'password', 'saml', '_timeout', '_timeout_id')

	def __init__(self, sessionid, username, password, saml=None):
		self.sessionid = sessionid
		self.username = username
		self.password = password
		self.saml = saml
		self._timeout_id = None
		self.reset_timeout()

	def _session_timeout_timer(self):
		session = UMCP_Dispatcher.sessions.get(self.sessionid)
		if session and session._requestid2response_queue:
			self._timeout = 1
			self._timeout_id = notifier.timer_add(1000, self._session_timeout_timer)
			return

		CORE.info('session %r timed out' % (self.sessionid,))
		Ressource.sessions.pop(self.sessionid, None)
		self.on_logout()
		return False

	def reset_timeout(self):
		self.disconnect_timer()
		self._timeout = monotonic() + _session_timeout
		self._timeout_id = notifier.timer_add(int(self.session_end_time - monotonic()) * 1000, self._session_timeout_timer)

	def disconnect_timer(self):
		try:
			notifier.timer_remove(self._timeout_id)
		except KeyError:
			# timer has already been removed, Bug #52535
			CORE.warn('Session timer has already been removed for %r.' % (self,))

	def timed_out(self, now):
		return self.session_end_time < now

	@property
	def session_end_time(self):
		if self.is_saml_user() and self.saml.session_end_time:
			return self.saml.session_end_time
		return self._timeout

	def is_saml_user(self):
		# self.saml indicates that it was originally a
		# saml user. but it may have upgraded and got a
		# real password. the saml user object is still there,
		# though
		return self.password is None and self.saml

	def on_logout(self):
		self.disconnect_timer()
		if SAML.SP and self.saml:
			try:
				SAML.SP.local_logout(decode_name_id(self.saml.name_id))
			except Exception as exc:  # e.g. bsddb.DBNotFoundError
				CORE.warn('Could not remove SAML session: %s' % (exc,))

	def get_umc_password(self):
		if self.is_saml_user():
			return self.saml.message
		else:
			return self.password

	def get_umc_auth_type(self):
		if self.is_saml_user():
			return "SAML"
		else:
			return None

	def __repr__(self):
		return '<User(%s, %s, %s)>' % (self.username, self.sessionid, self.saml is not None)


class SAMLUser(object):

	__slots__ = ('message', 'username', 'session_end_time', 'name_id')

	def __init__(self, response, message):
		self.name_id = encode_name_id(response.name_id)
		self.message = message
		self.username = u''.join(response.ava['uid'])
		self.session_end_time = 0
		if response.not_on_or_after:
			self.session_end_time = int(monotonic() + (response.not_on_or_after - time.time()))


traceback_pattern = re.compile(r'(Traceback.*most recent call|File.*line.*in.*\d)')


@log_exceptions
def default_error_page(status, message, traceback, version, result=None):
	if not traceback and traceback_pattern.search(message):
		index = message.find('Traceback') if 'Traceback' in message else message.find('File')
		message, traceback = message[:index].strip(), message[index:].strip()
	if traceback:
		CORE.error('%s' % (traceback,))
	if ucr.is_false('umc/http/show_tracebacks', False):
		traceback = None

	accept_json, accept_html = 0, 0
	for accept in cherrypy.request.headers.elements('Accept'):
		mimetype = accept.value
		if mimetype in ('text/*', 'text/html'):
			accept_html = max(accept_html, accept.qvalue)
		if mimetype in ('application/*', 'application/json'):
			accept_json = max(accept_json, accept.qvalue)
	if accept_json < accept_html:
		return default_error_page_html(status, message, traceback, version, result)
	page = default_error_page_json(status, message, traceback, version, result)
	if 'X-Iframe-Response' in cherrypy.request.headers:
		cherrypy.response.headers['Content-Type'] = 'text/html'
		return '<html><body><textarea>%s</textarea></body></html>' % (escape(page, False),)
	return page


def default_error_page_html(status, message, traceback, version, result=None):
	content = default_error_page_json(status, message, traceback, version, result)
	try:
		with open('/usr/share/univention-management-console-frontend/error.html', 'r') as fd:
			content = fd.read().replace('%ERROR%', json.dumps(escape(content, True)))
		cherrypy.response.headers['Content-Type'] = 'text/html; charset=UTF-8'
	except (OSError, IOError):
		pass
	return content


def default_error_page_json(status, message, traceback, version, result=None):
	""" The default error page for UMCP responses """
	status, _, description = valid_status(status)
	if status == 401 and message == description:
		message = ''
	location = '%s/%s' % (cherrypy.request.base, cherrypy.request.uri[len(cherrypy.request.base) + 1:].split('/', 1)[0])
	if status == 404:
		traceback = None
	response = {
		'status': status,
		'message': message,
		'traceback': unescape(traceback) if traceback else traceback,
		'location': location,
	}
	if result:
		response['result'] = result
	cherrypy.response.headers['Content-type'] = 'application/json'
	return json.dumps(response)


class UMC_HTTPError(HTTPError):

	""" HTTPError which sets a error result """

	def __init__(self, status=500, message=None, body=None, error=None, reason=None):
		HTTPError.__init__(self, status if not reason else '%s %s' % (status, reason), message)
		self.body = body
		self.error = error

	def set_response(self):
		cherrypy._cperror.clean_headers(self.status)

		traceback = None
		if isinstance(self.error, dict) and self.error.get('traceback'):
			traceback = '%s\nRequest: %s\n\n%s' % (self._message, self.error.get('command'), self.error.get('traceback'))
			traceback = traceback.strip()
		cherrypy.response.status = self.status
		content = default_error_page(self.status, self._message, traceback, None, self.body)
		cherrypy.response.body = content.encode('utf-8')

		cherrypy.response.headers['Content-Length'] = len(content)

		cherrypy._cperror._be_ie_unfriendly(self.status)


class SamlError(UMC_HTTPError):

	def __init__(self, _=_):
		self._ = _

	def error(func=None, status=400):  # noqa: N805
		def _decorator(func):
			def _decorated(self, *args, **kwargs):
				message = func(self, *args, **kwargs) or ()
				super(SamlError, self).__init__(status, message)
				if "Passive authentication not supported." not in message:
					# "Passive authentication not supported." just means an active login is required. That is expected and needs no logging. It still needs to be raised though.
					CORE.warn('SamlError: %s %s' % (status, message))
				return self
			return _decorated
		if func is None:
			return _decorator
		return _decorator(func)

	def from_exception(self, etype, exc, etraceback):
		if isinstance(exc, DefusedXmlException):
			return self.defusedxml(exc)
		if isinstance(exc, ResponseLifetimeExceed):
			return self.response_lifetime_exceed(exc)
		if isinstance(exc, UnknownPrincipal):
			return self.unknown_principal(exc)
		if isinstance(exc, UnsupportedBinding):
			return self.unsupported_binding(exc)
		if isinstance(exc, VerificationError):
			return self.verification_error(exc)
		if isinstance(exc, UnsolicitedResponse):
			return self.unsolicited_response(exc)
		if isinstance(exc, StatusError):
			return self.status_error(exc)
		if isinstance(exc, MissingKey):
			return self.missing_key(exc)
		if isinstance(exc, SignatureError):
			return self.signature_error(exc)
		six.reraise(etype, exc, etraceback)

	@error
	def defusedxml(self, exc):
		CORE.error('Hacking attempt: %s' % (exc,))
		return self._('A hacking attempt was prevented.')

	@error
	def response_lifetime_exceed(self, exc):
		return self._('The response lifetime has exceeded: %s. Please make sure the server times are in sync.') % (exc,)

	@error
	def unknown_principal(self, exc):
		return self._('The principal is unknown: %s') % (exc,)

	@error
	def unsupported_binding(self, exc):
		return self._('The requested SAML binding is not known: %s') % (exc,)

	@error
	def unknown_logout_binding(self, binding):
		return self._('The logout binding is not known.')

	@error
	def verification_error(self, exc):
		return self._('The SAML response could not be verified: %s') % (exc,)

	@error
	def unsolicited_response(self, exc):
		return self._('Received an unsolicited SAML response. Please try to single sign on again by accessing /univention/saml/. Error message: %s') % (exc,)

	@error
	def status_error(self, exc):
		return self._('The identity provider reported a status error: %s') % (exc,)

	@error(status=500)
	def missing_key(self, exc):
		return self._('The issuer %r is now known to the SAML service provider. This is probably a misconfiguration and might be resolved by restarting the univention-management-console-web-server.') % (str(exc),)

	@error
	def signature_error(self, exc):
		return self._('The SAML response contained a invalid signature: %s') % (exc,)

	@error
	def unparsed_saml_response(self):
		return self._("The SAML message is invalid for this service provider.")

	@error(status=500)
	def no_identity_provider(self):
		return self._('There is a configuration error in the service provider: No identity provider are set up for use.')

	@error  # TODO: multiple choices redirection status
	def multiple_identity_provider(self, idps, idp_query_param):
		return self._('Could not pick an identity provider. You can specify one via the query string parameter %(param)r from %(idps)r') % {'param': idp_query_param, 'idps': idps}


class Ressource(object):

	# NOTE: only use CORE.process, _not_ CORE.error; since CORE.error writes as
	#	   well to /var/log/syslog, this seems to cause problems with cherrypy.
	# (Bug #22634)
	_logOptions = {
		'error': CORE.process,
		'warn': CORE.warn,
		'info': CORE.info,
	}

	sessions = {}

	@property
	def name(self):
		"""returns class name"""
		return self.__class__.__name__

	def _log(self, loglevel, _msg):
		remote = cherrypy.request.remote
		msg = '%s (%s:%s) %s' % (self.name, get_ip_address(), remote.port, _msg)
		self._logOptions.get(loglevel, lambda x: ud.debug(ud.MAIN, loglevel, x))(msg)

	def suffixed_cookie_name(self, name):
		host, _, port = cherrypy.request.headers.get('Host', '').partition(':')
		if port:
			try:
				port = '-%d' % (int(port),)
			except ValueError:
				port = ''
		return '%s%s' % (name, port)

	def create_sessionid(self, random=True):
		if self.get_session():
			# if the user is already authenticated at the UMC-Server
			# we must not change the session ID cookie as this might cause
			# race conditions in the frontend during login, especially when logged in via SAML
			return self.get_session_id()
		user = self.get_user()
		if user:
			# If the user was already authenticated at the UMC-Server
			# and the connection was lost (e.g. due to a module timeout)
			# we must not change the session ID cookie, as there might be multiple concurrent
			# requests from the same client during a new initialization of the connection to the UMC-Server.
			# They must cause that the session has one singleton connection!
			return user.sessionid
		if random:
			return str(uuid.uuid4())
		return sessionidhash()

	def get_session_id(self):
		"""get the current session ID from cookie (or basic auth hash)."""
		# caution: use this function wisely: do not create a new session with this ID!
		# because it is an arbitrary value coming from the Client!
		return self.get_cookie('UMCSessionId') or sessionidhash()

	def get_session(self):
		return UMCP_Dispatcher.sessions.get(self.get_session_id())

	def check_saml_session_validity(self):
		user = self.get_user()
		if user and user.saml is not None and user.timed_out(monotonic()):
			raise UMC_HTTPError(UNAUTHORIZED)

	def set_cookies(self, *cookies, **kwargs):
		# TODO: use expiration from session timeout?
		# set the cookie once during successful authentication
		if kwargs.get('expires'):
			expires = kwargs.get('expires')
		elif ucr.is_true('umc/http/enforce-session-cookie'):
			# session cookie (will be deleted when browser closes)
			expires = None
		else:
			# force expiration of cookie in 5 years from now on...
			expires = (datetime.datetime.now() + datetime.timedelta(days=5 * 365))
		cookie = cherrypy.response.cookie
		for name, value in cookies:
			name = self.suffixed_cookie_name(name)
			cookie[name] = value
			if expires:
				cookie[name]['expires'] = expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
			cookie[name]['version'] = 1
			cookie[name]['path'] = '/univention/'
			if cherrypy.request.scheme == 'https' and ucr.is_true('umc/http/enforce-secure-cookie'):
				cookie[name]['secure'] = True
			if ucr.get('umc/http/cookie/samesite') in ('Strict', 'Lax', 'None'):
				cookie[name]['samesite'] = ucr['umc/http/cookie/samesite']

	def get_cookie(self, name):
		cookie = cherrypy.request.cookie.get
		morsel = cookie(self.suffixed_cookie_name(name)) or cookie(name)
		if morsel:
			return morsel.value

	def set_session(self, sessionid, username, password=None, saml=None):
		olduser = self.get_user()
		if olduser:
			olduser.disconnect_timer()
		user = User(sessionid, username, password, saml or olduser and olduser.saml)
		self.sessions[sessionid] = user
		self.set_cookies(('UMCSessionId', sessionid), ('UMCUsername', username))
		return user

	def expire_session(self):
		sessionid = self.get_session_id()
		if sessionid:
			user = self.sessions.pop(sessionid, None)
			if user:
				user.on_logout()
			UMCP_Dispatcher.cleanup_session(sessionid)
			self.set_cookies(('UMCSessionId', ''), expires=datetime.datetime.fromtimestamp(0))

	def get_user(self):
		value = self.get_session_id()
		if not value or value not in self.sessions:
			return
		user = self.sessions[value]
		if user.timed_out(monotonic()):
			return
		return user


class CPgeneric(Ressource):

	def get_request(self, path, args):
		return Request(['generic'], opts={})

	def add_request_headers(self, request):
		request.http_method = cherrypy.request.method
		if six.PY2:
			request.headers = dict((name.decode('ISO8859-1').title(), value.decode('ISO8859-1')) for name, value in cherrypy.request.headers.items())
		else:
			request.headers = dict(cherrypy.request.headers.items())
		request.headers.pop('Cookie', None)
		if six.PY2:
			request.cookies = dict((x.key.decode('ISO8859-1'), x.value.decode('ISO8859-1')) for x in cherrypy.request.cookie.values())
		else:
			request.cookies = dict((x.key, x.value) for x in cherrypy.request.cookie.values())
		for name, value in list(request.cookies.items()):
			if name == self.suffixed_cookie_name('UMCSessionId'):
				request.cookies['UMCSessionId'] = value

	def add_response_headers(self, response):
		if response.headers:
			cherrypy.response.headers.update(response.headers)
		for key, item in response.cookies.items():
			if six.PY2 and not isinstance(key, bytes):
				key = key.encode('utf-8')  # bug in python Cookie!
			if isinstance(item, dict):
				cherrypy.response.cookie[key] = item.pop('value', '')
				cherrypy.response.cookie[key].update(item)
			else:
				cherrypy.response.cookie[key] = item
		if isinstance(response.body, dict):
			response.body.pop('headers', None)
			response.body.pop('cookies', None)

	def load_json(self, body):
		try:
			json_ = json.loads(body)
			if not isinstance(json_, dict):
				raise UMC_HTTPError(BAD_REQUEST, 'JSON document have to be dict')
		except ValueError:
			self._log('error', 'cannot parse JSON body')
			raise UMC_HTTPError(BAD_REQUEST, 'Invalid JSON document')
		return json_

	@cherrypy.expose
	def default(self, *path, **kwargs):
		self._log('info', 'got new request')
		self.check_saml_session_validity()
		return self.get_response(self.create_sessionid(), path, self.get_arguments(kwargs))

	def get_arguments(self, kwargs):
		if cherrypy.request.headers.get('Content-Type', '').startswith('application/json'):  # normal (json) request
			# get body and parse json
			body = u'{}'
			if cherrypy.request.method in cherrypy.request.methods_with_bodies:
				if not cherrypy.request.headers.get(u"Content-Length"):
					self._log('warn', 'missing Content-Length header')
					raise UMC_HTTPError(LENGTH_REQUIRED, 'Missing Content-Length header')
				body = cherrypy.request.body.read().decode('UTF-8', 'replace')

			args = self.load_json(body)
		else:
			# request is not json
			args = {'options': kwargs}
			if 'flavor' in kwargs:
				args['flavor'] = kwargs['flavor']
		return args

	def get_response(self, sessionid, path, args):
		# create new UMCP request
		req = self.get_request('/'.join(path), args)

		user = self.get_user()
		client = UMCP_Dispatcher.sessions.get(sessionid)
		if user and (user.password or user.saml) and (not client or not client.authenticated):
			auth = Request('AUTH')
			auth.body = {
				'username': user.username,
				'password': user.get_umc_password(),
				'auth_type': user.get_umc_auth_type(),
			}
			try:
				self.make_queue_request(sessionid, auth)
				self.set_session(sessionid, user.username, password=user.password)
			except UMC_HTTPError:
				self.expire_session()
				raise

		response = self.make_queue_request(sessionid, req)
		body = response.body
		if response.mimetype == 'application/json':
			body = json.dumps(response.body).encode('ASCII')

		return body

	def make_queue_request(self, sessionid, request):
		"""Appends a UMCP request to the queue and waits/blocks until the response is available"""

		self.add_request_headers(request)
		self.set_accept_language(request)

		response_queue = Queue.Queue()
		user = self.get_user()

		queue_request = QueueRequest(sessionid, request, response_queue, get_ip_address())
		UMCP_Dispatcher._queue_send.put(queue_request)
		if user:
			user.reset_timeout()

		self._log(99, 'queue(0x%x): sessionid=%r' % (id(response_queue), sessionid,))
		self._log('info', 'pushed request(0x%x) to queue(0x%x) - waiting for response' % (id(queue_request), id(response_queue)))
		response = response_queue.get()
		self._log('info', 'got response(0x%x) from queue(0x%x): status=%s' % (id(response), id(response_queue), response.status))

		self.add_response_headers(response)

		status = response.status or 200  # status is not set if not json
		if 200 <= status < 300:
			cherrypy.response.headers['Content-Type'] = response.mimetype
			cherrypy.response.status = status
			return response
		elif 300 <= status < 400:
			raise HTTPRedirect(response.headers.get('Location', ''), status)

		# something bad happened
		self._log('error', 'response status code: %s' % (response.status,))
		self._log('error', 'response reason : %s' % (response.reason,))
		self._log('error', 'response message: %s' % (response.message,))
		self._log('error', 'response result: %s' % (response.result,))
		if response.error:
			self._log('error', 'response error: %r' % (response.error,))
		raise UMC_HTTPError(response.status, response.message, response.result, response.error, response.reason)

	def set_accept_language(self, request):
		# set language based on Accept-Language header
		try:
			languages = [x.value for x in cherrypy.request.headers.elements('Accept-Language') if x.qvalue > 0]
		except Exception as exc:
			CORE.warn('malformed Accept-Language header: %s' % (exc,))
			languages = []

		# workaround for Safari (de-de -> de-DE): https://bugs.webkit.org/show_bug.cgi?id=163096
		languages = [re.sub('^([a-z][a-z]-)([a-z][a-z])$', lambda m: m.group(1) + m.group(2).upper(), lang) for lang in languages]

		# pre parse the HTTP syntax so that the UMC-Server doesn't need to do this (because there are no utility functions there)
		request.headers['Accept-Language'] = ', '.join(languages) or 'en-US'


class CPGet(CPgeneric):

	@cherrypy.expose
	def index(self, *args, **kwargs):
		raise UMC_HTTPError(NOT_FOUND)

	def get_request(self, path, args):
		return Request('GET', arguments=[path], options=args.get('options', {}))

	@cherrypy.expose
	def session_info(self, *args, **kwargs):
		info = {}
		user = self.get_user()
		if user is None:
			raise UMC_HTTPError(UNAUTHORIZED)
		info['username'] = user.username
		info['auth_type'] = user.saml and 'SAML'
		info['remaining'] = int(user.session_end_time - monotonic())
		return json.dumps({"status": 200, "result": info, "message": ""}).encode('ASCII')

	@cherrypy.expose
	def ipaddress(self, *a, **kw):
		try:
			addresses = self.addresses
		except ValueError:
			# hacking attempt
			addresses = [cherrypy.request.remote.ip]
		return json.dumps(addresses).encode('ASCII')

	@property
	def addresses(self):
		addresses = cherrypy.request.headers.get('X-FORWARDED-FOR', cherrypy.request.remote.ip).split(',') + [cherrypy.request.remote.ip]
		addresses = set(ip_address(x.decode('ASCII', 'ignore').strip() if isinstance(x, bytes) else x.strip()) for x in addresses)
		addresses.discard(ip_address(u'::1'))
		addresses.discard(ip_address(u'127.0.0.1'))
		return tuple(address.exploded for address in addresses)


class CPSet(CPgeneric):

	def get_request(self, path, args):
		return Request('SET', options=args.get('options', {}))


class CPCommand(CPgeneric):

	def get_request(self, path, args):
		if self._is_file_upload():
			return self.get_request_upload(path, args)

		if not path:
			raise UMC_HTTPError(NOT_FOUND)

		req = Request('COMMAND', [path], options=args.get('options', {}))
		if 'flavor' in args:
			req.flavor = args['flavor']

		return req

	def get_response(self, sessionid, path, args):
		response = super(CPCommand, self).get_response(sessionid, path, args)

		# check if the request is a iframe upload
		if 'X-Iframe-Response' in cherrypy.request.headers:
			# this is a workaround to make iframe uploads work, they need the textarea field
			cherrypy.response.headers['Content-Type'] = 'text/html'
			return '<html><body><textarea>%s</textarea></body></html>' % (response)

		return response

	def get_request_upload(self, path, args):
		self._log('info', 'Handle upload command')
		cherrypy.request.headers['Accept'] = 'application/json'  # enforce JSON response in case of errors
		if 'iframe' in args and (args['iframe'] not in ('false', False, 0, '0')):
			cherrypy.request.headers['X-Iframe-Response'] = 'true'  # enforce textarea wrapping
		req = Request('UPLOAD', arguments=[path])
		req.body = self._get_upload_arguments(req, args)
		return req

	def get_arguments(self, kwargs):
		if self._is_file_upload():
			return kwargs
		return super(CPCommand, self).get_arguments(kwargs)

	def _is_file_upload(self):
		return cherrypy.request.headers.get('Content-Type', '').startswith('multipart/form-data')

	def _get_upload_arguments(self, req, args):
		options = []
		body = {}

		# check if enough free space is available
		min_size = get_int('umc/server/upload/min_free_space', 51200)  # kilobyte
		s = os.statvfs(TEMPUPLOADDIR)
		free_disk_space = s.f_bavail * s.f_frsize // 1024  # kilobyte
		if free_disk_space < min_size:
			self._log('error', 'there is not enough free space to upload files')
			raise UMC_HTTPError(BAD_REQUEST, 'There is not enough free space on disk')

		for iid, ifield in args.items():
			if isinstance(ifield, cherrypy._cpreqbody.Part):
				tmpfile = _upload_manager.add(req.id, ifield)
				options.append(self._sanitize_file(tmpfile, ifield))
			elif isinstance(ifield, list):
				# multiple files
				for jfield in ifield:
					if isinstance(jfield, cherrypy._cpreqbody.Part):
						tmpfile = _upload_manager.add(req.id, jfield)
						options.append(self._sanitize_file(tmpfile, jfield))
					else:
						CORE.warn('Unknown type of multipart/form-data entry: %r=%r' % (iid, jfield))
			elif isinstance(ifield, six.string_types):
				# field is a string :)
				body[iid] = ifield
			else:
				CORE.warn('Unknown type of multipart/form-data entry: %r=%r' % (iid, ifield))

		body['options'] = options
		return body

	def _sanitize_file(self, tmpfile, store):
		# check if filesize is allowed
		st = os.stat(tmpfile)
		max_size = get_int('umc/server/upload/max', 64) * 1024
		if st.st_size > max_size:
			self._log('warn', 'file of size %d could not be uploaded' % (st.st_size))
			raise UMC_HTTPError(BAD_REQUEST, 'The size of the uploaded file is too large')

		filename = store.filename
		# some security
		for c in '<>/':
			filename = filename.replace(c, '_')

		return {
			'filename': filename,
			'name': store.name,
			'tmpfile': tmpfile
		}


class CPAuth(CPgeneric):

	@cherrypy.config(**{'tools.umcp_auth.on': False})
	@cherrypy.expose
	def sso(self, *args, **kwargs):
		remote = cherrypy.request.remote
		CORE.info('CPAuth/auth/sso: got new auth request (%s:%s <=> %s)' % (get_ip_address(), remote.port, remote.name))

		user = self.get_user()
		if not user or not user.saml or user.timed_out(monotonic()):
			# redirect user to login page in case he's not authenticated or his session timed out
			raise HTTPRedirect('/univention/saml/')

		req = Request('AUTH')
		req.body = {
			"auth_type": "SAML",
			"username": user.username,
			"password": user.saml.message
		}

		try:
			self._auth_request(req, user.sessionid)
		except UMC_HTTPError as exc:
			if exc.status == UNAUTHORIZED:
				# slapd down, time synchronization between IDP and SP is wrong, etc.
				CORE.error('SAML authentication failed: Make sure slapd runs and the time on the service provider and identity provider is identical.')
				raise UMC_HTTPError(
					500,
					'The SAML authentication failed. This might be a temporary problem. Please login again.\n'
					'Further information can be found in the following logfiles:\n'
					'* /var/log/univention/management-console-web-server.log\n'
					'* /var/log/univention/management-console-server.log\n'
				)
			raise

		# protect against javascript:alert('XSS'), mailto:foo and other non relative links!
		location = urlparse(kwargs.get('return', '/univention/management/'))
		if location.path.startswith('//'):
			location = urlparse('')
		location = urlunsplit(('', '', location.path, location.query, location.fragment))
		cherrypy.response.status = 303
		cherrypy.response.headers['Location'] = location

	@cherrypy.config(**{'tools.umcp_auth.on': False})
	@cherrypy.expose
	def default(self, **kwargs):
		remote = cherrypy.request.remote
		CORE.info('CPAuth/auth: got new auth request (%s:%s <=> %s)' % (get_ip_address(), remote.port, remote.name))

		try:
			content_length = int(cherrypy.request.headers.get("Content-Length", 0))
		except ValueError:
			content_length = None
		if not content_length and content_length != 0:
			CORE.process('auth: missing Content-Length header')
			raise UMC_HTTPError(LENGTH_REQUIRED)

		if cherrypy.request.method in cherrypy.request.methods_with_bodies:
			max_length = 2000 * 1024
			if content_length >= max_length:  # prevent some DoS
				raise UMC_HTTPError(REQUEST_ENTITY_TOO_LARGE, 'Request data is too large, allowed length is %d' % max_length)

		data = self.get_arguments(kwargs)

		CORE.info('auth: request: command=%s' % cherrypy.request.path_info)

		# create a sessionid if the user is not yet authenticated
		sessionid = self.create_sessionid(True)

		# create new UMCP request
		req = Request('AUTH')
		req.body = data.get('options', {})
		req.body['auth_type'] = None
		return self._auth_request(req, sessionid)

	def _auth_request(self, req, sessionid):
		response = self.make_queue_request(sessionid, req)

		self._log(99, 'auth: creating session with sessionid=%r' % (sessionid,))
		CORE.process('auth_type=%r' % (req.body.get('auth_type'),))

		username = req.body.get('username')
		password = req.body.get('password')
		body = response.body
		if response.mimetype == 'application/json':
			username = body.get('result', {}).get('username', username)
			body = json.dumps(response.body).encode('UTF-8')
		self.set_session(sessionid, username, password=password)
		return body

	def basic(self):
		credentials = cherrypy.request.headers.get('Authorization')
		if not credentials:
			return
		sessionid = self.create_sessionid(False)
		if sessionid in UMCP_Dispatcher.sessions:
			return
		try:
			scheme, credentials = credentials.split(u' ', 1)
		except ValueError:
			return
		if scheme.lower() != u'basic':
			return
		try:
			username, password = base64.b64decode(credentials.encode('utf-8')).decode('latin-1').split(u':', 1)
		except ValueError:
			return

		# authenticate
		sessionid = sessionidhash()
		req = Request('AUTH')
		req.body = {
			"username": username,
			"password": password
		}
		self._auth_request(req, sessionid)


class Root(Ressource):

	def __init__(self):
		self.command = self.upload = CPCommand()
		self.auth = CPAuth()
		self.get = CPGet()
		self.set = CPSet()
		self.saml = SAML()
		reload_webserver.callbacks.append(self.saml.reload)

	@cherrypy.expose
	def index(self, **kw):
		"""
		http://localhost:<ucr:umc/http/port>/
		"""
		raise HTTPRedirect('/univention/', status=305)

	@cherrypy.expose
	def logout(self, **kwargs):
		user = self.get_user()
		if user and user.saml is not None:
			raise HTTPRedirect('/univention/saml/logout')
		self.expire_session()
		location = kwargs.get('location')
		if location:
			# protect against javascript:alert('XSS'), mailto:foo and other non relative links!
			location = urlparse(location)
			if location.path.startswith('//'):
				location = urlparse('')
			location = urlunsplit(('', '', location.path, location.query, location.fragment))
		location = location or ucr.get('umc/logout/location') or '/univention/'
		raise HTTPRedirect(location)


def get_ip_address():
	"""get the IP address of client by last entry (from apache) in X-FORWARDED-FOR header"""
	return cherrypy.request.headers.get('X-FORWARDED-FOR', cherrypy.request.remote.ip).rsplit(', ', 1).pop()


class SAML(Ressource):

	SP = None
	identity_cache = '/var/cache/univention-management-console/saml-%d.bdb'
	state_cache = None
	configfile = '/usr/share/univention-management-console/saml/sp.py'
	idp_query_param = "IdpQuery"
	bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_HTTP_ARTIFACT]
	outstanding_queries = {}

	def __init__(self):
		self.reload()

	@property
	def sp(self):
		if not self.SP and not self.reload():
			raise UMC_HTTPError(SERVICE_UNAVAILABLE, 'Single sign on is not available due to misconfiguration. See logfiles.', reason='SSO Service Unavailable')
		return self.SP

	@classmethod
	def reload(cls):
		CORE.info('Reloading SAML service provider configuration')
		sys.modules.pop(os.path.splitext(os.path.basename(cls.configfile))[0], None)
		try:
			cls.SP = Saml2Client(config_file=cls.configfile, identity_cache=cls.identity_cache % (PORT,), state_cache=cls.state_cache)
			return True
		except Exception:
			CORE.warn('Startup of SAML2.0 service provider failed:\n%s' % (traceback.format_exc(),))
		return False

	@cherrypy.expose
	def metadata(self, *args, **kwargs):
		metadata = create_metadata_string(self.configfile, None, valid='4', cert=None, keyfile=None, mid=None, name=None, sign=False)
		cherrypy.response.headers['Content-Type'] = 'application/xml'
		return metadata

	@cherrypy.expose
	def index(self, *args, **kwargs):
		binding, message, relay_state = self._get_saml_message()

		if message is None:
			return self.do_single_sign_on(relay_state=kwargs.get('location', '/univention/management/'))

		acs = self.attribute_consuming_service
		if relay_state == 'iframe-passive':
			acs = self.attribute_consuming_service_iframe
		return acs(binding, message, relay_state)

	@cherrypy.expose
	def iframe(self, *args, **kwargs):
		cherrypy.request.uri = cherrypy.request.uri.replace('/iframe', '')
		return self.do_single_sign_on(is_passive='true', relay_state='iframe-passive')

	def attribute_consuming_service(self, binding, message, relay_state):
		response = self.acs(message, binding)
		saml = SAMLUser(response, message)
		user = self.set_session(self.create_sessionid(), saml.username, saml=saml)
		self.drop_umcp_authentication(user.sessionid)

		# protect against javascript:alert('XSS'), mailto:foo and other non relative links!
		location = urlparse(relay_state)
		if location.path.startswith('//'):
			location = urlparse('')
		location = urlunsplit(('', '', location.path, location.query, location.fragment))
		raise HTTPRedirect(location, 303)

	def attribute_consuming_service_iframe(self, binding, message, relay_state):
		cherrypy.request.headers['Accept'] = 'application/json'  # enforce JSON response in case of errors
		cherrypy.request.headers['X-Iframe-Response'] = 'true'  # enforce textarea wrapping
		response = self.acs(message, binding)
		saml = SAMLUser(response, message)
		sessionid = self.create_sessionid()
		self.set_session(sessionid, saml.username, saml=saml)
		self.drop_umcp_authentication(sessionid)
		cherrypy.response.headers['Content-Type'] = 'text/html'
		data = {"status": 200, "result": {"username": saml.username}}
		return b'<html><body><textarea>%s</textarea></body></html>' % (json.dumps(data).encode('ASCII'),)

	def drop_umcp_authentication(self, sessionid):
		"""Force re-authentication if we get a new SAML message"""
		client = UMCP_Dispatcher.sessions.get(sessionid)
		if client:
			client.authenticated = False

	@cherrypy.expose
	def slo(self, *args, **kwargs):  # single logout service
		binding, message, relay_state = self._get_saml_message()
		if message is None:
			raise UMC_HTTPError(400, 'The HTTP request is missing required SAML parameter.')

		try:
			is_logout_request = b'LogoutRequest' in zlib.decompress(base64.b64decode(message.encode('UTF-8')), -15).split(b'>', 1)[0]
		except Exception:
			CORE.error(traceback.format_exc())
			is_logout_request = False

		if is_logout_request:
			user = self.get_user()
			if not user or user.saml is None:
				# The user is either already logged out or has no cookie because he signed in via IP and gets redirected to the FQDN
				name_id = None
			else:
				name_id = user.saml.name_id
				user.saml = None
			http_args = self.sp.handle_logout_request(message, name_id, binding, relay_state=relay_state)
			self.expire_session()
			return self.http_response(binding, http_args)
		else:
			response = self.sp.parse_logout_request_response(message, binding)
			self.sp.handle_logout_response(response)
		return self._logout_success()

	@cherrypy.expose
	def logout(self, *args, **kwargs):
		user = self.get_user()

		if user is None or user.saml is None:
			return self._logout_success()

		# What if more than one
		try:
			data = self.sp.global_logout(user.saml.name_id)
		except KeyError:
			try:
				tb = sys.exc_info()[2]
				while tb.tb_next:
					tb = tb.tb_next
				if tb.tb_frame.f_code.co_name != 'entities':
					raise
			finally:
				tb = None
			# already logged out or UMC-Webserver restart
			user.saml = None
			data = {}

		for entity_id, logout_info in data.items():
			if not isinstance(logout_info, tuple):
				continue  # result from logout, should be OK

			binding, http_args = logout_info
			if binding not in (BINDING_HTTP_POST, BINDING_HTTP_REDIRECT):
				raise SamlError().unknown_logout_binding(binding)

			return self.http_response(binding, http_args)
		return self._logout_success()

	def _logout_success(self):
		user = self.get_user()
		if user:
			user.saml = None
		raise HTTPRedirect('/univention/logout')

	def _get_saml_message(self):
		"""Get the SAML message and corresponding binding from the HTTP request"""
		if cherrypy.request.method not in ('GET', 'POST'):
			cherrypy.response.headers['Allow'] = 'GET, HEAD, POST'
			raise UMC_HTTPError(405)

		if cherrypy.request.method == 'GET':
			binding = BINDING_HTTP_REDIRECT
			args = cherrypy.request.query
		elif cherrypy.request.method == "POST":
			binding = BINDING_HTTP_POST
			args = cherrypy.request.params

		relay_state = args.get('RelayState', '')
		try:
			message = args['SAMLResponse']
		except KeyError:
			try:
				message = args['SAMLRequest']
			except KeyError:
				try:
					message = args['SAMLart']
				except KeyError:
					return None, None, None
				message = self.sp.artifact2message(message, 'spsso')
				binding = BINDING_HTTP_ARTIFACT

		return binding, message, relay_state

	def acs(self, message, binding):  # attribute consuming service  # TODO: rename into parse
		try:
			response = self.sp.parse_authn_request_response(message, binding, self.outstanding_queries)
		except (UnknownPrincipal, UnsupportedBinding, VerificationError, UnsolicitedResponse, StatusError, MissingKey, SignatureError, ResponseLifetimeExceed, DefusedXmlException):
			raise SamlError().from_exception(*sys.exc_info())
		if response is None:
			raise SamlError().unparsed_saml_response()
		self.outstanding_queries.pop(response.in_response_to, None)
		return response

	def do_single_sign_on(self, **kwargs):
		binding, http_args = self.create_authn_request(**kwargs)
		return self.http_response(binding, http_args)

	def create_authn_request(self, **kwargs):
		"""Creates the SAML <AuthnRequest> request and returns the SAML binding and HTTP response.

			Returns (binding, http-arguments)
		"""
		identity_provider_entity_id = self.select_identity_provider()
		binding, destination = self.get_identity_provider_destination(identity_provider_entity_id)

		relay_state = kwargs.pop('relay_state', None)

		reply_binding, service_provider_url = self.select_service_provider()
		sid, message = self.sp.create_authn_request(destination, binding=reply_binding, assertion_consumer_service_urls=(service_provider_url,), **kwargs)

		http_args = self.sp.apply_binding(binding, message, destination, relay_state=relay_state)
		self.outstanding_queries[sid] = service_provider_url  # cherrypy.request.uri  # TODO: shouldn't this contain service_provider_url?
		return binding, http_args

	def select_identity_provider(self):
		"""Select an identity provider based on the available identity providers.
			If multiple IDP's are set up the client might have specified one in the query string.
			Otherwise an error is raised where the user can choose one.

			Returns the EntityID of the IDP.
		"""
		idps = self.sp.metadata.with_descriptor("idpsso")
		if not idps and self.reload():
			idps = self.sp.metadata.with_descriptor("idpsso")
		if self.idp_query_param in cherrypy.request.query and cherrypy.request.query[self.idp_query_param] in idps:
			return cherrypy.request.query[self.idp_query_param]
		if len(idps) == 1:
			return list(idps.keys())[0]
		if not idps:
			raise SamlError().no_identity_provider()
		raise SamlError().multiple_identity_provider(list(idps.keys()), self.idp_query_param)

	def get_identity_provider_destination(self, entity_id):
		"""Get the destination (with SAML binding) of the specified entity_id.

			Returns (binding, destination-URI)
		"""
		return self.sp.pick_binding("single_sign_on_service", self.bindings, "idpsso", entity_id=entity_id)

	def select_service_provider(self):
		"""Select the ACS-URI and binding of this service provider based on the request uri.
			Tries to preserve the current scheme (HTTP/HTTPS) and netloc (host/IP) but falls back to FQDN if it is not set up.

			Returns (binding, service-provider-URI)
		"""
		acs = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"]
		service_url, reply_binding = acs[0]
		netloc = False
		p2 = urlparse(cherrypy.request.uri)
		for _url, _binding in acs:
			p1 = urlparse(_url)
			if p1.scheme == p2.scheme and p1.netloc == p2.netloc:
				netloc = True
				service_url, reply_binding = _url, _binding
				if p1.path == p2.path:
					break
			elif not netloc and p1.netloc == p2.netloc:
				service_url, reply_binding = _url, _binding
		CORE.info('SAML: picked %r for %r with binding %r' % (service_url, cherrypy.request.uri, reply_binding))
		return reply_binding, service_url

	def http_response(self, binding, http_args):
		"""Converts the HTTP arguments from pysaml2 into the cherrypy response."""
		body = u''.join(http_args["data"])
		for key, value in http_args["headers"]:
			cherrypy.response.headers[key] = value

		if binding in (BINDING_HTTP_ARTIFACT, BINDING_HTTP_REDIRECT):
			cherrypy.response.status = 303 if cherrypy.request.protocol >= (1, 1) and cherrypy.request.method == 'POST' else 302
			if not body:
				raise HTTPRedirect(cherrypy.response.headers['Location'], status=cherrypy.response.status)

		return body.encode('UTF-8')


@log_exceptions
def run_cherrypy(options):
	# TODO FIXME Folgenden Configeintrag einbauen, wenn loglevel in (0,1,2)
	# 'server.environment': 'production',
	root = Root()
	cherrypy.tools.umcp_auth = cherrypy.Tool('before_handler', root.auth.basic, priority=1)
	cherrypy.config.update({
		'log.screen': not options.daemon_mode,
		'server.socket_port': options.port,
		'server.socket_host': ucr.get('umc/http/interface', '127.0.0.1'),
		'server.request_queue_size': get_int('umc/http/requestqueuesize', 100),
		'server.thread_pool': get_int('umc/http/maxthreads', 35),
		'server.max_request_body_size': get_int('umc/http/max_request_body_size', 104857600),
		'response.timeout': get_int('umc/http/response-timeout', 310),
		'engine.autoreload.on': False,
		'tools.response_headers.on': True,
		'tools.response_headers.headers': [
			('Content-Type', 'application/json')
		],
		'tools.fix_uri.on': True,
		'tools.umcp_auth.on': True,
		'error_page.default': default_error_page,
		'request.show_tracebacks': ucr.is_true('umc/http/show_tracebacks', True),
	})

	cherrypy.quickstart(root=root)


def reload_webserver():
	for func in reload_webserver.callbacks:
		func()


reload_webserver.callbacks = [log_reopen]


class UMC_HTTP_Daemon(DaemonRunner):

	def __init__(self):
		self.parser = OptionParser()
		self.parser.add_option(
			'-n', '--no-daemon', action='store_false',
			dest='daemon_mode', default=True,
			help='if set the process will not fork into the background'
		)
		try:
			default_debug = int(ucr.get('umc/server/debug/level', '1'))
		except (TypeError, ValueError):
			default_debug = 1
		self.parser.add_option(
			'-d', '--debug', action='store', type='int', dest='debug', default=default_debug,
			help='if given than debugging is activated and set to the specified level [default: %default]'
		)
		self.parser.add_option(
			'-L', '--log-file', action='store', dest='logfile', default='management-console-web-server',
			help='specifies an alternative log file [default: %default]'
		)
		self.parser.add_option(
			'-p', '--port', action='store', type='int',
			dest='port', default=get_int('umc/http/port', 8090),
			help='defines an alternative port number [default %(default)s]')
		(self.options, self.arguments) = self.parser.parse_args()

		processes = get_int('umc/http/processes', 1)

		# cleanup environment
		os.environ.clear()
		os.environ['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin'

		# init logging
		if not self.options.daemon_mode:
			debug_fd = log_init('/dev/stderr', self.options.debug, processes > 1)
		else:
			debug_fd = log_init(self.options.logfile, self.options.debug, processes > 1)

		# default action: start
		if not self.arguments:
			sys.argv[1:] = ['start']
		elif self.arguments:
			sys.argv[1:] = self.arguments

		# for daemon runner
		if self.options.daemon_mode:
			self.stdin_path = os.path.devnull
			self.stdout_path = os.path.devnull
			self.stderr_path = os.path.devnull
		else:
			self.stdin_path = '/dev/stdin'
			self.stdout_path = '/dev/stdout'
			self.stderr_path = '/dev/stderr'
		self.pidfile_path = '/var/run/umc-web-server{}.pid'.format(self.options.port if self.options.port != 8090 else '')
		self.pidfile_timeout = 10

		global PORT
		PORT = self.options.port

		# init daemon runner
		DaemonRunner.__init__(self, self)
		self.daemon_context.prevent_core = False
		self.daemon_context.detach_process = self.options.daemon_mode
		self.daemon_context.umask = 0o077
		self.daemon_context.files_preserve = [debug_fd]

	def _open_streams_from_app_stream_paths(self, app):
		# Workaround for Python 3 Problem:
		# python-daemon does `open('/dev/null', 'w+t', buffering=0)`
		# which causes `ValueError: can't have unbuffered text I/O`
		if six.PY3:
			self.daemon_context.stdin = open(app.stdin_path, 'r') if app.stdin_path != '/dev/stdin' else sys.stdin
			self.daemon_context.stdout = open(app.stdout_path, 'w') if app.stdout_path != '/dev/stdout' else sys.stdout
			self.daemon_context.stderr = open(app.stderr_path, 'w') if app.stderr_path != '/dev/stderr' else sys.stderr
			return
		return super(UMC_HTTP_Daemon, self)._open_streams_from_app_stream_paths(app)

	def _restart(self):
		"""Handler for the restart action. """
		if self.pidfile.is_locked():
			CORE.process('Stopping UMC web server ...')
			self._stop()

		CORE.process('Starting UMC web server ...')
		self._start()

	def _crestart(self):
		"""Handler for the crestart action. """
		if not self.pidfile.is_locked():
			CORE.process('The UMC web server will not be restarted as it is not running currently')
			return

		CORE.process('Stopping UMC web server ...')
		self._stop()
		CORE.process('Starting UMC web server ...')
		self._start()

	def _reload(self):
		"""Handler for the reload action"""
		if self.pidfile.is_locked():
			pid = self.pidfile.read_pid()
			try:
				os.kill(pid, signal.SIGUSR1)
			except OSError as exc:
				if exc.errno == ESRCH:
					CORE.process('Reload failed: UMC-WebServer is not running')
					return
				raise
		else:
			CORE.process('Reload failed: webserver is not running')

	DaemonRunner.action_funcs['restart'] = _restart
	DaemonRunner.action_funcs['crestart'] = _crestart
	DaemonRunner.action_funcs['reload'] = _reload

	def _terminate_daemon_process(self):
		""" Terminate the daemon process specified in the current PID file.
			"""
		pid = self.pidfile.read_pid()
		try:
			os.kill(pid, signal.SIGTERM)
		except OSError as exc:
			raise DaemonRunnerStopFailureError("Failed to terminate %d: %s" % (pid, exc))

		if self.pidfile.is_locked():
			CORE.process('The UMC web server is still running. Will wait for 5 seconds')
			count = 10
			while count:
				time.sleep(0.5)
				if not self.pidfile.is_locked():
					break
				count -= 1
			if self.pidfile.is_locked():
				CORE.process('The UMC web server is still running. Kill it!')
				os.kill(pid, signal.SIGKILL)
				self.pidfile.break_lock()

	def _usage_exit(self, argv):
		self.parser.error('invalid action')
		sys.exit(1)

	def run(self):
		# cherrypy runs in a thread. signals can only be registered in the main thread
		# to prevent race conditions this must be called before the cherrypy thread gets created
		cherrypy.engine.signal_handler.handlers['SIGHUP'] = log_reopen
		cherrypy.engine.signal_handler.handlers['SIGUSR1'] = reload_webserver
		cherrypy.engine.signal_handler.subscribe()
		cherrypy.engine.subscribe('exit', lambda: notifier.dispatcher_add(lambda: sys.exit(0)))

		# start webserver as separate thread
		_thread_http = threading.Thread(target=run_cherrypy, args=(self.options,))
		_thread_http.deamon = True
		_thread_http.start()
		try:
			fd_limit = get_int('umc/http/max-open-file-descriptors', 65535)
			resource.setrlimit(resource.RLIMIT_NOFILE, (fd_limit, fd_limit))
		except (ValueError, resource.error) as exc:
			CORE.error('Could not raise NOFILE resource limits: %s' % (exc,))

		try:
			# start notifier loop
			notifier.init(notifier.GENERIC)
			notifier.dispatch.MIN_TIMER = get_int('umc/http/dispatch-interval', notifier.dispatch.MIN_TIMER)
			notifier.dispatcher_add(UMCP_Dispatcher.check_queue)
			notifier.loop()
		except (SystemExit, KeyboardInterrupt) as exc:
			# stop the web server
			CORE.info('stopping cherrypy: %s' % (exc,))
			cherrypy.engine.exit()
			CORE.info('cherrypy stopped')
		except BaseException:
			CORE.error('FATAL error: %s' % (traceback.format_exc(),))
			cherrypy.engine.exit()
			raise


if __name__ == '__main__':
	http_daemon = UMC_HTTP_Daemon()
	try:
		http_daemon.do_action()
	except DaemonRunnerStopFailureError as exc:
		CORE.process('Failed to shutdown server gracefully (may be its already dead): %s' % (exc,))
	except DaemonRunnerStartFailureError as exc:
		CORE.process('Failed to start server: %s' % (exc,))
