diff --git a/warden3/warden_client/LICENSE b/warden3/warden_client/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..637e108da406945bad1f52542e2b4306e9ba373c
--- /dev/null
+++ b/warden3/warden_client/LICENSE
@@ -0,0 +1,27 @@
+BSD License
+
+Copyright © 2011-2013 Cesnet z.s.p.o
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the Cesnet z.s.p.o nor the names of its
+      contributors may be used to endorse or promote products derived from this
+      software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE Cesnet z.s.p.o BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/warden3/warden_client/warden_client.cfg b/warden3/warden_client/warden_client.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..83211fde2df37a0a5aa8c298a72478bb3dc5e653
--- /dev/null
+++ b/warden3/warden_client/warden_client.cfg
@@ -0,0 +1,18 @@
+{
+    "url": "https://warden.example.com/warden3",
+
+    "certfile": "cert.pem",
+    "keyfile": "key.pem",
+    "cafile": "ca.pem",
+
+    "timeout": 60,
+    "recv_events_limit": 6000,
+
+    "errlog": {"level": "debug"},
+    "filelog": {"file": "warden_client.log", "level": "warning"},
+    "syslog": {"socket": "/dev/log", "facility": "local7", "level": "warning"},
+
+    "idstore": "warden_client.id",
+
+    "name": "warden_client"
+}
diff --git a/warden3/warden_client/warden_client.py b/warden3/warden_client/warden_client.py
new file mode 100644
index 0000000000000000000000000000000000000000..302215d70e3fdf50ba147c511abe4fe0e005aa29
--- /dev/null
+++ b/warden3/warden_client/warden_client.py
@@ -0,0 +1,410 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2013 Cesnet z.s.p.o
+# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+
+import json, httplib, ssl, socket, logging, logging.handlers
+from urlparse import urlparse
+from urllib import urlencode
+from sys import stderr, exc_info
+from UserList import UserList
+from pprint import pformat
+from traceback import format_tb
+from os import path
+
+
+class HTTPSConnection(httplib.HTTPSConnection):
+    '''
+    Overridden to allow peer certificate validation, configuration
+    of SSL/ TLS version and cipher selection.  See:
+    http://hg.python.org/cpython/file/c1c45755397b/Lib/httplib.py#l1144
+    and `ssl.wrap_socket()`
+    '''
+    def __init__(self, host, **kwargs):
+        self.ciphers = kwargs.pop('ciphers',None)
+        self.ca_certs = kwargs.pop('ca_certs',None)
+        self.ssl_version = kwargs.pop('ssl_version',ssl.PROTOCOL_SSLv23)
+
+        httplib.HTTPSConnection.__init__(self,host,**kwargs)
+
+    def connect(self):
+        sock = socket.create_connection( (self.host, self.port), self.timeout)
+
+        if self._tunnel_host:
+            self.sock = sock
+            self._tunnel()
+
+        self.sock = ssl.wrap_socket(
+            sock,
+            keyfile = self.key_file,
+            certfile = self.cert_file,
+            ca_certs = self.ca_certs,
+            cert_reqs = ssl.CERT_REQUIRED if self.ca_certs else ssl.CERT_NONE,
+            ssl_version = self.ssl_version)
+
+
+
+class Error(Exception):
+    """ Object for returning error messages to calling application.
+        Caller can test whether it received data or error by checking
+        isinstance(res, Error).
+        However if he does not want to deal with errors altogether,
+        this error object also returns False value if used in Bool
+        context (e.g. in "if res: print res" print is not evaluated),
+        and also acts as empty iterator (e.g. in "for e in res: print e"
+        print is also not evaluated).
+        Also, it can be raised as an exception.
+    """
+
+    def __init__(self, message, logger=None, error=None, method=None,
+            detail=None, exc=None):
+
+        self.error = error
+        self.method = method
+        self.message = message
+        self.detail = detail
+        (self.exctype, self.excval, self.exctb) = exc or exc_info()
+        self.cause = self.excval # compatibility with other exceptions
+        if logger:
+            logger.error(str(self))
+            logger.info(self.info_str())
+            logger.debug(self.debug_str())
+
+
+    def __len__ (self):
+        """ In list or iterable context we're empty """
+        return 0
+
+
+    def __iter__(self):
+        """ We are the iterator """
+        return self
+
+
+    def next(self):
+        """ In list or iterable context we're empty """
+        raise StopIteration
+
+
+    def __bool__(self):
+        """ In boolean context we're never True """
+        return False
+
+
+    def __str__(self):
+        out = []
+        out.append("(%s)" % (self.error or "local"))
+        if self.method is not None:
+            out.append(" in \"%s\"" % self.method)
+        if self.message is not None:
+            out.append(": %s" % self.message)
+        if self.excval is not None:
+            out.append(" - cause was %s: %s" % (type(self.excval).__name__, str(self.excval)))
+        return "".join(out)
+
+
+    def info_str(self):
+        return ("Detail: %s" % pformat(self.detail)) or ""
+
+
+    def debug_str(self):
+        out = []
+        if self.excval is not None:
+            out.append("Exception %s: %s\n" % (type(self.excval).__name__, str(self.excval)))
+        if self.exctb is not None:
+            out.append("Traceback:\n%s" % "".join(format_tb(self.exctb)))
+        return "".join(out)
+
+
+
+class Client(object):
+
+    def __init__(self,
+            url,
+            certfile=None,
+            keyfile=None,
+            cafile=None,
+            timeout=60,
+            recv_events_limit=6000,
+            errlog={"level": "debug"},
+            syslog=None,
+            filelog=None,
+            idstore=None,
+            name="warden_client"):
+
+        self.name = name
+        # Init logging as soon as possible and make sure we don't
+        # spit out exceptions but just log or return Error objects
+        self.init_log(errlog, syslog, filelog)
+
+        self.url = urlparse(url, allow_fragments=False)
+
+        self.conn = None
+
+        base = path.join(path.dirname(__file__))
+        self.certfile = path.join(base, certfile or "cert.pem")
+        self.keyfile  = path.join(base, keyfile or "key.pem")
+        self.cafile = path.join(base, cafile or "ca.pem")
+        self.timeout = int(timeout)
+        self.recv_events_limit = int(recv_events_limit)
+        self.idstore = path.join(base, idstore) if idstore is not None else None
+
+        self.ciphers = 'TLS_RSA_WITH_AES_256_CBC_SHA'
+        self.sslversion = ssl.PROTOCOL_TLSv1
+
+
+    def init_log(self, errlog, syslog, filelog):
+
+        def loglevel(lev):
+            try:
+                return int(getattr(logging, lev.upper()))
+            except (AttributeError, ValueError):
+                self.logger.warning("Unknown loglevel \"%s\", using \"debug\"" % lev)
+                return logging.DEBUG
+
+        def facility(fac):
+            try:
+                return int(getattr(logging.handlers.SysLogHandler, "LOG_" + fac.upper()))
+            except (AttributeError, ValueError):
+                self.logger.warning("Unknown syslog facility \"%s\", using \"local7\"" % fac)
+                return logging.handlers.SysLogHandler.LOG_LOCAL7
+
+        form = "%(filename)s[%(process)d]: (%(levelname)s) %(name)s %(message)s"
+        format_notime = logging.Formatter(form)
+        format_time = logging.Formatter('%(asctime)s ' + form)
+
+        self.logger = logging.getLogger(self.name)
+        self.logger.propagate = False   # Don't bubble up to root logger
+        self.logger.setLevel(logging.DEBUG)
+
+        if errlog is not None:
+            el = logging.StreamHandler(stderr)
+            el.setFormatter(format_time)
+            el.setLevel(loglevel(errlog.get("level", "debug")))
+            self.logger.addHandler(el)
+
+        if filelog is not None:
+            try:
+                fl = logging.FileHandler(
+                    filename=path.join(
+                        path.dirname(__file__),
+                        filelog.get("file", "%s.log" % self.name)))
+                fl.setLevel(loglevel(filelog.get("level", "warning")))
+                fl.setFormatter(format_time)
+                self.logger.addHandler(fl)
+            except Exception as e:
+                Error("Unable to setup file logging", self.logger)
+
+        if syslog is not None:
+            try:
+                sl = logging.handlers.SysLogHandler(
+                    address=syslog.get("socket", "/dev/log"),
+                    facility=facility(syslog.get("facility", "local7")))
+                sl.setLevel(loglevel(syslog.get("level", "warning")))
+                sl.setFormatter(format_notime)
+                self.logger.addHandler(sl)
+            except Exception as e:
+                Error("Unable to setup syslog logging", self.logger)
+
+        if not (errlog or filelog or syslog):
+            # User wants explicitly no logging, so let him shoot its socks off.
+            # This silences complaining of logging module about no suitable
+            # handler.
+            self.logger.addHandler(logging.NullHandler())
+
+
+    def connect(self):
+
+        try:
+            if self.url.scheme=="https":
+                self.conn = HTTPSConnection(
+                    self.url.netloc,
+                    key_file = self.keyfile,
+                    cert_file = self.certfile,
+                    timeout = self.timeout,
+                    ciphers = self.ciphers,
+                    ca_certs = self.cafile,
+                    ssl_version = self.sslversion)
+            elif self.url.scheme=="http":
+                self.conn = httplib.HTTPConnection(
+                    self.url.netloc,
+                    timeout = self.timeout)
+            else:
+                return Error("Don't know how to connect to \"%s\"" % self.url.scheme, self.logger,
+                        detail={"url": self.url.geturl()})
+        except Exception:
+            return Error("HTTPS connection failed", self.logger,
+                detail={
+                    "url": self.url.geturl(),
+                    "timeout": self.timeout,
+                    "key_file": self.keyfile,
+                    "cert_file": self.certfile,
+                    "cafile": self.cafile,
+                    "ciphers": self.ciphers,
+                    "ssl_version": self.sslversion})
+
+        return True
+
+
+    def sendRequest(self, func="", payload=None, **kwargs):
+
+        if kwargs:
+            for k in kwargs.keys():
+                if kwargs[k] is None:
+                    del kwargs[k]
+            argurl = "?" + urlencode(kwargs)
+        else:
+            argurl = ""
+
+        try:
+            if payload is None:
+                data = ""
+            else:
+                data = json.dumps(payload)
+        except:
+            return Error("Serialization to JSON failed", self.logger,
+                method=func, detail=payload)
+
+        self.headers = {
+            "Content-Type": "application/json",
+            "Accept": "application/json",
+            "Content-Length": str(len(data))
+        }
+
+        # We are connecting here at first use instead of in
+        # constructor, because constructor cannot return data/errors
+        # and we don't want to spit exceptions into user's face
+        # And maaaybee sometime we will implement reconnection on errors
+        if self.conn is None:
+            err = self.connect()
+            if not err:
+                return err  # either False of Error instance
+
+        loc = '%s/%s%s' % (self.url.path, func, argurl)
+        try:
+            self.conn.request("POST", loc, data, self.headers)
+        except:
+            return Error("Sending of request to server failed", self.logger,
+                method=func, detail={
+                    "loc": loc,
+                    "headers": self.headers,
+                    "data": data})
+
+        try:
+            res = self.conn.getresponse()
+        except:
+            return Error("HTTP reply failed", self.logger, method=func)
+
+        try:
+            response_data = res.read()
+        except:
+            return Error("Fetching HTTP data from server failed", self.logger, method=func)
+
+        if res.status==httplib.OK:
+            try:
+                data = json.loads(response_data)
+            except:
+                data = Error("JSON message parsing failed", self.logger,
+                    method=func, detail={"response": response_data})
+        else:
+            try:
+                data = json.loads(response_data)
+                data["error"]   # trigger exception if not dict or no error key
+            except:
+                data = Error("Generic server HTTP error", self.logger,
+                    method=func,
+                    error=res.status,
+                    detail={"response": response_data})
+            else:
+                data = Error(data.get("message", None), self.logger,
+                    method=data.get("method", None),
+                    error=res.status,
+                    detail=data.get("detail", None))
+
+        return data
+
+
+    def _saveID(self, id, idstore=None):
+        idf = idstore or self.idstore
+        if not idf:
+            return False
+        try:
+            with open(idf, "w+") as f:
+                f.write(str(id))
+        except (ValueError, IOError) as e:
+            # Use Error instance just for proper logging
+            Error("Writing id file \"%s\" failed" % idf, self.logger, detail={"idstore": idf})
+        return id
+
+
+    def _loadID(self, idstore=None):
+        idf = idstore or self.idstore
+        if not idf:
+            return None
+        try:
+            with open(idf, "r") as f:
+                id = int(f.read())
+        except (ValueError, IOError) as e:
+            Error("Reading id file \"%s\" failed, relying on server" % idf, self.logger, detail={"idstore": idf})
+            id = None
+        return id
+
+
+    def getDebug(self):
+        return self.sendRequest("getDebug")
+
+
+    def getInfo(self):
+        return self.sendRequest("getInfo")
+
+
+    def sendEvents(self, events=[]):
+        res = self.sendRequest(
+            "sendEvents", payload=events)
+        if not res:
+            return res  # Should be Error instance
+
+        return res.get("saved", 0)
+
+
+    def getEvents(self, id=None, idstore=None, count=1,
+            cat=None, nocat=None,
+            tag=None, notag=None,
+            group=None, nogroup=None):
+
+        if not id:
+            id = self._loadID(idstore)
+
+        res = self.sendRequest(
+            "getEvents", id=id, count=count or self.recv_events_limit, cat=cat,
+            nocat=nocat, tag=tag, notag=notag, group=group, nogroup=nogroup)
+        if not res:
+            return res  # Should be Error instance
+
+        try:
+            events = res["events"]
+            newid = res["lastid"]
+        except KeyError:
+            return Error("Server returned bogus reply", self.logger, method="getEvents", detail={"response": res})
+
+        self._saveID(newid)
+
+        return events
+
+
+    def close(self):
+
+        if hasattr(self, "conn") and hasattr(self.conn, "close"):
+            self.conn.close()
+
+
+    __del__ = close
+
+
+
+def read_cfg(cfgfile):
+    abspath = path.join(path.dirname(__file__), cfgfile)
+    with open(abspath, "r") as f:
+        stripcomments = "\n".join((l for l in f if not l.lstrip().startswith("#")))
+        return json.loads(stripcomments)
diff --git a/warden3/warden_client/warden_client_test.py b/warden3/warden_client/warden_client_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..081b7f47a79adf2ff135125e6c6dc19e45bad039
--- /dev/null
+++ b/warden3/warden_client/warden_client_test.py
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2013 Cesnet z.s.p.o
+# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+
+from warden_client import Client, Error, read_cfg
+import json
+from time import time, gmtime
+from math import trunc
+from uuid import uuid4
+from pprint import pprint
+from os import path
+
+def gen_random_idea():
+
+    def get_precise_timestamp():
+        t = time()
+        us = trunc((t-trunc(t))*1000000)
+        g = gmtime(t)
+        iso = '%04d-%02d-%02dT%02d:%02d:%02d.%0dZ' % (g[0:6]+(us,))
+        return iso
+
+    return {
+       "Format": "IDEA0",
+       "ID": str(uuid4()),
+       "DetectTime": get_precise_timestamp(),
+       "Category": ["Test"],
+    }
+
+wclient = Client(**read_cfg("warden_client.cfg"))
+# Also inline arguments are possible:
+# wclient = Client(
+#     url  = 'https://warden.example.com/warden3',
+#     keyfile  = '/opt/warden3/etc/key.pem',
+#     certfile = '/opt/warden3/etc/cert.pem',
+#     cafile = '/opt/warden3/etc/tcs-ca-bundle.pem',
+#     timeout=10,
+#     errlog={"level": "debug"},
+#     filelog={"level": "debug"},
+#     idstore="MyClient.id",
+#     name="MyClient")
+
+print "=== Getting 10 events ==="
+start = time()
+ret = wclient.getEvents(count=10)
+print "Time: %f" % (time()-start)
+for e in ret:
+    print e
+if ret:
+    print len(ret)
+
+print "=== Sending 500 events ==="
+start = time()
+ret = wclient.sendEvents([gen_random_idea() for i in range(500)])
+if ret:
+    print ret
+print "Time: %f" % (time()-start)
+
+print "=== Server info ==="
+info = wclient.getInfo()
+if not isinstance(info, Error):
+    pprint(info)
diff --git a/warden3/warden_client/warden_curl_test.sh b/warden3/warden_client/warden_curl_test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f00f4788c7b51d7d9e848ecfcec23dde3fb8a92f
--- /dev/null
+++ b/warden3/warden_client/warden_curl_test.sh
@@ -0,0 +1,121 @@
+#!/bin/sh
+#
+# Copyright (C) 2011-2013 Cesnet z.s.p.o
+# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+
+keyfile='key.pem'
+certfile='cert.pem'
+cafile='tcs-ca-bundle.pem'
+url='https://warden.example.com/warden3'
+
+#    --fail \
+#    --show-error \
+#
+
+echo "Test  404"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/blefub
+echo
+
+echo "Test  404"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/
+echo
+
+echo "Test  Deserialization"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    --data '{#$%^' \
+    $url/getEvents
+echo
+
+echo "Test  Invalid data for getEvents - silently discarded"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    --data '[1]' \
+    $url/getEvents
+echo
+
+echo "Test  Called with internal args - just in log"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/getEvents?self=test
+echo
+
+echo "Test  Called with superfluous args - just in log"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/getEvents?bad=guy
+echo
+
+echo "Test  getEvents with no args - should be OK"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/getEvents
+echo
+
+echo "Test  getEvents - should be OK"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    "$url/getEvents?count=3&id=10"
+echo
+
+echo "Test  getDebug"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/getDebug
+echo
+
+echo "Test  getInfo"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    $url/getInfo
+echo
+
+#curl \
+#    --fail \
+#    --connect-timeout 3 \
+#    --request POST \
+#    $url/getEvents
diff --git a/warden3/warden_server/LICENSE b/warden3/warden_server/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..e50d3602f0db0e77cad78166544a934282f66415
--- /dev/null
+++ b/warden3/warden_server/LICENSE
@@ -0,0 +1,27 @@
+BSD License
+
+Copyright © 2011-2014 Cesnet z.s.p.o
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the Cesnet z.s.p.o nor the names of its
+      contributors may be used to endorse or promote products derived from this
+      software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE Cesnet z.s.p.o BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/warden3/warden_server/apache.conf b/warden3/warden_server/apache.conf
new file mode 100644
index 0000000000000000000000000000000000000000..380cd441f5b4920028780b4ddc77145d74d34b55
--- /dev/null
+++ b/warden3/warden_server/apache.conf
@@ -0,0 +1,18 @@
+SSLEngine on
+
+SSLVerifyClient require
+SSLVerifyDepth 4
+SSLOptions +StdEnvVars +ExportCertData
+
+#SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
+
+SSLCertificateFile      /opt/warden_server_3/etc/cert.pem
+SSLCertificateKeyFile   /opt/warden_server_3/etc/key.pem
+SSLCACertificateFile    /opt/warden_server_3/etc/tcs-ca-bundle.pem
+
+WSGIScriptAlias /warden3 /opt/warden_server_3/warden_server.wsgi
+
+<Directory /opt/warden_server_3/warden_server.wsgi>
+    Order allow,deny
+    Allow from all
+</Directory>
diff --git a/warden3/warden_server/idea.schema b/warden3/warden_server/idea.schema
new file mode 100644
index 0000000000000000000000000000000000000000..c6acf2754d9b5fdef4d7602479060074d40ad167
--- /dev/null
+++ b/warden3/warden_server/idea.schema
@@ -0,0 +1,574 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "description": "= IDEA0 format definition =\n\nKeys use !CamelCase, however to avoid confusion, they must be case insensitively unique within their parent object. When parsing, keys \"ID\", \"id\", \"iD\" and \"Id\" must be considered as equivalent.\n\nEach definition line is in form KEY: TYPE, followed by an explanation line, where type can be basic JSON type (in ''italics''), syntactically restricted type (with reference to [[#Types|Types]] chapter), or array of former two (order is important). Types define expected syntax, however their content may be further syntactically or semantically restricted according to particular key explanation.\n\nThe keys ''Format'', ''ID'', ''!DetectTime'' and ''Category'' are mandatory, rest of the keys is optional (nonexistent key indicates that information is not applicable or unknown).\n\nAs human language may be ambiguous inadvertently or by omission, when in doubt, consult [[IDEA/Schema|JSON schema]].",
+    "type": "object",
+    "required": ["Format", "ID", "DetectTime", "Category"],
+    "definitions": {
+        "Boolean": {
+            "description": "JSON \"true\" or \"false\" value.",
+            "type": "boolean"
+        },
+        "Integer": {
+            "description": "JSON \"number\" with no fractional and exponential part.",
+            "type": "integer"
+        },
+        "Version": {
+            "description": "Must contain string \"IDEA0\". (Trailing zero denotes draft version, after review/discussion and specification finalisation the name will change.)",
+            "type": "string",
+            "enum": ["IDEA0"]
+        },
+        "MediaType": {
+            "description": "Internet media type without parameters. Format is type and subtype, separated by slash, where type can contain only alphanumeric, underscore and minus sign, and subtype can contain only alphanumeric, plus and minus sign, underscore and dot.",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_+.-]+$"
+        },
+        "Charset": {
+            "description": "Character set name may consist of alphanumeric, dot, colon, minus sign, underscore and parentheses (round brackets).",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9.:_()-]+$"
+        },
+        "Encoding": {
+            "description": "May contain only string \"base64\" (however note that key can be nonexistent, which means native encoding).",
+            "type": "string",
+            "enum": ["base64"]
+        },
+        "Handle": {
+            "description": "String value unique among all \"Handle\" element values. May contain only alphanumeric or underscore, must not start with number and must not be empty.",
+            "type": "string",
+            "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
+        },
+        "ID": {
+            "description": "String, containing reasonably globally unique identifier. UUID version 4 (random) or 5 (SHA-1) is recommended. As IDs are meant to be used at other mediums, transfer protocols and formats (an example being query string fields in URL), they are allowed to contain only reasonably safe subset of characters. May thus contain only alphanumeric, dot, minus sign and underscore and must not be empty.",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9._-]+$"
+        },
+        "Timestamp": {
+            "description": "String, containing timestamp conforming to [[http://tools.ietf.org/html/rfc3339|RFC 3339]].",
+            "type": "string",
+            "format": "date-time",
+            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt ][0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:[Zz]|(?:[+-][0-9]{2}:[0-9]{2}))?$"
+        },
+        "Duration": {
+            "description": "String, containing time offset, intended for representing difference between two timestamps. Format is time part of [[http://tools.ietf.org/html/rfc3339|RFC 3339]], optionally prepended by \"D\" or \"d\" separator and number of days (which can have arbitrary number number of digits). \"D\" separator has been chosen to distinguish from internet time, and as a memory aid for \"duration\" or \"days\". For example \"536D10:20:30.5\" means 536 days, 10 hours, 20 seconds, 30.5 seconds, whereas 00:05:00 represents five minutes.\n\n[[http://tools.ietf.org/html/rfc2234|ABNF]] syntax:\n{{{\ntime-hour       = 2DIGIT  ; 00-23\ntime-minute     = 2DIGIT  ; 00-59\ntime-second     = 2DIGIT  ; 00-59\ntime-secfrac    = \".\" 1*DIGIT\nseparator       = \"D\" / \"d\"\ndays            = 1*DIGIT\n\nduration        = [days separator] time-hour \":\" time-minute \":\" time-second [time-secfrac]\n}}}",
+            "type": "string",
+            "format": "date-time",
+            "pattern": "^(?:[0-9]+[Dd])?[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?$"
+        },
+        "URI": {
+            "description": "String, containing URI as defined in [[http://tools.ietf.org/html/rfc3986|RFC 3986]] and related.",
+            "type": "string",
+            "format": "uri",
+            "pattern": "^[a-zA-Z][a-zA-Z0-9+.-]*:[][a-zA-Z0-9._~:/?#@*'&'()*+,;=%-]*$"
+        },
+        "Net4": {
+            "description": "String, containing IPv4 range in human readable form. Range can be specified as CIDR network (\"192.0.2.0/24\") or two IP addresses in dot-decimal notation, separated by minus sign (\"192.0.2.0-192.0.2.255\").",
+            "type": "string",
+            "pattern": "^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:(?:/(?:[0-9]|[1-2][0-9]|3[0-2]))|(?:-(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))?$"
+        },
+        "Net6": {
+            "description": "String, containing IPv6 range in human readable form. Range can be specified as CIDR notation (\"2001:db8::/48\") or two IP addresses in colon-hexadecimal notation, separated by minus sign (\"2001:db8::-2001:db8:0:ffff:ffff:ffff:ffff:ffff\").",
+            "type": "string",
+            "pattern": "^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:[0-9A-Fa-f]{1,4}|:))|(?:(?:[0-9A-Fa-f]{1,4}:){6}(?::[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){5}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){4}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,3})|(?:(?::[0-9A-Fa-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?:(?:[0-9A-Fa-f]{1,4}:){3}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,4})|(?:(?::[0-9A-Fa-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?:(?:[0-9A-Fa-f]{1,4}:){2}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,5})|(?:(?::[0-9A-Fa-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?:(?:[0-9A-Fa-f]{1,4}:){1}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,6})|(?:(?::[0-9A-Fa-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?::(?:(?:(?::[0-9A-Fa-f]{1,4}){1,7})|(?:(?::[0-9A-Fa-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(?:(?:/(?:\\d|\\d\\d|1[0-1]\\d|12[0-8]))?|-(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:[0-9A-Fa-f]{1,4}|:))|(?:(?:[0-9A-Fa-f]{1,4}:){6}(?::[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){5}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){4}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,3})|(?:(?::[0-9A-Fa-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?:(?:[0-9A-Fa-f]{1,4}:){3}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,4})|(?:(?::[0-9A-Fa-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?:(?:[0-9A-Fa-f]{1,4}:){2}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,5})|(?:(?::[0-9A-Fa-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?:(?:[0-9A-Fa-f]{1,4}:){1}(?:(?:(?::[0-9A-Fa-f]{1,4}){1,6})|(?:(?::[0-9A-Fa-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(?::(?:(?:(?::[0-9A-Fa-f]{1,4}){1,7})|(?:(?::[0-9A-Fa-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))))$"
+        },
+        "FQDN": {
+            "description": "String, containing fully qualified domain name. See [[https://tools.ietf.org/html/rfc1034#section-3.1|RFC 1034, chapter 3.1]], [[https://tools.ietf.org/html/rfc1035#section-2.3.1|RFC 1035, chapter 2.3.1]], [[https://tools.ietf.org/html/rfc1123#section-2|RFC 1123, section2]] and related.",
+            "type": "string",
+            "format": "hostname",
+            "allOf": [
+                {
+                    "description": "FQDN label may start and/or end with letter or number, contain letters, numbers or hyphen. Labels must be separated by one dot.",
+                    "pattern": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)*(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)?$"
+                },
+                {
+                    "description": "Domain name labels may contain at most 63 characters.",
+                    "pattern": "^(?:[^.]{1,63}\\.)*(?:[^.]{0,63})?$"
+                },
+                {
+                    "description": "There can be at most 127 levels of labels.",
+                    "pattern": "^(?:[^.]*\\.[^.]*){1,126}|[^.]*$"
+                },
+                {
+                    "description": "Maximum length of domain name is 253 characters.",
+                    "maxLength": 253
+                }
+            ]
+        },
+        "DN": {
+            "description": "String, containing (possibly relative, not fully qualified) domain name. See [[https://tools.ietf.org/html/rfc1034#section-3.1|RFC 1034, chapter 3.1]], [[https://tools.ietf.org/html/rfc1035#section-2.3.1|RFC 1035, chapter 2.3.1]], [[https://tools.ietf.org/html/rfc1123#section-2|RFC 1123, section2]] and related.",
+            "type": "string",
+            "format": "hostname",
+            "allOf": [
+                {
+                    "description": "DN label may start and/or end with letter or number, contain letters, numbers or hyphen. Labels must be separated by one dot.",
+                    "pattern": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)*(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)?$"
+                },
+                {
+                    "description": "Domain name labels may contain at most 63 characters.",
+                    "pattern": "^(?:[^.]{1,63}\\.)*(?:[^.]{0,63})?$"
+                },
+                {
+                    "description": "There can be at most 127 levels of labels.",
+                    "pattern": "^(?:[^.]*\\.[^.]*){1,126}|[^.]*$"
+                },
+                {
+                    "description": "Maximum length of domain name is 253 characters.",
+                    "maxLength": 253
+                }
+            ]
+        },
+        "MAC": {
+            "description": "String, containing MAC address in human friendly form - six groups of two hexadecimal digits, separated by colon.",
+            "type": "string",
+            "pattern": "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$"
+        },
+        "Netname": {
+            "description": "URI string, containing LIR identifier and network identifier within LIR namespace, separated by colon.",
+            "type": "string",
+            "format": "uri",
+            "pattern": "^[a-zA-Z][a-zA-Z0-9+.-]*:[][a-zA-Z0-9._~:/?#@*'&'()*+,;=%-]*$"
+        },
+        "Hash": {
+            "description": "URI string, defining hash type and hash value, separated by colon.",
+            "type": "string",
+            "format": "uri",
+            "pattern": "^[a-zA-Z][a-zA-Z0-9+.-]*:[][a-zA-Z0-9._~:/?#@*'&'()*+,;=%-]*$"
+        },
+        "EventTag": {
+            "description": "Category name consists of one or two abbreviated parts - category and optional subcategory, separated by dot. If unsure of more precise nature of the incident, subcategory and dot may be omitted. Category and subcategory name must contain only alphanumeric, underscore and minus sign.\n\nFor semantics and taxonomy see [[IDEA/Classifications#EventTag|security event types classification]].",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)?$"
+        },
+        "ProtocolName": {
+            "description": "Name must not be empty, must contain only alphanumeric and minus sign, must contain at least one letter, must not begin or end with a hyphen and two hyphens must not be adjacent.\n\nFor semantics and applicable strings see [[IDEA/Classifications#ProtocolName|protocols classification]].",
+            "type": "string",
+            "allOf": [
+                {
+                    "description": "Protocol name must contain at least one letter.",
+                    "pattern": "[a-zA-Z]"
+                },
+                {
+                    "description": "Protocol name must contain only alphanumeric and minus sign.",
+                    "pattern": "^[a-zA-Z0-9-]*$"
+                },
+                {
+                    "description": "Protocol name must begin with alphanumeric.",
+                    "pattern": "^[a-zA-Z0-9]"
+                },
+                {
+                    "description": "Protocol name must end with alphanumeric.",
+                    "pattern": "[a-zA-Z0-9]$"
+                },
+                {
+                    "description": "There must not be two adjacent hyphens in protocol name.",
+                    "not": {
+                        "pattern": "--"
+                    }
+                }
+            ]
+        },
+        "SourceTargetTag": {
+            "description": "Tag name must contain only alphanumeric, underscore and minus sign.\n\nFor semantics and taxonomy see [[IDEA/Classifications#SourceTargetTag|source/target classification]].",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9_-]+$"
+        },
+        "NodeTag": {
+            "description": "Tag name must contain only alphanumeric, underscore and minus sign.\n\nFor semantics and taxonomy see [[IDEA/Classifications#NodeTag|classification of detection nodes]].",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9_-]+$"
+        },
+        "AttachmentTag": {
+            "description": "Tag name must contain only alphanumeric, underscore and minus sign.\n\nFor semantics and taxonomy see [[IDEA/Classifications#AttachmentTag|attachment description]].",
+            "type": "string",
+            "pattern": "^[a-zA-Z0-9_-]+$"
+        }
+    },
+    "properties": {
+        "Format": {
+            "description": "Identifier of the IDEA container.",
+            "$ref": "#/definitions/Version"
+        },
+        "ID": {
+            "description": "Unique message identifier.",
+            "$ref": "#/definitions/ID"
+        },
+        "AltNames": {
+            "description": "Array of alternative identifiers.",
+            "type": "array",
+            "items": {
+                "description": "Alternative identifiers; strings which help to pair the event to internal system information (for example tickets in request tracker systems).",
+                "type": "string"
+            }
+        },
+        "CorrelID": {
+            "description": "Array of correlated messages identifiers.",
+            "type": "array",
+            "items": {
+                "description": "Identifiers of messages, which are information sources for creation of this message in case the message has been created based on correlation/analysis/deduction of other messages.",
+                "$ref": "#/definitions/ID"
+            }
+        },
+        "AggrID": {
+            "description": "Array of aggregated messages identifiers.",
+            "type": "array",
+            "items": {
+                "description": "Identifiers of messages, which are aggregated into more concise form by this message. Should be sent mostly by intermediary nodes, which detect duplicates, or aggregate events, spanning multiple detection windows, into one longer.",
+                "$ref": "#/definitions/ID"
+            }
+        },
+        "PredID": {
+            "description": "Array of obsoleted messages identifiers.",
+            "type": "array",
+            "items": {
+                "description": "Identifiers of messages, which are obsoleted and information in them is replaced by this message. Should be sent only by detection nodes to incorporate further data about ongoing event.",
+                "$ref": "#/definitions/ID"
+            }
+        },
+        "RelID": {
+            "description": "Array of related messages identifiers.",
+            "type": "array",
+            "items": {
+                "description": "Otherwise related messages.",
+                "$ref": "#/definitions/ID"
+            }
+        },
+        "CreateTime": {
+            "description": "Timestamp of the creation of the IDEA message. May point out delay between detection and processing of data.",
+            "$ref": "#/definitions/Timestamp"
+        },
+        "DetectTime": {
+            "description": "Timestamp of the moment of detection of event (not necessarily time of the event taking place). This timestamp is mandatory, because every detector is able to know when it detected the information - for example when line about event appeared in the logfile, or when its information source says the event was detected, or at least when it accepted the information from the source.",
+            "$ref": "#/definitions/Timestamp"
+        },
+        "EventTime": {
+            "description": "Deduced start of the event/attack, or just time of the event if its solitary.",
+            "$ref": "#/definitions/Timestamp"
+        },
+        "CeaseTime": {
+            "description": "Deduced end of the event/attack.",
+            "$ref": "#/definitions/Timestamp"
+        },
+        "WinStartTime": {
+            "description": "Beginning of aggregation window in which event has been observed.",
+            "$ref": "#/definitions/Timestamp"
+        },
+        "WinEndTime": {
+            "description": "End of aggregation window in which event has been observed.",
+            "$ref": "#/definitions/Timestamp"
+        },
+        "ConnCount": {
+            "description": "Number of individual connections attempted or taken place.",
+            "$ref": "#/definitions/Integer"
+        },
+        "FlowCount": {
+            "description": "Number of individual simplex (one direction) flows.",
+            "$ref": "#/definitions/Integer"
+        },
+        "PacketCount": {
+            "description": "Number of individual packets transferred.",
+            "$ref": "#/definitions/Integer"
+        },
+        "ByteCount": {
+            "description": "Number of bytes transferred.",
+            "$ref": "#/definitions/Integer"
+        },
+        "Category": {
+            "description": "Array of event categories.",
+            "type": "array",
+            "items": {
+                "description": "Category of event.",
+                "$ref": "#/definitions/EventTag"
+            }
+        },
+        "Ref": {
+            "description": "Array of references.",
+            "type": "array",
+            "items": {
+                "description": "References to known sources, related to attack and/or vulnerability. May be URL of the additional info, or URN (according to [[http://tools.ietf.org/html/rfc2141|RFC 2141]]) in registered namespace ([[http://www.iana.org/assignments/urn-namespaces/urn-namespaces.xhtml|IANA]]) or unregistered ad-hoc namespace bearing reasonable information value and uniqueness, such as \"urn:cve:CVE-2013-5634\".",
+                "$ref": "#/definitions/URI"
+            }
+        },
+        "Confidence": {
+            "description": "Confidence of detector in its own reliability of this particular detection. (0 – surely false, 1 – no doubts). If key is not presented, detector does not know (or has no capability to estimate the confidence).",
+            "type": "number",
+            "maximum": 1,
+            "minimum": 0
+        },
+        "Description": {
+            "description": "Short free text human readable description.",
+            "type": "string"
+        },
+        "Note": {
+            "description": "Free text human readable addidional note, possibly longer description of incident if not obvious.",
+            "type": "string"
+        },
+        "Source": {
+            "type": "array",
+            "description": "Array of source or target descriptions.",
+            "items": {
+                "description": "Information concerning particular source or target.",
+                "type": "object",
+                "properties": {
+                    "Type": {
+                        "description": "Array of source/target categories.",
+                        "type": "array",
+                        "items": {
+                            "description": "Closer category of source/target.",
+                            "$ref": "#/definitions/SourceTargetTag"
+                        }
+                    },
+                    "Hostname": {
+                        "description": "Array of hostnames.",
+                        "type": "array",
+                        "items": {
+                            "description": "Hostname of this source/target. Should be FQDN, but may not conform exactly, because values, extracted from logs, messages, DNS, etc. may themselves be malformed. Empty array can be used to explicitly indicate that value has been inquired and not found (missing DNS name).",
+                            "type": "string"
+                        }
+                    },
+                    "IP4": {
+                        "description": "Array of IPv4 addresses.",
+                        "type": "array",
+                        "items": {
+                            "description": "IPv4 addresses of this source/target.",
+                            "$ref": "#/definitions/Net4"
+                        }
+                    },
+                    "MAC": {
+                        "description": "Array of MAC addresses.",
+                        "type": "array",
+                        "items": {
+                            "description": "MAC addresses of this source/target.",
+                            "$ref": "#/definitions/MAC"
+                        }
+                    },
+                    "IP6": {
+                        "description": "Array of IPv6 addresses.",
+                        "type": "array",
+                        "items": {
+                            "description": "IPv6 addresses of this source/target.",
+                            "$ref": "#/definitions/Net6"
+                        }
+                    },
+                    "Port": {
+                        "description": "Array of port numbers.",
+                        "type": "array",
+                        "items": {
+                            "description": "Source or destination ports affected.",
+                            "$ref": "#/definitions/Integer"
+                        }
+                    },
+                    "Proto": {
+                        "description": "Array of protocol names.",
+                        "type": "array",
+                        "items": {
+                            "description": "Protocols, concerning connections from/to this source/target.",
+                            "$ref": "#/definitions/ProtocolName"
+                        }
+                    },
+                    "URL": {
+                        "description": "Array of URLs.",
+                        "type": "array",
+                        "items": {
+                            "description": "Unified Resource Locator of this source/target. Should be formatted according to [[http://tools.ietf.org/html/rfc1738|RFC 1738]], [[http://tools.ietf.org/html/rfc1808|RFC 1808]] and related, however may not conform exactly, because values, extracted from logs, messages, etc. may themselves be malformed.",
+                            "type": "string"
+                        }
+                    },
+                    "Email": {
+                        "description": "Array of email addresses.",
+                        "type": "array",
+                        "items": {
+                            "description": "Email address (for example Reply-To address in phishing message). Should be formatted according to [[http://tools.ietf.org/html/rfc5322#section-3.4|RFC 5322, section 3.4]] and related, however may not conform exactly, because values, extracted from logs, messages, DNS, etc. may themselves be malformed.",
+                            "type": "string"
+                        }
+                    },
+                    "AttachHand": {
+                        "description": "Array of attachment identifiers.",
+                        "type" : "array",
+                        "items": {
+                            "description": "Identifiers of attachments related to this source/target - contain \"Handle\"s of related attachments.",
+                            "$ref": "#/definitions/Handle"
+                        }
+                    },
+                    "Note": {
+                        "description": "Free text human readable additional note.",
+                        "type": "string"
+                    },
+                    "Spoofed": {
+                        "description": "Establishes whether this source/target is forged.",
+                        "$ref": "#/definitions/Boolean"
+                    },
+                    "Imprecise": {
+                        "description": "Establishes whether this source/target is knowingly imprecise.",
+                        "$ref": "#/definitions/Boolean"
+                    },
+                    "Anonymised": {
+                        "description": "Establishes whether this source/target is willingly incomplete.",
+                        "$ref": "#/definitions/Boolean"
+                    },
+                    "ASN": {
+                        "description": "Autonomous system numbers.",
+                        "type": "array",
+                        "items": {
+                            "description": "Autonomous system number of this source/target.",
+                            "$ref": "#/definitions/Integer"
+                        }
+                    },
+                    "Router": {
+                        "description": "Array of router/interface paths.",
+                        "type": "array",
+                        "items": {
+                            "description": "Router/interface path information. Intentionally organisation specific, router identifiers have usually no clear meaning outside organisational unit.",
+                            "type": "string"
+                        }
+                    },
+                    "Netname": {
+                        "description": "Array of RIR network identifiers.",
+                        "type": "array",
+                        "items": {
+                            "description": "RIR database reference network identifier (for example \"ripe:CESNET-BB2\" or \"arin:WETEMAA\"). Common network identifiers are: ripe, arin, apnic, lacnic, afrinic. Empty array can be used to explicitly indicate that value has been inquired and not found (IP address from unassigned block).",
+                            "$ref": "#/definitions/Netname"
+                        }
+                    },
+                    "Ref": {
+                        "description": "Array of references.",
+                        "type": "array",
+                        "items": {
+                            "description": "References to known sources, related to attack and/or vulnerability, specific to this source/target. May be URL of the additional info, or URN (according to [[http://tools.ietf.org/html/rfc2141|RFC 2141]]) in registered namespace ([[http://www.iana.org/assignments/urn-namespaces/urn-namespaces.xhtml|IANA]]) or unregistered ad-hoc namespace bearing reasonable information value and uniqueness, such as \"urn:cve:CVE-2013-2266\".",
+                            "$ref": "#/definitions/URI"
+                        }
+                    }
+                }
+            }
+        },
+        "Target": {
+            "$ref": "#/properties/Source"
+        },
+        "Attach": {
+            "description": "Array of attachment descriptions.",
+            "type": "array",
+            "items": {
+                "type": "object",
+                "description": "Additional attachment information and data.",
+                "properties": {
+                    "Handle": {
+                        "description": "Message unique identifier for reference through Attach elements.",
+                        "$ref": "#/definitions/Handle"
+                    },
+                    "FileName": {
+                        "description": "Array of filenames.",
+                        "type": "array",
+                        "items": {
+                            "description": "Names of the attached file.",
+                            "type": "string"
+                        }
+                    },
+                    "Type": {
+                        "description": "Array of attachment type tags.",
+                        "type": "array",
+                        "items": {
+                            "description": "Type of the attached data.",
+                            "$ref": "#/definitions/AttachmentTag"
+                        }
+                    },
+                    "Hash": {
+                        "description": "Array of checksums.",
+                        "type": "array",
+                        "items": {
+                            "description": "Checksum of the content (for example \"sha1:794467071687f7c59d033f4de5ece6b46415b633\" or \"md5:dc89f0b4ff9bd3b061dd66bb66c991b1\").",
+                            "$ref": "#/definitions/Hash"
+                        }
+                    },
+                    "Size": {
+                        "description": "Length of the content.",
+                        "$ref": "#/definitions/Integer"
+                    },
+                    "Ref": {
+                        "description": "Array of references.",
+                        "type": "array",
+                        "items": {
+                            "description": "References to known sources, related to attack and/or vulnerability, specific to this attachment. May be URL of the additional info, or URN (according to [[http://tools.ietf.org/html/rfc2141|RFC 2141]]) in registered namespace ([[http://www.iana.org/assignments/urn-namespaces/urn-namespaces.xhtml|IANA]]) or unregistered ad-hoc namespace bearing reasonable information value and uniqueness, such as \"urn:clamav:Win.Trojan.Banker-14334\".",
+                            "$ref": "#/definitions/URI"
+                        }
+                    },
+                    "Note": {
+                        "description": "Free text human readable additional note.",
+                        "type": "string"
+                    },
+                    "ContentType": {
+                        "description": "Internet Media Type of the attachment, according to [[http://tools.ietf.org/html/rfc2046|RFC 2046]] and related. Along with [[http://www.iana.org/assignments/media-types/media-types.xhtml|types standardized by IANA]] also non standard but widely used media types can be used (for examples see [[http://www.freeformatter.com/mime-types-list.html|MIME types list at freeformatter.com]]).",
+                        "$ref": "#/definitions/MediaType"
+                    },
+                    "ContentCharset": {
+                        "description": "Name of the content character set according to [[http://www.iana.org/assignments/character-sets/character-sets.xhtml|IANA list]]. If key is not defined, unspecified binary encoding is assumed.",
+                        "$ref": "#/definitions/Charset"
+                    },
+                    "ContentEncoding": {
+                        "description": "Encoding of the content, if feasible. Nonexistent key means native JSON encoding.",
+                        "$ref": "#/definitions/Encoding"
+                    },
+                    "Content": {
+                        "description": "Attachment content.",
+                        "type": "string"
+                    },
+                    "ContentID": {
+                        "description": "Array of external content IDs.",
+                        "type": "array",
+                        "items": {
+                            "description": "If content of attachment is transferred separately (in underlaying container), this key contains external ID of the content, so it can be paired back to message.",
+                            "type": "string"
+                        }
+                    },
+                    "ExternalURI": {
+                        "description": "Array of external URIs.",
+                        "type": "array",
+                        "items": {
+                            "description": "If content of attachment is available and/or recognizable from external source, this is defining URI (usually URL). May also be URN (according to [[http://tools.ietf.org/html/rfc2141|RFC 2141]]) in registered namespace ([[http://www.iana.org/assignments/urn-namespaces/urn-namespaces.xhtml|IANA]]) or unregistered ad-hoc namespace bearing reasonable information value and uniqueness, such as \"urn:mhr:55eaf7effadc07f866d1eaed9c64e7ee49fe081a\", \"magnet:?xt=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C\".",
+                            "$ref": "#/definitions/URI"
+                        }
+                    }
+                }
+            }
+        },
+        "Node": {
+            "description": "Array of detector descriptions.",
+            "type": "array",
+            "items": {
+                "description": "Detector or possible intermediary (event aggregator, correlator, etc.) description.",
+                "type": "object",
+                "properties": {
+                    "Name": {
+                        "description": "Name of the detector, chosen by (and local to) organisational unit, within which it should be unique.",
+                        "$ref": "#/definitions/DN"
+                    },
+                    "Realm": {
+                        "description": "Administrative domain string. Usually denotes organisation (or smaller organisational unit) which detector belongs to. The tuple (Name, Realm) thus should be reasonably unique, however still bear some readily meaningful sense.",
+                        "$ref": "#/definitions/FQDN"
+                    },
+                    "Type": {
+                        "description": "Array of detection node types.",
+                        "type": "array",
+                        "items": {
+                            "description": "Tag, describing various facets of the detector.",
+                            "$ref": "#/definitions/NodeTag"
+                        }
+                    },
+                    "SW": {
+                        "description": "Array of detection software names.",
+                        "type": "array",
+                        "items": {
+                            "description": "The name of the detection software (optionally including version). For example \"labrea-2.5-stable-1\" or \"HP !TippingPoint 7500NX\".",
+                            "type": "string"
+                        }
+                    },
+                    "AggrWin": {
+                        "description": "The size of the aggregation window, if applicable.",
+                        "$ref": "#/definitions/Duration"
+                    },
+                    "Note": {
+                        "description": "Free text human readable additional description.",
+                        "type": "string"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/warden3/warden_server/warden_server.cfg b/warden3/warden_server/warden_server.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..6a40f8b517f53f635884c4063d24eb0826dcad21
--- /dev/null
+++ b/warden3/warden_server/warden_server.cfg
@@ -0,0 +1,11 @@
+{
+    "Log": {
+        # If not specified, FileLogger is default
+        "level": "debug"
+    },
+    "Handler": {
+        "send_events_limit": 500,
+        "get_events_limit": 1000,
+        "description": "Warden 3 not even alpha development server"
+    }
+}
diff --git a/warden3/warden_server/warden_server.py b/warden3/warden_server/warden_server.py
new file mode 100755
index 0000000000000000000000000000000000000000..755cb490edec75420dc49f4fe7a2694af730f0d8
--- /dev/null
+++ b/warden3/warden_server/warden_server.py
@@ -0,0 +1,682 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2013 Cesnet z.s.p.o
+# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+
+import sys
+import logging
+import logging.handlers
+import ConfigParser
+from traceback import format_tb
+import M2Crypto.X509
+import json
+from uuid import uuid4
+from time import time, gmtime
+from math import trunc
+from io import BytesIO
+from urlparse import parse_qs
+from os import path
+
+# for local version of up to date jsonschema
+sys.path.append(path.join(path.dirname(__file__), "..", "lib"))
+
+from jsonschema import Draft4Validator, FormatChecker
+
+
+VERSION = "3.0-not-even-alpha"
+
+
+class Error(Exception):
+
+    def __init__(self, message, error=500, method=None,
+            detail=None, exc=(None, None, None)):
+        self.error = int(error)
+        self.method = method
+        self.message = message
+        self.detail = detail
+        (self.exctype, self.excval, self.exctb) = exc or sys.exc_info()
+        self.cause = self.excval # compatibility with other exceptions
+
+
+    def __str__(self):
+        out = []
+        out.append("Error(%s)" % (self.error))
+        if self.method is not None:
+            out.append(" in \"%s\"" % self.method)
+        if self.message is not None:
+            out.append(": %s" % self.message)
+        if self.excval is not None:
+            out.append(" - cause was %s: %s" % (type(self.excval).__name__, str(self.excval)))
+        return "".join(out)
+
+
+    def info_str(self):
+        return ("Detail: %s" % self.detail) or ""
+
+
+    def debug_str(self):
+        out = []
+        if self.excval is not None:
+            out.append("Exception %s: %s\n" % (type(self.excval).__name__, str(self.excval)))
+        if self.exctb is not None:
+            out.append("Traceback:\n%s" % "".join(format_tb(self.exctb)))
+        return "".join(out)
+
+
+    def to_dict(self):
+        d = {}
+        if self.error is not None:
+            d["error"] = self.error
+        if self.method is not None:
+            d["method"] = self.method
+        if self.message is not None:
+            d["message"] = self.message
+        if self.detail is not None:
+            d["detail"] = self.detail
+        if self.excval is not None:
+            d["message"] = d["message"] + ", cause was %s: %s" % (type(self.excval).__name__, str(self.excval))
+        return d
+
+
+
+def get_clean_root_logger(level=logging.INFO):
+    """ Attempts to get logging module into clean slate state """
+
+    # We want to be able to set up at least stderr logger before any
+    # configuration is read, and then later get rid of it and set up
+    # whatever administrator requires.
+    # However, there can exist only one logger, but we want to get a clean
+    # slate everytime we initialize StreamLogger or FileLogger... which
+    # is not exactly supported by logging module.
+    # So, we look directly inside logger class and clean up handlers/filters
+    # manually.
+    logger = logging.getLogger()  # no need to create new
+    logger.setLevel(level)
+    while logger.handlers:
+        logger.removeHandler(logger.handlers[0])
+    while logger.filters:
+        logger.removeFilter(logger.filters[0])
+    return logger
+
+
+
+def StreamLogger(stream=sys.stderr, level=logging.INFO):
+    """ Fallback handler just for setup, not meant to be used from
+        configuration file because during wsgi query stdout/stderr
+        is forbidden.
+    """
+
+    fhand = logging.StreamHandler(stream)
+    fform = logging.Formatter('%(asctime)s %(filename)s[%(process)d]: (%(levelname)s) %(message)s')
+    fhand.setFormatter(fform)
+    logger = get_clean_root_logger(level)
+    logger.addHandler(fhand)
+
+
+
+def FileLogger(filename, level=logging.INFO):
+
+    fhand = logging.FileHandler(filename)
+    fform = logging.Formatter('%(asctime)s %(filename)s[%(process)d]: (%(levelname)s) %(message)s')
+    fhand.setFormatter(fform)
+    logger = get_clean_root_logger(level)
+    logger.addHandler(fhand)
+    logging.info("Initialized FileLogger(filename=\"%s\", \"%s\")" % (filename, level))
+
+
+
+def SysLogger(socket="/dev/log", facility=logging.handlers.SysLogHandler.LOG_DAEMON, level=logging.INFO):
+
+    fhand = logging.handlers.SysLogHandler(address=socket, facility=facility)
+    fform = logging.Formatter('%(filename)s[%(process)d]: (%(levelname)s) %(message)s')
+    fhand.setFormatter(fform)
+    logger = get_clean_root_logger(level)
+    logger.addHandler(fhand)
+    logging.info("Initialized SysLogger(socket=\"%s\", facility=\"%s\", level=\"%s\")" % (socket, facility, level))
+
+
+
+class Object(object):
+
+    def __str__(self):
+        return "%s()" % type(self).__name__
+
+
+
+class NoAuthenticator(Object):
+
+    def __init__(self):
+        Object.__init__(self)
+
+    def authenticate (self, env):
+        return "anybody"    # or None
+
+
+    def authorize(self, env, client, method, args):
+        return (client is not None)
+
+
+
+class X509Authenticator(NoAuthenticator):
+
+    def __init__(self, db):
+        self.db = db
+        NoAuthenticator.__init__(self)
+
+
+    def __str__(self):
+        return "%s(db=%s)" % (type(self).__name__, type(self.db).__name__)
+
+
+    def get_cert_dns_names(self, pem):
+
+        cert = M2Crypto.X509.load_cert_string(pem)
+
+        subj = cert.get_subject()
+        commons = [n.get_data().as_text() for n in subj.get_entries_by_nid(subj.nid["CN"])]
+
+        ext = cert.get_ext("subjectAltName")
+        extstrs = [val.strip() for val in ext.get_value().split(",")]
+        altnames = [val[4:] for val in extstrs if val.startswith("DNS:")]
+
+        # bit of mangling to get rid of duplicates and leave commonname first
+        firstcommon = commons[0]
+        return [firstcommon] + list(set(altnames+commons) - set([firstcommon]))
+
+
+    def authenticate (self, env):
+        names = self.get_cert_dns_names(env["SSL_CLIENT_CERT"])
+        # FIXME: should probably fetch and return id from db, not textual username
+        env["warden.x509_dns_names"] = names
+        return names[0] if names else None
+
+
+    def authorize(self, env, client, method, args):
+        # Here we might choose with methods or args to (dis)allow for which
+        # client.
+        # FIXME: fetch reader/writer or better list of allowed methods from db
+        return (client is not None)
+
+
+
+class NoValidator(Object):
+
+    def check(self, event):
+        return []
+
+
+
+class JSONSchemaValidator(NoValidator):
+
+    def __init__(self, filename=None):
+        self.path = filename or path.join(path.dirname(__file__), "idea.schema")
+        with open(self.path) as f:
+            self.schema = json.load(f)
+        self.validator = Draft4Validator(self.schema, format_checker=FormatChecker())
+
+
+    def __str__(self):
+        return "%s(filename=\"%s\")" % (type(self).__name__, self.path)
+
+
+    def check(self, event):
+
+        def sortkey(k):
+            """ Treat keys as lowercase, prefer keys with less path segments """
+            return (len(k.path), "/".join(str(k.path)).lower())
+
+        res = []
+        for error in sorted(self.validator.iter_errors(event), key=sortkey):
+            res.append(
+                "Validation error: key \"%s\", value \"%s\", expected - %s, error message - %s\n" % (
+                    u"/".join(str(v) for v in error.path),
+                    error.instance,
+                    error.schema.get('description', 'no additional info'),
+                    error.message))
+
+        return res
+
+
+
+class Database(Object):
+    #FIXME: here database model will dictate methods, which other
+    #       objects will use. This is only dull example.
+
+    def __init__(self):
+        # Will accept db configuration parameters, initialize connection, etc.
+        pass
+
+    def gen_random_idea(self):
+
+        def get_precise_timestamp():
+            t = time()
+            us = trunc((t-trunc(t))*1000000)
+            g = gmtime(t)
+            iso = '%04d-%02d-%02dT%02d:%02d:%02d.%0dZ' % (g[0:6]+(us,))
+            return iso
+
+        return {
+           "Format": "IDEA0",
+           "ID": str(uuid4()),
+           "DetectTime": get_precise_timestamp(),
+           "Category": ["Test"],
+        }
+
+
+    def fetch_events(self, client, id, count,
+            cat=None, nocat=None,
+            tag=None, notag=None,
+            group=None, nogroup=None):
+        return {
+            "lastid": (id or 0)+count,
+            "events": [self.gen_random_idea() for i in range(count)]
+        }
+
+
+    def store_events(self, client, events):
+        errs = {}   # See sendEvents and validation, should return something similar
+        return errs
+
+
+
+def expose(meth):
+    meth.exposed = True
+    return meth
+
+
+class Server(Object):
+
+    def __init__(self, auth, handler):
+        self.auth = auth
+        self.handler = handler
+
+
+    def __str__(self):
+        return "%s(auth=%s, handler=%s)" % (type(self).__name__, type(self.auth).__name__, type(self.handler).__name__)
+
+
+    def sanitize_args(self, path, func, args, exclude=["self", "_env", "_client"]):
+        # silently remove internal args, these should never be used
+        # but if somebody does, we do not expose them by error message
+        intargs = set(args).intersection(exclude)
+        for a in intargs:
+            del args[a]
+        if intargs:
+            logging.info("%s called with internal args: %s" % (path, ", ".join(intargs)))
+
+        # silently remove surplus arguments - potential forward
+        # compatibility (unknown args will get ignored)
+        badargs = set(args)-set(func.func_code.co_varnames[0:func.func_code.co_argcount])
+        for a in badargs:
+            del args[a]
+        if badargs:
+            logging.info("%s called with superfluous args: %s" % (path, ", ".join(badargs)))
+
+        return args
+
+
+    def wsgi_app(self, environ, start_response, exc_info=None):
+        path = environ.get("PATH_INFO", "").lstrip("/")
+        output = ""
+        status = "200 OK"
+        headers = [('Content-type', 'application/json')]
+        exception = None
+
+        try:
+            try:
+                injson = environ['wsgi.input'].read()
+            except:
+                raise Error("Data read error", 400, method=path, exc=sys.exc_info())
+
+            try:
+                method = getattr(self.handler, path)
+                method.exposed    # dummy access to trigger AttributeError
+            except Exception:
+                raise Error("You've fallen of the cliff.", 404, method=path)
+
+            client = self.auth.authenticate(environ)
+            if not client:
+                raise Error("I'm watching YOU.", 403, method=path)
+
+            try:
+                events = json.loads(injson) if injson else None
+            except Exception:
+                raise Error("Deserialization error", 400, method=path,
+                    exc=sys.exc_info(), detail={"args": injson})
+
+            args = parse_qs(environ.get('QUERY_STRING', ""))
+            for k, v in args.iteritems():
+                args[k] = v[0]
+            logging.debug("%s called with %s" % (path, str(args)))
+            if events:
+                args["events"] = events
+
+            if not self.auth.authorize(environ, client, path, args):
+                raise Error("I'm watching YOU.", 403, method=path, detail={"client": client})
+
+            args = self.sanitize_args(path, method, args)
+            result = method(_env=environ, _client=client, **args)   # call requested method
+
+            try:
+            # 'default': takes care of non JSON serializable objects,
+            # which could (although shouldn't) appear in handler code
+                output = json.dumps(result, default=lambda v: str(v))
+            except Exception as e:
+                raise Error("Serialization error", 500, method=path,
+                    exc=sys.exc_info(), detail={"args": str(result)})
+
+        except Error as e:
+            exception = e
+        except Exception as e:
+            exception = Error("Server exception", 500, method=path)
+
+        if exception:
+            status = "%d %s" % (exception.error, exception.message)
+            result = exception.to_dict()
+            try:
+                output = json.dumps(result, default=lambda v: str(v))
+            except Exception as e:
+                # Here all bets are off, generate at least sane output
+                output = '{"error": %d, "message": "%s"}' % (
+                    exception.error, exception.message)
+
+            logging.error(str(exception))
+            i = exception.info_str()
+            if i:
+                logging.info(i)
+            d = exception.debug_str()
+            if d:
+                logging.debug(d)
+
+        headers.append(('Content-Length', str(len(output))))
+        start_response(status, headers)
+        return [output]
+
+
+    __call__ = wsgi_app
+
+
+
+class WardenHandler(Object):
+
+    def __init__(self, validator, db,
+            send_events_limit=100000, get_events_limit=100000,
+            description=None):
+
+        self.db = db
+        self.validator = validator
+        self.send_events_limit = send_events_limit
+        self.get_events_limit = get_events_limit
+        self.description = description
+
+
+    def __str__(self):
+        return "%s(validator=%s, db=%s, send_events_limit=%s, get_events_limit=%s, description=\"%s\")" % (
+            type(self).__name__, type(self.validator).__name__, type(self.db).__name__,
+            self.get_events_limit, self.send_events_limit, self.description)
+
+
+    @expose
+    def getDebug(self, _env, _client):
+        return _env
+
+
+    @expose
+    def getInfo(self, _env, _client):
+        info = {
+            "version": VERSION,
+            "send_events_limit": self.send_events_limit,
+            "get_events_limit": self.get_events_limit
+        }
+        if self.description:
+            info["description"] = self.description
+        return info
+
+
+    @expose
+    def getEvents(self, _env, _client, id=None, count=None,
+            cat=None, nocat=None,
+            tag=None, notag=None,
+            group=None, nogroup=None):
+
+        try:
+            id = int(id)
+        except (ValueError, TypeError):
+            id=0
+
+        try:
+            count = int(count)
+        except (ValueError, TypeError):
+            count = 1
+
+        if self.get_events_limit:
+            count = min(count, self.get_events_limit)
+
+        logging.debug("getEvents - count: %s" % count)
+        res = self.db.fetch_events(_client, id, count, cat, nocat, tag, notag, group, nogroup)
+        logging.info("getEvents(%d, %d, %s, %s, %s, %s, %s, %s): sending %d events" % (
+            id, count, cat, nocat, tag, notag, group, nogroup, len(res["events"])))
+
+        return res
+
+
+    @expose
+    def sendEvents(self, _env, _client, events=[]):
+        if not isinstance(events, list):
+            raise Error("List of events expected", 400, method="sendEvents")
+
+        if len(events)>self.send_events_limit:
+            raise Error("Too much events in one batch", 400, method="sendEvents",
+                detail={"limit": self.send_events_limit})
+
+        # FIXME: Maybe just croak on first bad event, save good ones so far
+        # and make client deal with the rest? Would simplify server error
+        # handling greatly.
+        okevents = []
+        valerrs = []
+        for event in events:
+            verrs = self.validator.check(event)
+            if verrs:
+                valerrs.append({"errors": verrs, "event": event})
+            else:
+                okevents.append(event)
+
+        dberrs = self.db.store_events(_client, okevents)
+
+        if valerrs or dberrs:
+            raise Error("Event storage error", 500, method="sendEvents",
+                detail=valerrs+dberrs)
+
+        logging.info("sendEvents(...): Saved %i events" % len(okevents))
+
+        return {"saved": len(okevents)}
+
+
+
+def read_ini(path):
+    c = ConfigParser.RawConfigParser()
+    res = c.read(path)
+    if not res or not path in res:
+        # We don't have loggin yet, hopefully this will go into webserver log
+        raise Error("Unable to read config: %s" % path)
+    data = {}
+    for sect in c.sections():
+        for opts in c.options(sect):
+            lsect = sect.lower()
+            if not lsect in data:
+                data[lsect] = {}
+            data[lsect][opts] = c.get(sect, opts)
+    return data
+
+
+def read_cfg(path):
+    with open(path, "r") as f:
+        stripcomments = "\n".join((l for l in f if not l.lstrip().startswith("#")))
+        conf = json.loads(stripcomments)
+
+    # Lowercase keys
+    conf = dict((sect.lower(), dict(
+        (subkey.lower(), val) for subkey, val in subsect.iteritems())
+    ) for sect, subsect in conf.iteritems())
+
+    return conf
+
+
+def fallback_wsgi(environ, start_response, exc_info=None):
+
+    # If server does not start, set up simple server, returning
+    # Warden JSON compliant error message
+    error=503
+    message="Server not running due to initialization error"
+    headers = [('Content-type', 'application/json')]
+
+    logline = "Error(%d): %s" % (error, message)
+    status = "%d %s" % (error, message)
+    output = '{"error": %d, "message": "%s"}' % (
+        error, message)
+
+    logging.critical(logline)
+    start_response(status, headers)
+    return [output]
+
+
+def build_server(conf):
+
+    # Functions for validation and conversion of config values
+    def facility(name):
+        return int(getattr(logging.handlers.SysLogHandler, "LOG_" + name.upper()))
+
+    def loglevel(name):
+        return int(getattr(logging, name.upper()))
+
+    def natural(name):
+        num = int(name)
+        if num<1:
+            raise ValueError("Not a natural number")
+        return num
+
+    def filepath(name):
+        # Make paths relative to dir of this script
+        return path.join(path.dirname(__file__), name)
+
+    def objdef(name):
+        return objects[name.lower()]
+
+    obj = objdef    # Draw into local namespace for init_obj
+
+    objects = {}    # Already initialized objects
+
+    # List of sections and objects, configured by them
+    # First object in each object list is the default one, otherwise
+    # "type" keyword in section may be used to choose other
+    section_def = {
+        "log": ["FileLogger", "SysLogger"],
+        "db": ["Database"],
+        "auth": ["X509Authenticator", "NoAuthenticator"],
+        "validator": ["JSONSchemaValidator", "NoValidator"],
+        "handler": ["WardenHandler"],
+        "server": ["Server"]
+    }
+
+    # Object parameter conversions and defaults
+    param_def = {
+        "FileLogger": {
+            "filename": {"type": filepath, "default": path.join(path.dirname(__file__), path.splitext(path.split(__file__)[1])[0] + ".log")},
+            "level": {"type": loglevel, "default": "info"},
+        },
+        "SysLogger": {
+            "socket": {"type": filepath, "default": "/dev/log"},
+            "facility": {"type": facility, "default": "daemon"},
+            "level": {"type": loglevel, "default": "info"}
+        },
+        "NoAuthenticator": {},
+        "X509Authenticator": {
+            "db": {"type": obj, "default": "db"}
+        },
+        "NoValidator": {},
+        "JSONSchemaValidator": {
+            "filename": {"type": filepath, "default": path.join(path.dirname(__file__), "idea.schema")}
+        },
+        "Database": {},
+        "WardenHandler": {
+            "validator": {"type": obj, "default": "validator"},
+            "db": {"type": obj, "default": "DB"},
+            "send_events_limit": {"type": natural, "default": 10000},
+            "get_events_limit": {"type": natural, "default": 10000},
+            "description": {"type": str, "default": ""}
+        },
+        "Server": {
+            "auth": {"type": obj, "default": "auth"},
+            "handler": {"type": obj, "default": "handler"}
+        }
+    }
+
+    def init_obj(sect_name):
+        config = conf.get(sect_name, {})
+        sect_name = sect_name.lower()
+        sect_def = section_def[sect_name]
+
+        try:    # Object type defined?
+            objtype = config["type"]
+            del config["type"]
+        except KeyError:    # No, fetch default object type for this section
+            objtype = sect_def[0]
+        else:
+            if not objtype in sect_def:
+                raise KeyError("Unknown type %s in section %s" % (objtype, sect_name))
+
+        params = param_def[objtype]
+
+        # No surplus parameters? Disallow also 'obj' attributes, these are only
+        # to provide default referenced section
+        for name in config:
+            if name not in params or (name in params and params[name]["type"] is objdef):
+                raise KeyError("Unknown key %s in section %s" % (name, sect_name))
+
+        # Process parameters
+        kwargs = {}
+        for name, definition in params.iteritems():
+            raw_val = config.get(name, definition["default"])
+            try:
+                val = definition["type"](raw_val)
+            except Exception:
+                raise KeyError("Bad value \"%s\" for %s in section %s" % (raw_val, name, sect_name))
+            kwargs[name] = val
+
+        cls = globals()[objtype]   # get class/function type
+        try:
+            obj = cls(**kwargs)         # run it
+        except Exception as e:
+            raise KeyError("Cannot initialize %s from section %s: %s" % (
+                objtype, sect_name, str(e)))
+
+        if isinstance(obj, Object):
+            # Log only objects here, functions must take care of themselves
+            logging.info("Initialized %s" % str(obj))
+
+        objects[sect_name] = obj
+        return obj
+
+    # Init logging with at least simple stderr StreamLogger
+    # Dunno if it's ok within wsgi, but we have no other choice, let's
+    # hope it at least ends up in webserver error log
+    StreamLogger()
+
+    try:
+        # Now try to init required objects
+        for o in ("log", "db", "auth", "validator", "handler", "server"):
+            init_obj(o)
+    except Exception as e:
+        logging.critical(str(e))
+        logging.debug("", exc_info=sys.exc_info())
+        return fallback_wsgi
+
+    logging.info("Ready to serve")
+
+    return objects["server"]
+
+
+if __name__=="__main__":
+    # FIXME: just development stuff
+    srv = build_server(read_ini("warden3.cfg.wheezy-warden3"))
diff --git a/warden3/warden_server/warden_server.wsgi b/warden3/warden_server/warden_server.wsgi
new file mode 100755
index 0000000000000000000000000000000000000000..609ab80604d7e93d4ec818b68df211c5a70557d0
--- /dev/null
+++ b/warden3/warden_server/warden_server.wsgi
@@ -0,0 +1,15 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2013 Cesnet z.s.p.o
+# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+
+from sys import path
+from os.path import dirname, join
+
+path.append(dirname(__file__))
+from warden_server import build_server
+
+## JSON configuration with line comments (trailing #)
+from warden_server import read_cfg
+application = build_server(read_cfg(join(dirname(__file__), "warden_server.cfg")))