[Osaas-commits] r2 - in trunk: . server server/osaas server/osaas/http server/test
scm-commit@wald.intevation.org
scm-commit at wald.intevation.org
Mon Aug 27 13:09:42 CEST 2007
Author: bh
Date: 2007-08-27 13:09:41 +0200 (Mon, 27 Aug 2007)
New Revision: 2
Added:
trunk/client/
trunk/server/
trunk/server/adduser.py
trunk/server/osaas/
trunk/server/osaas/__init__.py
trunk/server/osaas/config.py
trunk/server/osaas/dbbackend.py
trunk/server/osaas/formparser.py
trunk/server/osaas/http/
trunk/server/osaas/http/__init__.py
trunk/server/osaas/http/httpserver.py
trunk/server/osaas/http/run.py
trunk/server/osaas/http/threadedserver.py
trunk/server/osaas/http/threadpool.py
trunk/server/osaas/pycompat.py
trunk/server/osaas/recordfilter.py
trunk/server/osaas/run.py
trunk/server/osaas/server.py
trunk/server/osaas/userdb.py
trunk/server/startosaas.py
trunk/server/test/
trunk/server/test/runtests.py
trunk/server/test/serversupport.py
trunk/server/test/support.py
trunk/server/test/test_config.py
trunk/server/test/test_formparser.py
trunk/server/test/test_httpserver.py
trunk/server/test/test_osasserver.py
trunk/server/test/test_run.py
trunk/server/test/test_threadedserver.py
trunk/server/test/test_threadpool.py
trunk/server/test/test_userdb.py
Log:
import osaas server
Added: trunk/server/adduser.py
===================================================================
--- trunk/server/adduser.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/adduser.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,49 @@
+#! /usr/bin/env python
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Adds a username and password to a OSAAS user database
+Usage
+ adduser.py -f USERDBFILE USERNAME
+"""
+
+import sys
+import getpass
+
+import optparse
+
+from osaas.userdb import UserDB
+
+def ask_password(username):
+ while 1:
+ pw1 = getpass.getpass("Password for %r: " % username)
+ pw2 = getpass.getpass("Repeat password: ")
+ if pw1 == pw2:
+ return pw1
+ else:
+ print "Passwords don't match."
+
+def main():
+ parser = optparse.OptionParser()
+ parser.add_option("--userdb", "-f")
+
+ opts, rest = parser.parse_args()
+
+ if len(rest) != 1:
+ print >>sys.stderr, __doc__
+ sys.exit(1)
+
+ username = rest[0]
+
+ userdb = UserDB()
+ userdb.read(opts.userdb)
+ userdb.add_user(username, ask_password(username))
+ userdb.write(opts.userdb)
+
+
+if __name__ == "__main__":
+ main()
Property changes on: trunk/server/adduser.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/__init__.py
===================================================================
--- trunk/server/osaas/__init__.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/__init__.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,8 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+import pycompat
Property changes on: trunk/server/osaas/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/config.py
===================================================================
--- trunk/server/osaas/config.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/config.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,45 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+
+import optparse
+from xml.dom.minidom import parse
+
+class OSAASOption(optparse.Option):
+
+ ATTRS = optparse.Option.ATTRS[:]
+ ATTRS.extend(["xml_path"])
+
+
+class ConfigXMLPathError(Exception):
+
+ pass
+
+
+def find_xml_path(node, path):
+ for name in path.split("/"):
+ children = [child for child in node.childNodes
+ if child.nodeName == name]
+ if not children:
+ return None
+ if len(children) > 1:
+ raise ConfigXMLPathError("More than one element matches %r" % path)
+ node = children[0]
+ node.normalize()
+ value_node = node.firstChild
+ if value_node.nodeType != value_node.TEXT_NODE:
+ raise ConfigXMLPathError("Config file entry %r must only have text"
+ " content" % path)
+ return value_node.data
+
+def read_config_file(filename, options, setter):
+ dom = parse(filename)
+ for option in options:
+ if option.dest is not None and option.xml_path:
+ value = find_xml_path(dom, option.xml_path)
+ if value is not None:
+ setter(option.dest, option.check_value(option.xml_path, value))
Property changes on: trunk/server/osaas/config.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/dbbackend.py
===================================================================
--- trunk/server/osaas/dbbackend.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/dbbackend.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,11 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Database backend of OSAAS"""
+
+def dbhandler(record):
+ pass
Property changes on: trunk/server/osaas/dbbackend.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/formparser.py
===================================================================
--- trunk/server/osaas/formparser.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/formparser.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,77 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Parse the OSAAS form data into a record"""
+
+import cgi
+
+class FormParserException(Exception):
+
+ pass
+
+
+class Field(object):
+
+ def __init__(self, name, required=False):
+ self.name = name
+ self.required = required
+
+
+class Record:
+
+ def set_fields(self, fields, formdata, fieldname_normalizer=None):
+ try:
+ parsed_form = cgi.parse_qs(formdata, keep_blank_values=True,
+ strict_parsing=True)
+ except ValueError, err:
+ raise FormParserException("Error parsing form data: %s" % err)
+
+ if fieldname_normalizer is not None:
+ for name, value in parsed_form.items():
+ normalized = fieldname_normalizer(name)
+ del parsed_form[name]
+ parsed_form[normalized] = value
+
+ for field in fields:
+ value = parsed_form.get(field.name)
+ if value:
+ if len(value) == 1:
+ setattr(self, field.name, value[0])
+ else:
+ raise FormParserException("only one value allowed"
+ " for field %r" % field.name)
+ elif field.required:
+ raise FormParserException("formdata is missing required"
+ " field %r" % field.name)
+
+form_fields = [Field(name, required=True)
+ for name in ["user", "responsetime",
+ "wmsidextern", "wmsidintern",
+ "requeststring"]]
+
+request_fields = [
+ Field("VERSION", required=True),
+ Field("REQUEST", required=True),
+ Field("WIDTH", required=True),
+ Field("HEIGHT", required=True),
+ Field("BBOX", required=True),
+ Field("LAYERS", required=True),
+ Field("SERVICE", required=True),
+ Field("FORMAT"),
+ Field("TRANSPARENT"),
+ Field("EXCEPTIONS"),
+ Field("BGCOLOR"),
+ Field("STYLES"),
+ Field("SRS"),
+ ]
+
+def parse_formdata(formdata):
+ record = Record()
+ record.set_fields(form_fields, formdata)
+ record.set_fields(request_fields, record.requeststring,
+ fieldname_normalizer=lambda s: s.upper())
+ return record
Property changes on: trunk/server/osaas/formparser.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/http/__init__.py
===================================================================
Property changes on: trunk/server/osaas/http/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/http/httpserver.py
===================================================================
--- trunk/server/osaas/http/httpserver.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/httpserver.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,119 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""HTTP server that can run in a thread and be stopped"""
+
+import socket
+import errno
+import threading
+import BaseHTTPServer
+import logging
+
+
+class HTTPServer(BaseHTTPServer.HTTPServer):
+
+ """HTTP server that can be run in a thread and be stopped"""
+
+ do_shutdown = False
+
+ def __init__(self, *args, **kw):
+ for name, value in [("access_logger_name", "httpserver.access"),
+ ("error_logger_name", "httpserver.error")]:
+ if kw.has_key(name):
+ value = kw[name]
+ del kw[name]
+ setattr(self, name, value)
+ BaseHTTPServer.HTTPServer.__init__(self, *args, **kw)
+
+ def serve_forever(self):
+ """Handle requests until self.do_shutdown is True."""
+ while not self.do_shutdown:
+ self.handle_request()
+
+ def server_close(self):
+ """Shutdown the server.
+
+ Usually this is called from a thread while another thread
+ executes the serve_forever method. In that case the other
+ thread is most likely blocked while listening for new
+ connections. After this method returns, the caller will have to
+ do something to unblock the other thread, such as creating a TCP
+ connection to the server.
+ """
+ BaseHTTPServer.HTTPServer.server_close(self)
+ self.do_shutdown = True
+
+
+class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+ def log_message(self, format, *args):
+ """Log an arbitrary message.
+
+ Overrides the base class version to write the message into the
+ logger given by the server's access_logger_name. The messge is
+ logged with INFO level.
+ """
+ logger = logging.getLogger(self.server.access_logger_name)
+ logger.info("%s - - [%s] %s",
+ self.address_string(),
+ self.log_date_time_string(),
+ format % args)
+
+ def log_error(self, *args):
+ """Log an error.
+
+ Overrides the base class method so that the message is written
+ to the logger given by the server's error_logger_name. The
+ message is logged with the ERROR level.
+ """
+ logger = logging.getLogger(self.server.error_logger_name)
+ logger.error(*args)
+
+
+class ServerThread:
+
+ """Class to run a HTTPServer instance in a thread"""
+
+ def __init__(self, server):
+ """Initializes the server thread with an instance of HTTPServer"""
+ self.server = server
+ self.server_port = self.server.server_port
+ self.server_thread = None
+
+ def start(self, daemon=False):
+ """Starts the server thread"""
+ self.server_thread = threading.Thread(target=self._serve_forever)
+ self.server_thread.setDaemon(daemon)
+ self.server_thread.start()
+
+ def _serve_forever(self):
+ """Helper method to run the server's serve_forever method in a thread"""
+ self.server.serve_forever()
+
+ def stop(self):
+ """Stops the server thread"""
+ if self.server_thread is None:
+ return
+
+ self.server.server_close()
+
+ # The server thread might be blocked while listening on the
+ # socket. Unblock it by connecting to the port.
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(("localhost", self.server_port))
+ s.close()
+ except socket.error, exc:
+ # The server may have shut down already when we try to
+ # connect, so we ignore connection failures.
+ if exc[0] == errno.ECONNREFUSED:
+ pass
+ else:
+ raise
+
+ self.server_thread.join()
+ self.server_thread = None
Property changes on: trunk/server/osaas/http/httpserver.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/http/run.py
===================================================================
--- trunk/server/osaas/http/run.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/run.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,153 @@
+#! /bin/env python
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Functions to run a httpserver as a standalone program"""
+
+import sys
+import os
+import optparse
+import logging
+import signal
+
+import httpserver
+
+
+class SIGTERMException(Exception):
+ pass
+
+
+class OptionStack(object):
+
+ def __init__(self, default_opts, file_opts, commandline_opts):
+ self.__opts = (commandline_opts, file_opts, default_opts)
+
+ def __getattr__(self, attr):
+ for opts in self.__opts:
+ if hasattr(opts, attr):
+ return getattr(opts, attr)
+ raise AttributeError(attr)
+
+ def set_file_option(self, name, value):
+ setattr(self.__opts[1], name, value)
+
+class ProgramWithOptions(object):
+
+ optparse_option_class = None
+
+ def create_option_parser(self, default_port):
+ kw = {}
+ if self.optparse_option_class is not None:
+ kw["option_class"] = self.optparse_option_class
+ parser = optparse.OptionParser(**kw)
+ parser.set_defaults(port=default_port)
+ parser.add_option("--port", type="int")
+ parser.add_option("--access-log")
+ parser.add_option("--error-log")
+ parser.add_option("--config-file")
+ return parser
+
+ def read_config_file(self, opts):
+ """Reads options from a configuratin file
+
+ The parameter opts is an OptionStack instance holding defaults
+ and values read from the command line. Derived classes wishing
+ to read config files should override this method and set options
+ in opts with its set_file_option method.
+
+ How the configuration file(s) are located and how settings are
+ read is up to derived classes. However, the base class already
+ provides the config_file option as a standard way to provide the
+ name of a configuration file on the command line.
+ """
+
+ def parse_options(self, args, default_port):
+ parser = self.create_option_parser(default_port=default_port)
+
+ default_opts = parser.get_default_values()
+ configfile_opts = optparse.Values()
+ commandline_opts, rest = parser.parse_args(args, optparse.Values())
+ opts = OptionStack(default_opts, configfile_opts, commandline_opts)
+ self.read_config_file(opts)
+ return opts, rest
+
+
+class ServerProgram(ProgramWithOptions):
+
+ """Main program of the server"""
+
+ def __init__(self):
+ self.server = None
+
+ def setup_signals(self):
+ """Set handle_term_signal as signal handler for SIGTERM"""
+ signal.signal(signal.SIGTERM, self.handle_term_signal)
+
+ def handle_term_signal(self, signum, frame):
+ """Handler for the SIGTERM signal. Throws SIGTERMException"""
+ raise SIGTERMException
+
+ def setup_logging(self, opts):
+ """Sets up the default loggers of the httpserver based on opts.
+ The function sets up the httpserver.access and httpserver.error
+ loggers. If opts has attributes access_log or error_log, they
+ are assumed to be the names of the files the corresponding
+ logger should write to. If one of the attributes does not exist
+ or is None or an empty string, the corresponding logger writes
+ to sys.stderr.
+ """
+ for logtype, format in [("access", "%(message)s"),
+ ("error",
+ "%(asctime)s %(levelname)s %(name)s %(message)s")]:
+ logger = logging.getLogger("httpserver." + logtype)
+ logger.setLevel(logging.DEBUG)
+
+ filename = getattr(opts, logtype + "_log")
+ if filename:
+ stream = open(filename, "a")
+ else:
+ stream = sys.stderr
+ hdlr = logging.StreamHandler(stream)
+ hdlr.setFormatter(logging.Formatter(format))
+ logger.addHandler(hdlr)
+
+ def instantiate_server(self, server_class, server_address, opts, **kw):
+ return server_class(server_address, **kw)
+
+ def main(self, server_class=httpserver.HTTPServer,
+ default_port=8989, bind_host="127.0.0.1", **kw):
+ opts, rest = self.parse_options(sys.argv[1:], default_port=default_port)
+
+ self.setup_signals()
+ self.setup_logging(opts)
+
+ logger = logging.getLogger("httpserver.error")
+
+ logger.info("server starting up")
+ server_address = (bind_host, opts.port)
+ self.server = self.instantiate_server(server_class, server_address,
+ opts, **kw)
+
+ print "Serving HTTP on port", self.server.server_port, "..."
+ logger.info("serving HTTP on port %d", self.server.server_port)
+
+ try:
+ self.server.serve_forever()
+ except (KeyboardInterrupt, SIGTERMException), err:
+ if isinstance(err, KeyboardInterrupt):
+ logger.info("keyboard interrupt; stopping server")
+ else:
+ logger.info("received SIGTERM; stopping server")
+ self.server.server_close()
+
+ logmessage = "server stopped"
+ print logmessage
+ logger.info(logmessage)
+
+def main(*args, **kw):
+ program = ServerProgram()
+ program.main(*args, **kw)
Property changes on: trunk/server/osaas/http/run.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/http/threadedserver.py
===================================================================
--- trunk/server/osaas/http/threadedserver.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/threadedserver.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,45 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from __future__ import nested_scopes
+
+import logging
+
+from httpserver import HTTPServer
+from threadpool import ThreadPool
+
+
+class ThreadedHTTPServer(HTTPServer):
+
+ def __init__(self, *args, **kw):
+ for name, value in [("num_threads", 5)]:
+ if kw.has_key(name):
+ value = kw[name]
+ del kw[name]
+ setattr(self, name, value)
+ HTTPServer.__init__(self, *args, **kw)
+
+ pool_logger = logging.getLogger(self.error_logger_name + ".workerpool")
+ self.thread_pool = ThreadPool(self.num_threads, lambda f: f(),
+ logger=pool_logger)
+ self.thread_pool.start()
+
+ def process_request(self, request, client_address):
+ """Put the request into the queue to be handled by a worker thread
+ """
+ def process_in_worker():
+ try:
+ self.finish_request(request, client_address)
+ self.close_request(request)
+ except:
+ self.handle_error(request, client_address)
+ self.close_request(request)
+ self.thread_pool.put(process_in_worker)
+
+ def server_close(self):
+ HTTPServer.server_close(self)
+ self.thread_pool.stop()
Property changes on: trunk/server/osaas/http/threadedserver.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/http/threadpool.py
===================================================================
--- trunk/server/osaas/http/threadpool.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/threadpool.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,85 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Simple pool of worker threads to process tasks like e.g. HTTP requests"""
+
+
+import traceback
+import threading
+import Queue
+
+
+class ThreadPool(object):
+
+ """A class to manage a set of threads that read tasks from a queue.
+
+ The threads managed by the class are started as deamon threads so
+ that it's not required to terminate them if the program using them
+ wants to exit. Still, they can be explicitly stopped with the stop
+ method.
+
+ The threads read tasks from a Queue object managed by the ThreadPool
+ instance using the queue's blocking get() method. Tasks taken from
+ the queue are then passed to the handler_function which was
+ specified then the ThreadPool instance was created. The
+ handler_function is called with one parameter, the task object
+ (which can be any object except None), and should return when the
+ task is finished. Repeatedly getting new tasks from the queue is
+ handled by the thread pool itself.
+
+ The program using the thread pool puts tasks into the queue with the
+ thread pool's put method.
+ """
+
+ def __init__(self, num_threads, handler_function, logger=None):
+ """Initialize the thread pool with a number of threads and a handler"""
+ self.queue = Queue.Queue(0)
+ self.num_threads = num_threads
+ self.handler_function = handler_function
+ self.logger = logger
+ self.threads = []
+
+ def start(self):
+ """Starts the threads"""
+ if self.logger:
+ self.logger.info("Starting %d threads", self.num_threads)
+ self.threads = []
+ for i in range(self.num_threads):
+ thread = threading.Thread(target = self._thread)
+ thread.setDaemon(1)
+ thread.start()
+ self.threads.append(thread)
+
+ def _thread(self):
+ """Takes tasks repeatedly the queue and calls the handler_function
+ This is an internal method.
+ """
+ while 1:
+ item = self.queue.get()
+ if item is None:
+ break
+ try:
+ self.handler_function(item)
+ except:
+ if self.logger:
+ self.logger.exception("Exception raised by"
+ " threadpool handler function")
+ else:
+ traceback.print_exc()
+
+ def stop(self):
+ """Stops the worker threads"""
+ if self.logger:
+ self.logger.info("Stopping threads")
+ for i in range(len(self.threads)):
+ self.put(None)
+ for thread in self.threads:
+ thread.join()
+
+ def put(self, item):
+ """Puts item into the queue to be processed by a worker thread"""
+ self.queue.put(item)
Property changes on: trunk/server/osaas/http/threadpool.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/pycompat.py
===================================================================
--- trunk/server/osaas/pycompat.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/pycompat.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,31 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+def sorted(sequence):
+ L = list(sequence)
+ L.sort()
+ return L
+
+# add missing builtins
+import __builtin__
+for name, value in [("sorted", sorted)]:
+ if not hasattr(__builtin__, name):
+ setattr(__builtin__, name, value)
+
+# add some missing functions to existing modules
+
+import base64
+def b64encode(s):
+ return base64.encodestring(s).strip()
+def b64decode(s):
+ return base64.decodestring(s)
+
+for modulename, function in [("base64", b64encode),
+ ("base64", b64decode)]:
+ module = __import__(modulename)
+ if not hasattr(module, function.__name__):
+ setattr(module, function.__name__, function)
Property changes on: trunk/server/osaas/pycompat.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/recordfilter.py
===================================================================
--- trunk/server/osaas/recordfilter.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/recordfilter.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,11 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Filter for requests"""
+
+def default_filter(record):
+ return record.SERVICE == "WMS" and record.REQUEST == "GetMap"
Property changes on: trunk/server/osaas/recordfilter.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/run.py
===================================================================
--- trunk/server/osaas/run.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/run.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,36 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from osaas.http.run import ServerProgram
+from osaas.server import OSAASServer
+from osaas.config import OSAASOption, read_config_file
+
+class OSAASServerProgram(ServerProgram):
+
+ optparse_option_class = OSAASOption
+
+ def create_option_parser(self, **kw):
+ self.parser = ServerProgram.create_option_parser(self, **kw)
+ for name in ["port", "access-log", "error-log"]:
+ path = "OSAASConfig/" + "".join([part.capitalize()
+ for part in name.split("-")])
+ self.parser.get_option("--" + name).xml_path = path
+ self.parser.add_option("--userdb-file", xml_path="OSAASConfig/UserDB")
+ return self.parser
+
+ def instantiate_server(self, server_class, server_address, opts, **kw):
+ return server_class(server_address, userdbfile=opts.userdb_file, **kw)
+
+ def read_config_file(self, opts):
+ if opts.config_file is not None:
+ read_config_file(opts.config_file, self.parser.option_list,
+ opts.set_file_option)
+
+
+def main():
+ program = OSAASServerProgram()
+ program.main(server_class=OSAASServer)
Property changes on: trunk/server/osaas/run.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/server.py
===================================================================
--- trunk/server/osaas/server.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/server.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,132 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+import logging
+import cgi
+import base64
+
+from BaseHTTPServer import BaseHTTPRequestHandler
+
+from osaas.http.httpserver import HTTPRequestHandler
+from osaas.http.threadedserver import ThreadedHTTPServer
+from osaas.http.threadpool import ThreadPool
+from dbbackend import dbhandler
+from recordfilter import default_filter
+from userdb import UserDB
+from formparser import parse_formdata, FormParserException
+
+
+def parse_basic_authentication(authorization):
+ """Parse a HTTP Basic authentication header and return (username, password).
+ If the header cannot be parsed a ValueError exception is raised.
+ """
+ if authorization.startswith("Basic "):
+ credentials_b64 = authorization.split()[1]
+ credentials = base64.b64decode(credentials_b64)
+ fields = credentials.split(":", 1)
+ if len(fields) == 2:
+ return fields
+ else:
+ raise ValueError("Malformed Basic auth informaion")
+ else:
+ raise ValueError("Unsupported authorization scheme")
+
+
+class OSAASRequestHandler(HTTPRequestHandler):
+
+ responses = HTTPRequestHandler.responses.copy()
+ responses.update({
+ 411: ('Length Required', 'Client must specify Content-Length.'),
+ 415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
+ })
+
+ def do_POST(self):
+ try:
+ self.handle_owsaccounting_formdata()
+ except:
+ self.log_exception("An unexpected exception occurred;"
+ " sending 500 response")
+ self.send_error(500)
+
+ def handle_owsaccounting_formdata(self):
+ if self.path != "/owsaccounting/":
+ self.send_error(404)
+ return
+
+ # check authentication
+ if not self.check_user():
+ return
+
+ # check for the expected content type
+ content_type = self.headers.getheader("Content-Type")
+ if content_type != "application/x-www-form-urlencoded":
+ self.send_error(415)
+ return
+
+ # If any data is associated with the request read it
+ length = int(self.headers.getheader("Content-Length", "0"))
+ if length:
+ # FIXME: check whether length bytes were read
+ data = self.rfile.read(length)
+ else:
+ self.send_error(411)
+ return
+
+ try:
+ record = parse_formdata(data)
+ except FormParserException, err:
+ self.send_error(400, "Bad request: %s" % err)
+ return
+
+ if default_filter(record):
+ self.log_debug("passing record to db thread")
+ self.server.dbthreadpool.put(record)
+ else:
+ self.log_debug("record (SERVICE=%r, REQUEST=%r) was filtered out",
+ record.SERVICE, record.REQUEST)
+
+ self.send_response(200)
+
+ def check_user(self):
+ authorization = self.headers.getheader("Authorization")
+ try:
+ if authorization:
+ username, password = parse_basic_authentication(authorization)
+ if self.server.userdb.check_credentials(username, password):
+ return True
+ except:
+ self.log_exception("Error parsing Authorization header")
+ self.send_response(401)
+ self.send_header("WWW-Authenticate", 'Basic realm="osaas"')
+ return False
+
+ def log_debug(self, *args, **kw):
+ logger = logging.getLogger(self.server.error_logger_name)
+ logger.debug(*args, **kw)
+
+ def log_exception(self, *args, **kw):
+ logger = logging.getLogger(self.server.error_logger_name)
+ logger.debug(exc_info=1, *args, **kw)
+
+
+class OSAASServer(ThreadedHTTPServer):
+
+ def __init__(self, serveraddress, RequestHandlerClass=OSAASRequestHandler,
+ dbhandler=dbhandler, userdbfile=None,
+ **kw):
+ ThreadedHTTPServer.__init__(self, serveraddress, RequestHandlerClass,
+ **kw)
+ self.userdb = UserDB()
+ if userdbfile:
+ self.userdb.read(userdbfile)
+ logger = logging.getLogger(self.error_logger_name + ".dbpool")
+ self.dbthreadpool = ThreadPool(1, dbhandler, logger=logger)
+ self.dbthreadpool.start()
+
+ def server_close(self):
+ ThreadedHTTPServer.server_close(self)
+ self.dbthreadpool.stop()
Property changes on: trunk/server/osaas/server.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/osaas/userdb.py
===================================================================
--- trunk/server/osaas/userdb.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/userdb.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,76 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Simple User database"""
+
+import sha
+import base64
+import errno
+
+SSHA_PREFIX = "{SSHA}"
+
+def random_salt(length):
+ random = open("/dev/urandom")
+ try:
+ return random.read(length)
+ finally:
+ random.close()
+
+def encode_ssha(password, salt):
+ digester = sha.new(password)
+ digester.update(salt)
+ return SSHA_PREFIX + base64.b64encode(digester.digest() + salt)
+
+def check_password(password, hashed):
+ if hashed.startswith(SSHA_PREFIX):
+ salt = base64.b64decode(hashed[len(SSHA_PREFIX):])[20:]
+ return encode_ssha(password, salt) == hashed
+ else:
+ return False
+
+class UserDB(object):
+
+ def __init__(self, users=()):
+ self.users = {}
+ for username, password in users:
+ self.add_user(username, password)
+
+ def read(self, filename):
+ try:
+ pwfile = open(filename)
+ except IOError, err:
+ # If the file doesn't exist, treat it as an empty file
+ if err.errno == errno.ENOENT:
+ return
+ try:
+ while 1:
+ line = pwfile.readline()
+ if not line:
+ break
+
+ fields = line.strip().split(":", 1)
+ if len(fields) == 2:
+ self.users[fields[0]] = fields[1]
+ else:
+ # FIXME: warn about this
+ pass
+ finally:
+ pwfile.close()
+
+ def write(self, filename):
+ pwfile = open(filename, "w")
+ for username, pwhash in sorted(self.users.items()):
+ pwfile.write("%s:%s\n" % (username, pwhash))
+ pwfile.close()
+
+ def add_user(self, username, password):
+ self.users[username] = encode_ssha(password, random_salt(8))
+
+ def check_credentials(self, username, password):
+ if self.users.has_key(username):
+ return check_password(password, self.users[username])
+ return False
Property changes on: trunk/server/osaas/userdb.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/startosaas.py
===================================================================
--- trunk/server/startosaas.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/startosaas.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,11 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from osaas.run import main
+
+if __name__ == "__main__":
+ main()
Property changes on: trunk/server/startosaas.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/runtests.py
===================================================================
--- trunk/server/test/runtests.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/runtests.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,76 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""
+Main entry point for the test suite.
+
+Just run this file as a python script to execute all tests
+"""
+
+import os
+import sys
+import unittest
+import getopt
+
+try:
+ __file__
+except NameError:
+ __file__ = sys.argv[0]
+test_dir = os.path.dirname(__file__)
+sys.path.append(os.path.join(test_dir, os.pardir))
+
+def find_test_modules(dirname, package = None):
+ """Return a list the names of the test modules in the directory dirname
+
+ The return value is a list of names that can be passed to
+ unittest.defaultTestLoader.loadTestsFromNames. Each name of the
+ list is the name of a pure python module, one for each file in
+ dirname whose name starts with 'test' and ends with '.py'.
+
+ The optional parameter package should be the name of the python
+ package whose directory is dirname. If package is given all names
+ in the returned list will be prefixed with package and a dot.
+ """
+ if package:
+ prefix = package + "."
+ else:
+ prefix = ""
+
+ suffix = ".py"
+ return [prefix + name[:-len(suffix)]
+ for name in os.listdir(dirname)
+ if name.startswith("test") and name.endswith(suffix)]
+
+
+def main():
+ """Run all the tests in the test suite"""
+ verbosity = 1
+
+ opts, rest = getopt.getopt(sys.argv[1:], "v")
+ for optchar, optvalue in opts:
+ if optchar == "-v":
+ verbosity = 2
+ else:
+ raise RuntimeError("Unknown option %r" % optchar)
+
+ # Build the list of test names. If names were given on the command
+ # line, run exactly those. Othwerwise build a default list of
+ # names.
+ if rest:
+ names = rest
+ else:
+ # All Python files starting with 'test' in the current directory
+ # and some directories in Extensions contain test cases.
+ names = find_test_modules(test_dir)
+ suite = unittest.defaultTestLoader.loadTestsFromNames(names)
+ runner = unittest.TextTestRunner(verbosity=verbosity)
+ result = runner.run(suite)
+ sys.exit(not result.wasSuccessful())
+
+
+if __name__ == "__main__":
+ main()
Property changes on: trunk/server/test/runtests.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/serversupport.py
===================================================================
--- trunk/server/test/serversupport.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/serversupport.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,137 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Support code for tests involving osaas.http.httpserver"""
+
+import logging
+import unittest
+import httplib
+import re
+import time
+from math import floor
+import StringIO
+
+
+import osaas.http.httpserver as httpserver
+
+
+class HTTPRequestHandler(httpserver.HTTPRequestHandler):
+
+ def do_GET(self):
+ """Handle a GET request"""
+ for path, headers, data in self.server.contents:
+ if path == self.path:
+ self.send_response(200)
+ for header, value in headers:
+ self.send_header(header, value)
+ self.send_header("Content-Length", len(data))
+ self.end_headers()
+ self.wfile.write(data)
+ return
+ else:
+ self.send_error(404)
+
+
+class HTTPServer(httpserver.HTTPServer):
+
+ def __init__(self, contents, server_address=("127.0.0.1", 0),
+ RequestHandlerClass=HTTPRequestHandler, **kw):
+ self.contents = contents
+ httpserver.HTTPServer.__init__(self, server_address,
+ RequestHandlerClass, **kw)
+
+
+class LoggerMixin:
+
+ def new_logger(self, logger_name, format="%(message)s"):
+ """Create a new logger with some default configuration.
+
+ The return value is the StringIO object used as the stream into
+ which the logger writes.
+ """
+ logger = logging.getLogger(logger_name)
+ logger.setLevel(logging.DEBUG)
+ logger.propagate = False
+ stream = StringIO.StringIO()
+ hdlr = logging.StreamHandler(stream)
+ hdlr.setFormatter(logging.Formatter(format))
+ logger.addHandler(hdlr)
+ return stream
+
+
+class ServerTest(unittest.TestCase, LoggerMixin):
+
+ access_logger_name = "test.access"
+ error_logger_name = "test.error"
+
+ def setUp(self):
+ self.access_log_stream = self.new_logger(self.access_logger_name)
+ self.error_log_stream = self.new_logger(self.error_logger_name)
+
+ server = self.create_server(("127.0.0.1", 0),
+ access_logger_name=self.access_logger_name,
+ error_logger_name=self.error_logger_name)
+ server.test_case = self
+ self.server = httpserver.ServerThread(server)
+ self.server.start(daemon=True)
+
+ self.test_start_time = time.time()
+
+ def create_server(self, *args, **kw):
+ return httpserver.HTTPServer(*args, **kw)
+
+ def tearDown(self):
+ self.server.stop()
+
+ def reset_access_log(self):
+ self.access_log_stream.truncate(0)
+
+ def check_log(self, expected_lines):
+ log_lines = self.access_log_stream.getvalue().splitlines()
+
+ for idx in range(len(log_lines)):
+ line = log_lines[idx]
+
+ # extract the timestamp from the line so that we can check
+ # the contents of the line without the ever changing time
+ # stamp and check the timestamp value separately
+ match = re.search(r"\[([^]]+)\]", line)
+ if match:
+ line = line[:match.start(1)] + line[match.end(1):]
+
+ # Check time stamps. Round both time stamps down to the
+ # nearest second. Otherwise start_time might actually be
+ # larger than timestamp because timestamp only has 1s
+ # accuracy
+ # FIXME: enforce "C" locale for strptime
+ timestamp = time.mktime(time.strptime(match.group(1),
+ "%d/%b/%Y %H:%M:%S"))
+ self.failUnless(floor(timestamp) >= floor(self.test_start_time))
+
+ log_lines[idx] = line
+
+ self.assertEquals(log_lines, expected_lines)
+
+
+ def run_simple_http_test(self, path, status, access_log=None,
+ method="GET", extra_headers=(), body=None):
+ self.reset_access_log()
+ http = httplib.HTTPConnection("localhost", self.server.server_port)
+ http.putrequest(method, path)
+ for header, value in extra_headers:
+ http.putheader(header, value)
+ http.endheaders()
+ if body is not None:
+ http.send(body)
+ response = http.getresponse()
+ self.assertEquals(response.status, status)
+
+ if access_log is None:
+ access_log = ['localhost - - [] "%s %s HTTP/1.1" %d -'
+ % (method, path, status)]
+ self.check_log(access_log)
+ return response.read()
Property changes on: trunk/server/test/serversupport.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/support.py
===================================================================
--- trunk/server/test/support.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/support.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,60 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Support code for the test cases"""
+
+import os
+
+def create_temp_dir():
+ """Create a temporary directory for the test-suite and return its name.
+
+ The temporary directory is always called temp and is created in the
+ directory where the support module is located.
+
+ If the temp directory already exists, just return the name.
+ """
+ name = os.path.abspath(os.path.join(os.path.dirname(__file__), "temp"))
+
+ # if the directory already exists, we're done
+ if os.path.isdir(name):
+ return name
+
+ # create the directory
+ os.mkdir(name)
+ return name
+
+
+class FileTestMixin:
+
+ """Mixin class for tests that use files in the temporary directory
+ """
+
+ def temp_file_name(self, basename, remove=False):
+ """Return the full name of the file named basename in the temp. dir.
+ If the remove parameter is true, the file is removed if already exists.
+ """
+ filename = os.path.join(create_temp_dir(), basename)
+ if remove and os.path.exists(filename):
+ os.remove(filename)
+ return filename
+
+ def create_temp_file(self, basename, contents, mode = None):
+ """Create a file in the temp directory"""
+ filename = self.temp_file_name(basename)
+ file = open(filename, "w")
+ file.write(contents)
+ file.close()
+ if mode is not None:
+ os.chmod(filename, mode)
+ return filename
+
+
+class AttributeTestMixin:
+
+ def check_attributes(self, obj, **attrs):
+ for key, value in attrs.items():
+ self.assertEquals(getattr(obj, key), value)
Property changes on: trunk/server/test/support.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_config.py
===================================================================
--- trunk/server/test/test_config.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_config.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,83 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+import unittest
+
+from support import FileTestMixin
+from osaas.config import OSAASOption, ConfigXMLPathError
+from osaas.run import OSAASServerProgram
+
+class TestOSAASOption(unittest.TestCase):
+
+ def test_xml_path(self):
+ opt = OSAASOption("--port", type="int", xml_path="OSAASConfig/Port")
+ self.assertEquals(opt.xml_path, "OSAASConfig/Port")
+
+
+
+class ConfigParserTest(unittest.TestCase, FileTestMixin):
+
+ config = ""
+
+ def setUp(self):
+ self.filename = self.create_temp_file(self.id() + ".cfg", self.config)
+
+
+class TestConfigParsing(ConfigParserTest):
+
+ config = """\
+<?xml version='1.0'?>
+<OSAASConfig>
+ <Port>6688</Port>
+ <AccessLog>/var/log/osaas-access.log</AccessLog>
+ <ErrorLog>/var/log/osaas-error.log</ErrorLog>
+</OSAASConfig>
+"""
+
+ def test(self):
+ prog = OSAASServerProgram()
+ opts, rest = prog.parse_options(["--config-file=%s" % self.filename],
+ default_port=9090)
+ self.assertEquals(opts.port, 6688)
+ self.assertEquals(opts.access_log, "/var/log/osaas-access.log")
+ self.assertEquals(opts.error_log, "/var/log/osaas-error.log")
+
+
+class TestConfigParsing_MissingEntries(ConfigParserTest):
+
+ config = """\
+<?xml version='1.0'?>
+<OSAASConfig>
+ <ErrorLog>/var/log/osaas-error.log</ErrorLog>
+</OSAASConfig>
+"""
+
+ def test(self):
+ prog = OSAASServerProgram()
+ opts, rest = prog.parse_options(["--config-file=%s" % self.filename],
+ default_port=9090)
+ self.assertEquals(opts.port, 9090)
+ self.assertEquals(opts.access_log, None)
+ self.assertEquals(opts.error_log, "/var/log/osaas-error.log")
+
+
+class TestConfigParsing_AmbiguousEntries(ConfigParserTest):
+
+ config = """\
+<?xml version='1.0'?>
+<OSAASConfig>
+ <ErrorLog>/var/log/osaas-error.log</ErrorLog>
+ <ErrorLog>/var/log/osaas-error2.log</ErrorLog>
+</OSAASConfig>
+"""
+
+ def test(self):
+ prog = OSAASServerProgram()
+ self.assertRaises(ConfigXMLPathError,
+ prog.parse_options,
+ ["--config-file=%s" % self.filename],
+ default_port=9090)
Property changes on: trunk/server/test/test_config.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_formparser.py
===================================================================
--- trunk/server/test/test_formparser.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_formparser.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,142 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.formparser"""
+
+import sys
+import unittest
+import traceback
+
+import support
+
+from osaas.formparser import parse_formdata, FormParserException
+
+
+class FormParsingTests(unittest.TestCase, support.AttributeTestMixin):
+
+ def test_convert_form_urlencoded(self):
+ formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"])
+ self.check_attributes(parse_formdata(formdata),
+ user="karl",
+ responsetime="2007-06-01T12:14:16Z",
+ wmsidextern="example.com/cgi-bin/fridawms",
+ wmsidintern="localhost/cgi-bin/myfrida",
+ requeststring=("VERSION=1.1.1&REQUEST=GetMap"
+ "&FORMAT=image/png"
+ "&TRANSPARENT=TRUE"
+ "&WIDTH=460&HEIGHT=348"
+ "&EXCEPTIONS=application/vnd.ogc.se_xml"
+ "&BGCOLOR=0xffffff"
+ "&BBOX=0.0,0.0,460.0,348.0"
+ "&LAYERS=gewaesser&STYLES="
+ "&SRS=EPSG:4326"
+ "&SERVICE=WMS"),
+ VERSION="1.1.1",
+ REQUEST="GetMap",
+ FORMAT="image/png",
+ TRANSPARENT="TRUE",
+ WIDTH="460",
+ HEIGHT="348",
+ EXCEPTIONS="application/vnd.ogc.se_xml",
+ BGCOLOR="0xffffff",
+ BBOX="0.0,0.0,460.0,348.0",
+ LAYERS="gewaesser",
+ STYLES="",
+ SRS="EPSG:4326",
+ SERVICE="WMS")
+
+ def check_exception(self, exception_class, exception_message,
+ function, *args, **kw):
+ try:
+ function(*args, **kw)
+ except exception_class, err:
+ self.assertEquals(str(err), exception_message)
+ except:
+ self.fail("Unexpected exception:\n%s"
+ % "".join(traceback.format_exception(*sys.exc_info())))
+
+
+ def test_missing_required_parameters_in_formdata(self):
+ # formdata is missing the required field "user"
+ formdata = "&".join(["responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"])
+ self.check_exception(FormParserException,
+ "formdata is missing required field 'user'",
+ parse_formdata, formdata)
+
+ def test_missing_required_parameters_in_requeststring(self):
+ # requeststring is missing the required field "REQUEST"
+ formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"])
+ self.check_exception(FormParserException,
+ "formdata is missing required field 'REQUEST'",
+ parse_formdata, formdata)
+
+ def test_duplicate_parameters(self):
+ # The responsetime occurs twice
+ formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348%26REQUEST=GetMap"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS",
+ "responsetime=2007-06-01T12:14:16Z"])
+ self.check_exception(FormParserException,
+ "only one value allowed for field 'responsetime'",
+ parse_formdata, formdata)
+
+ def test_duplicate_parameters_in_requeststring(self):
+ # The SERVICE field occurs twice in the requeststring
+ formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348%26SERVICE=WMS"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"])
+ self.check_exception(FormParserException,
+ "only one value allowed for field 'SERVICE'",
+ parse_formdata, formdata)
+
+ def test_invalid_formdata_syntax_no_equals(self):
+ # No equals sign in form field
+ formdata = "user"
+ self.check_exception(FormParserException,
+ "Error parsing form data: bad query field: 'user'",
+ parse_formdata, formdata)
Property changes on: trunk/server/test/test_formparser.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_httpserver.py
===================================================================
--- trunk/server/test/test_httpserver.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_httpserver.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,35 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.http.httpserver"""
+
+import serversupport
+
+import osaas.http.httpserver as httpserver
+
+
+class SimpleHTTPServerTests(serversupport.ServerTest):
+
+ server_contents = [
+ ("/wms", [("Content-Type", "text/plain")], "wms data"),
+ ]
+
+ def create_server(self, *args, **kw):
+ return serversupport.HTTPServer(self.server_contents, *args, **kw)
+
+ def test_get_200(self):
+ self.run_simple_http_test("/wms", 200)
+ self.assertEquals(self.error_log_stream.getvalue(), "")
+
+ def test_get_404(self):
+ self.run_simple_http_test("/something/that/doesnt/exist", 404)
+ # The case of the default messages in BaseHTTPServer has changed
+ # between Python 2.1 and 2.4. Compare lower case strings to
+ # avoid test failures because of that.
+ self.assertEquals(self.error_log_stream.getvalue().lower(),
+ 'code 404, message not found\n')
+
Property changes on: trunk/server/test/test_httpserver.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_osasserver.py
===================================================================
--- trunk/server/test/test_osasserver.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_osasserver.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,242 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.server"""
+
+import base64
+import threading
+
+import serversupport
+import support
+
+from osaas.server import OSAASServer
+
+
+class OSAASServerTests(serversupport.ServerTest, support.FileTestMixin):
+
+ def setUp(self):
+ self.userdbfile = self.create_temp_file(self.id() + "-userdb",
+ "karl:{SSHA}NGVfMfEuUjT3wFNgP0KiqWs47m9zYWx6")
+ self.authentication_headers = [
+ ("Authorization", "Basic %s" % base64.b64encode("karl:xyzzy")),
+ ]
+ serversupport.ServerTest.setUp(self)
+
+ def create_server(self, *args, **kw):
+ return OSAASServer(userdbfile=self.userdbfile, *args, **kw)
+
+ def test_post_wrong_path(self):
+ self.run_simple_http_test("/some/path", 404, method="POST",
+ extra_headers=self.authentication_headers)
+
+ def test_post_no_content_type(self):
+ self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+ extra_headers=self.authentication_headers)
+
+ def test_post_no_content_length(self):
+ extra_headers = [("Content-Type", "application/x-www-form-urlencoded")]
+ self.run_simple_http_test("/owsaccounting/", 411, method="POST",
+ extra_headers=(extra_headers
+ + self.authentication_headers))
+
+ def test_unauthenticated(self):
+ body = "requeststring=SERVICE=WMS%26REQUEST=GetMap"
+ extra_headers = [("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", len(body))]
+ self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+ extra_headers=extra_headers,
+ body=body)
+
+
+ def test_malformed_basic_authentication(self):
+ # If the authorization were successful we would get 415 errors
+ # because of the missing content type. With malformed
+ # authentication headers we should get 401.
+
+ # sanity check first: do we get 415 with correct authentication?
+ self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+ extra_headers=self.authentication_headers)
+
+ malformed = [("Authorization",
+ "Basic %s" % base64.b64encode("hans 123"))]
+ self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+ extra_headers=malformed)
+
+ def test_malformed_basic_authentication2(self):
+ # If the authorization were successful we would get 415 errors
+ # because of the missing content type. With malformed
+ # authentication headers we should get 401.
+
+ # sanity check first: do we get 415 with correct authentication?
+ self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+ extra_headers=self.authentication_headers)
+
+ malformed = [("Authorization", "Basic ")]
+ self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+ extra_headers=malformed)
+
+ def test_authentication_header_but_not_basic(self):
+ # If the authorization were successful we would get 415 errors
+ # because of the missing content type. With malformed
+ # authentication headers we should get 401.
+
+ # sanity check first: do we get 415 with correct authentication?
+ self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+ extra_headers=self.authentication_headers)
+
+ malformed = [("Authorization", "Invented data")]
+ self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+ extra_headers=malformed)
+
+
+class OSAASDBServerTests(serversupport.ServerTest, support.AttributeTestMixin,
+ support.FileTestMixin):
+
+ def setUp(self):
+ self.dbrequests = []
+ self.dbrequest_event = threading.Event()
+ self.userdbfile = self.create_temp_file(self.id() + "-userdb",
+ "karl:{SSHA}NGVfMfEuUjT3wFNgP0KiqWs47m9zYWx6")
+ self.authentication_headers = [
+ ("Authorization", "Basic %s" % base64.b64encode("karl:xyzzy")),
+ ]
+ serversupport.ServerTest.setUp(self)
+
+ def dbhandler(self, req):
+ self.dbrequests.append(req)
+ self.dbrequest_event.set()
+
+ def create_server(self, *args, **kw):
+ return OSAASServer(dbhandler=self.dbhandler, userdbfile=self.userdbfile,
+ *args, **kw)
+
+ def send_formdata_request(self, formdata, expected_status=200):
+ extra_headers = ([("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", len(formdata))]
+ + self.authentication_headers)
+ return self.run_simple_http_test("/owsaccounting/", expected_status,
+ method="POST",
+ extra_headers=extra_headers,
+ body=formdata)
+
+ def test_post_with_content(self):
+ formdata = "&".join([
+ "user=karl", "responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"])
+ self.send_formdata_request(formdata)
+ self.dbrequest_event.wait(1)
+ self.assertEquals(len(self.dbrequests), 1)
+ self.check_attributes(self.dbrequests[0],
+ user="karl",
+ responsetime="2007-06-01T12:14:16Z",
+ wmsidextern="example.com/cgi-bin/fridawms",
+ wmsidintern="localhost/cgi-bin/myfrida",
+ requeststring=("VERSION=1.1.1&REQUEST=GetMap"
+ "&FORMAT=image/png"
+ "&TRANSPARENT=TRUE"
+ "&WIDTH=460&HEIGHT=348"
+ "&EXCEPTIONS=application/vnd.ogc.se_xml"
+ "&BGCOLOR=0xffffff"
+ "&BBOX=0.0,0.0,460.0,348.0"
+ "&LAYERS=gewaesser&STYLES="
+ "&SRS=EPSG:4326"
+ "&SERVICE=WMS"),
+ VERSION="1.1.1",
+ REQUEST="GetMap",
+ FORMAT="image/png",
+ TRANSPARENT="TRUE",
+ WIDTH="460",
+ HEIGHT="348",
+ EXCEPTIONS="application/vnd.ogc.se_xml",
+ BGCOLOR="0xffffff",
+ BBOX="0.0,0.0,460.0,348.0",
+ LAYERS="gewaesser",
+ STYLES="",
+ SRS="EPSG:4326",
+ SERVICE="WMS")
+
+
+ def test_post_with_content_and_filtering(self):
+ self.send_formdata_request("&".join([
+ "user=karl", "responsetime=2007-06-01T12:14:16Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"]))
+ self.send_formdata_request("&".join([
+ "user=karl", "responsetime=2007-06-01T12:14:17Z",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetCapabilities"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"]))
+
+ self.dbrequest_event.wait(1)
+ self.assertEquals(len(self.dbrequests), 1)
+ self.check_attributes(self.dbrequests[0],
+ user="karl",
+ responsetime="2007-06-01T12:14:16Z",
+ wmsidextern="example.com/cgi-bin/fridawms",
+ wmsidintern="localhost/cgi-bin/myfrida",
+ requeststring=("VERSION=1.1.1&REQUEST=GetMap"
+ "&FORMAT=image/png"
+ "&TRANSPARENT=TRUE"
+ "&WIDTH=460&HEIGHT=348"
+ "&EXCEPTIONS=application/vnd.ogc.se_xml"
+ "&BGCOLOR=0xffffff"
+ "&BBOX=0.0,0.0,460.0,348.0"
+ "&LAYERS=gewaesser&STYLES="
+ "&SRS=EPSG:4326"
+ "&SERVICE=WMS"),
+ VERSION="1.1.1",
+ REQUEST="GetMap",
+ FORMAT="image/png",
+ TRANSPARENT="TRUE",
+ WIDTH="460",
+ HEIGHT="348",
+ EXCEPTIONS="application/vnd.ogc.se_xml",
+ BGCOLOR="0xffffff",
+ BBOX="0.0,0.0,460.0,348.0",
+ LAYERS="gewaesser",
+ STYLES="",
+ SRS="EPSG:4326",
+ SERVICE="WMS")
+
+ def test_post_with_content_with_missing_fields(self):
+ # the formdata is missing the "responsetime" field
+ formdata = "&".join([
+ "user=karl",
+ "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+ "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+ "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+ "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+ "%26WIDTH=460%26HEIGHT=348"
+ "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+ "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+ "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+ "%26SERVICE=WMS"])
+ response_text = self.send_formdata_request(formdata,
+ expected_status=400)
+ self.failUnless("formdata is missing required field 'responsetime'"
+ in response_text)
Property changes on: trunk/server/test/test_osasserver.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_run.py
===================================================================
--- trunk/server/test/test_run.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_run.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,122 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.http.run"""
+
+import sys
+import unittest
+import optparse
+import traceback
+
+from osaas.http.run import ProgramWithOptions, OptionStack
+
+
+class TestProgramWithOptions(unittest.TestCase):
+
+ def test_default_options(self):
+ prog = ProgramWithOptions()
+ opts, rest = prog.parse_options([], default_port=12345)
+ self.assertEquals(opts.port, 12345)
+ self.assertEquals(opts.error_log, None)
+ self.assertEquals(opts.access_log, None)
+
+ def test_options(self):
+ prog = ProgramWithOptions()
+ opts, rest = prog.parse_options(["--port=4321",
+ "--error-log=/var/log/testerror.log"],
+ default_port=12345)
+ self.assertEquals(opts.port, 4321)
+ self.assertEquals(opts.error_log, "/var/log/testerror.log")
+ self.assertEquals(opts.access_log, None)
+
+ def test_adding_options(self):
+ class Program(ProgramWithOptions):
+ def create_option_parser(self, **kw):
+ parser = ProgramWithOptions.create_option_parser(self, **kw)
+ parser.add_option("--num-threads", type="int")
+ parser.set_defaults(num_threads=5)
+ return parser
+
+ # defaults
+ prog = Program()
+ opts, rest = prog.parse_options([], default_port=12345)
+ self.assertEquals(opts.port, 12345)
+ self.assertEquals(opts.error_log, None)
+ self.assertEquals(opts.access_log, None)
+ self.assertEquals(opts.num_threads, 5)
+
+ # some options supplied
+ prog = Program()
+ opts, rest = prog.parse_options(["--port=6543", "--num-threads=10"],
+ default_port=12345)
+ self.assertEquals(opts.port, 6543)
+ self.assertEquals(opts.error_log, None)
+ self.assertEquals(opts.access_log, None)
+ self.assertEquals(opts.num_threads, 10)
+
+ def test_read_config_file_method(self):
+ class Program(ProgramWithOptions):
+ def read_config_file(self, opts):
+ opts.set_file_option("port", 9988)
+ opts.set_file_option("access_log", "/home/test/access.log")
+
+ prog = Program()
+ opts, rest = prog.parse_options(["--port=8877"], default_port=12345)
+
+ # The port has been given on the command line, so that value
+ # should win:
+ self.assertEquals(opts.port, 8877)
+
+ # The access_log has been set in read_config_file but not on the
+ # command line, so the value from read_config_file wins:
+ self.assertEquals(opts.access_log, "/home/test/access.log")
+
+ # The error_log has only been set in the defaults (with the
+ # default default value of None).
+ self.assertEquals(opts.error_log, None)
+
+ def test_optparse_option_class(self):
+ class MyOption(optparse.Option):
+ ATTRS = optparse.Option.ATTRS[:]
+ ATTRS.extend(["extra_arg"])
+
+ class Program(ProgramWithOptions):
+ optparse_option_class = MyOption
+ def create_option_parser(self, **kw):
+ parser = ProgramWithOptions.create_option_parser(self, **kw)
+ # the default option class will raise an error when an
+ # unknown argument like "extra_arg" is passed.
+ parser.add_option("--some-option", extra_arg=True)
+ return parser
+
+ prog = Program()
+ try:
+ prog.parse_options([], default_port=12345)
+ except:
+ self.fail("Unexpected exception when parsing options:\n%s"
+ % "".join(traceback.format_exception(*sys.exc_info())))
+
+class TestOptionStack(unittest.TestCase):
+
+ def test(self):
+ parser = optparse.OptionParser()
+ parser.add_option("--port", type="int")
+ parser.add_option("--config-file")
+ parser.add_option("--log-file")
+ parser.set_defaults(port=1234)
+
+ default_opts = parser.get_default_values()
+ configfile_opts = optparse.Values()
+ commandline_opts, rest = parser.parse_args(["--config-file=/etc/cfg"],
+ optparse.Values())
+ opts = OptionStack(default_opts, configfile_opts, commandline_opts)
+
+ opts.set_file_option("log_file", "/var/log/my.log")
+
+ self.assertEquals(opts.port, 1234)
+ self.assertEquals(opts.config_file, "/etc/cfg")
+ self.assertEquals(opts.log_file, "/var/log/my.log")
Property changes on: trunk/server/test/test_run.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_threadedserver.py
===================================================================
--- trunk/server/test/test_threadedserver.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_threadedserver.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,120 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.http.threadedserver"""
+
+import httplib
+from urlparse import urlparse
+from threading import Event
+
+from osaas.http.httpserver import HTTPRequestHandler
+from osaas.http.threadedserver import ThreadedHTTPServer
+
+from serversupport import ServerTest
+
+class BlockingRequestHandler(HTTPRequestHandler):
+
+ """RequestHandler that blocks on event object provided by the test case.
+ See the actual test methods for documentation on how the tests and
+ the request handler interact
+ """
+
+ def do_GET(self):
+ try:
+ events = self.server.test_case.events[self.path]
+ except KeyError:
+ self.send_error(404)
+ return
+
+ events[0].wait()
+
+ try:
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(self.path)
+ finally:
+ # set events[1] in a finally block to make user it is always
+ # set. Also, it must be set after the response has been
+ # sent. Otherwise it can happen that the log entry is not
+ # written until the another thread has written a log entry
+ # which can make the test cases fail
+ events[1].set()
+
+
+class SimpleHTTPServerTests(ServerTest):
+
+ def setUp(self):
+ self.workerpool_log_stream = self.new_logger(self.error_logger_name
+ + ".workerpool")
+ ServerTest.setUp(self)
+
+ def create_server(self, *args, **kw):
+ return ThreadedHTTPServer(RequestHandlerClass=BlockingRequestHandler,
+ *args, **kw)
+
+ def test_get_multiple(self):
+ """Test that multiple concurrent requests can be handled out of order"""
+ # The test basically sends two requests to the server and tests
+ # whether they have been processed in reverse order. For this
+ # to work the test has to cooperate with the request handlers to
+ # make sure the first request is accepted by the serve but not
+ # actually processed until the second request has been
+ # processed. This is made more complex by the desire to make
+ # sure that the test runs to completion even if the server
+ # implementation can only process requests in order of arrival.
+ #
+ # Even if the server only processes requests in order, the
+ # server and therefore the handlers run in their own thread but
+ # in the same process as the test case, so we can use lock based
+ # thread synchronization mechanisms. In this case we use Event
+ # objects stored in the test case's instance variable events.
+ # The handlers can access the test case object through the serve
+ # object (see setUp).
+ #
+ # The events attribute is a dictionary mapping the paths used in
+ # HTTP GET requests to pairs of Event objects. The test case
+ # creates two such pairs and sends corresponding GET requests to
+ # the server. The handlers block on the first event object of
+ # the relevant pair. The test case signals the first event of
+ # the request sent last and then waits for the second event with
+ # a timeout. The requests will signal the second event after
+ # the first event has been signaled. Normally, if the events
+ # are processed in parallel, the request startet second will
+ # already be waiting for the event and immediatly signal the
+ # other event. If the requests are processed in order, the test
+ # case will wait until timeout. In both cases the test case
+ # then signals the event of the first request. Afterwards the
+ # responses of both requests are read and the log file is
+ # checked whether the requests were actually processed in the
+ # expected order.
+
+ self.events = {}
+ eventnames = ["/event1", "/event2"]
+
+ connections = {}
+ for name in eventnames:
+ self.events[name] = (Event(), Event())
+ http = httplib.HTTPConnection("localhost", self.server.server_port)
+ http.request("GET", name)
+ connections[name] = http
+
+ # unblock the second event first and then the first event and
+ # wait for it to report back. The unblock the first event.
+ self.events[eventnames[1]][0].set()
+ self.events[eventnames[1]][1].wait(1.0)
+ self.events[eventnames[0]][0].set()
+
+ for name in eventnames:
+ http = connections[name]
+ response = http.getresponse()
+ self.assertEquals(response.status, 200)
+ self.assertEquals(response.read(), name)
+
+ self.check_log(['localhost - - [] "GET /event2 HTTP/1.1" 200 -',
+ 'localhost - - [] "GET /event1 HTTP/1.1" 200 -'])
+ self.assertEquals(self.workerpool_log_stream.getvalue(),
+ "Starting 5 threads\n")
Property changes on: trunk/server/test/test_threadedserver.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_threadpool.py
===================================================================
--- trunk/server/test/test_threadpool.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_threadpool.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,65 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for ThreadPool"""
+
+from __future__ import nested_scopes
+
+import logging
+import unittest
+
+from serversupport import LoggerMixin
+
+from osaas.http.threadpool import ThreadPool
+
+class TestThreadPool(unittest.TestCase, LoggerMixin):
+
+ def setUp(self):
+ self.log_stream = self.new_logger("threadpool",
+ format="%(levelname)s %(name)s %(message)s")
+ self.logger = logging.getLogger("threadpool")
+
+ def test_threadpool_start_stop_logging(self):
+ def handler(item):
+ pass
+ pool = ThreadPool(2, handler, logger=self.logger)
+ pool.start()
+ pool.stop()
+ self.assertEquals(self.log_stream.getvalue(),
+ "INFO threadpool Starting 2 threads\n"
+ "INFO threadpool Stopping threads\n")
+
+ def test_threadpool_error_handling(self):
+ # If the handler function of a thread pool raises an exception,
+ # the thread must not stop. It must continue to take items out
+ # of the thread pool's queue and process them. To test this we
+ # use a handler function that records whether it has been called
+ # and then raises an exception. We put more items into the
+ # thread pool's queue than there are threads in the queue. In
+ # the end, after the thread pool has been stopped, the handler
+ # function must have been called once for each item put into the
+ # queue.
+ record = []
+ def error(item):
+ record.append(item)
+ raise RuntimeError
+
+ pool = ThreadPool(2, error, logger=self.logger)
+ pool.start()
+
+ for i in range(3):
+ pool.put(i)
+
+ pool.stop()
+ self.assertEquals(sorted(record), range(3))
+
+ # rudimentary check that the errors were logged.
+ self.log_stream.seek(0)
+ self.assertEquals([line.strip() for line in self.log_stream.readlines()
+ if line.startswith("ERROR")],
+ ["ERROR threadpool Exception raised by"
+ " threadpool handler function"] * 3)
Property changes on: trunk/server/test/test_threadpool.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
Added: trunk/server/test/test_userdb.py
===================================================================
--- trunk/server/test/test_userdb.py 2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_userdb.py 2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,83 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for UserDB"""
+
+import sys
+import os
+import unittest
+import traceback
+
+from support import FileTestMixin
+
+from osaas.userdb import UserDB, encode_ssha, check_password
+
+
+class TestSSHA1(unittest.TestCase, FileTestMixin):
+
+ def test_encode_ssha(self):
+ self.assertEquals(encode_ssha("secret", "salz"),
+ "{SSHA}uGAUfJr9yps6jrPoJXwpSFb+ndxzYWx6")
+
+ def test_check_password_ssha_correct_password(self):
+ self.failUnless(check_password("secret",
+ "{SSHA}uGAUfJr9yps6jrPoJXwpSFb+ndxzYWx6"))
+
+ def test_check_password_ssha_wrong_password(self):
+ self.failIf(check_password("secret",
+ "{SSHA}ki+V0/wTlQFLv2lL1RShgx5O1EFzYWx6"))
+
+class TestUserDB(unittest.TestCase):
+
+ def test_valid_password(self):
+ db = UserDB([("user", "geheim")])
+ self.failUnless(db.check_credentials("user", "geheim"))
+
+ def test_invalid_password(self):
+ db = UserDB([("user", "geheim")])
+ self.failIf(db.check_credentials("user", "geraten"))
+
+ def test_unknown_user(self):
+ db = UserDB([("user", "geheim")])
+ self.failIf(db.check_credentials("irgendwer", "geraten"))
+
+ def test_add_user(self):
+ db = UserDB([])
+ self.failIf(db.check_credentials("arthur", "42"))
+ db.add_user("arthur", "42")
+ self.failUnless(db.check_credentials("arthur", "42"))
+
+
+class TestUserDBFileIO(unittest.TestCase, FileTestMixin):
+
+ contents = """\
+hans:{SSHA}l7c79AuF2IRrdNUBBCDH6Xs3adpsaW51eA==
+karl:{SSHA}uGAUfJr9yps6jrPoJXwpSFb+ndxzYWx6
+"""
+
+ def setUp(self):
+ self.filename = self.create_temp_file(self.id(), self.contents)
+ self.userdb = UserDB()
+ self.userdb.read(self.filename)
+
+ def test_reading(self):
+ self.failUnless(self.userdb.check_credentials("hans", "tux"))
+ self.failUnless(self.userdb.check_credentials("karl", "secret"))
+
+ def test_reading_and_writing(self):
+ filename = self.temp_file_name(self.id() + "-write")
+ self.userdb.write(filename)
+ self.assertEquals(open(filename).read(), self.contents)
+
+ def test_nonexisting_file(self):
+ os.remove(self.filename)
+ db = UserDB()
+ try:
+ db.read(self.filename)
+ except:
+ self.fail("Error when opening non-existing file:\n%s"
+ % "".join(traceback.format_exception(*sys.exc_info())))
Property changes on: trunk/server/test/test_userdb.py
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ native
More information about the Osaas-commits
mailing list