[Mpuls-commits] r5313 - base/trunk/mpulsweb/lib

scm-commit@wald.intevation.org scm-commit at wald.intevation.org
Fri Sep 9 19:02:12 CEST 2011


Author: bh
Date: 2011-09-09 19:02:10 +0200 (Fri, 09 Sep 2011)
New Revision: 5313

Added:
   base/trunk/mpulsweb/lib/metaclient.py
Log:
Move meta protocol client library from WASKU to mpulsweb.
The file comes from wasku-web revision 390:1334ddc05a11


Added: base/trunk/mpulsweb/lib/metaclient.py
===================================================================
--- base/trunk/mpulsweb/lib/metaclient.py	2011-09-09 14:51:07 UTC (rev 5312)
+++ base/trunk/mpulsweb/lib/metaclient.py	2011-09-09 17:02:10 UTC (rev 5313)
@@ -0,0 +1,349 @@
+import socket
+import httplib
+import base64
+import re
+import logging
+
+import simplejson
+import OpenSSL.SSL as SSL
+
+from mpulsweb.lib.translation import _
+
+
+log = logging.getLogger(__name__)
+
+
+class MetaException(Exception):
+
+    """Exception for meta specific errors"""
+
+
+class MetaConnectionError(MetaException):
+
+    """Exception raised when the meta server connection cannot be established.
+    """
+
+
+class MetaProtocolException(MetaException):
+
+    """Base class for all exceptions originating from meta API protocol"""
+
+
+class MetaUnauthorized(MetaProtocolException):
+
+    """Raised if the meta server sent a 401 Unauthorized response."""
+
+
+class UnknownMetaCase(MetaProtocolException):
+
+    """
+    Raised if the meta case referenced in the request does not exist.
+    """
+
+
+class UnknownProjectPart(MetaProtocolException):
+
+    """
+    Raised if the project part referenced in the request does not exist.
+    """
+
+
+class MetaCaseAnonymized(MetaProtocolException):
+
+    """
+    Raised if the meta case referenced in the request has been anonymized.
+    Anonymized cases don't exist as such anymore, so this exception is
+    derived from UnknownMetaCase
+    """
+
+
+class MetaCasePending(MetaProtocolException):
+
+    """
+    Raised if the meta case has been marked for deletion or anonymization.
+    """
+
+
+
+class SocketHTTPConnection(httplib.HTTPConnection):
+
+    """Extends HTTPConnection to accept a predefined socket-like object.
+    The base class tries to establish the connection itself, which is
+    awkward given all the different ways a connection can be
+    established, such as directly per tcp, or SSL over an HTTPs proxy.
+    """
+
+    def __init__(self, sock, host, port=None, *args, **kw):
+        httplib.HTTPConnection.__init__(self, host, port, *args, **kw)
+        self.sock = sock
+
+    def connect(self):
+        pass
+
+
+def connect_tcp(host, port, log_debug=log.debug):
+    """Establish a TCP connection to port port on host.
+    The return value is the socket object for the connection
+    """
+    msg = "getaddrinfo returns an empty list"
+    for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
+        af, socktype, proto, canonname, sa = res
+        try:
+            sock = socket.socket(af, socktype, proto)
+            log_debug("connect: (%s, %s)", host, port)
+            sock.connect(sa)
+        except socket.error, msg:
+            log_debug("connect fail:", host, port)
+            if sock:
+                sock.close()
+            sock = None
+            continue
+        break
+    if not sock:
+        raise socket.error, msg
+
+    return sock
+
+
+class OpenSSLSocket(httplib.FakeSocket):
+
+    """pyOpenSSL specific version of httplib.FakeSocket.
+
+    The OpenSSL.SSL.Connection object behaves a little different than
+    the ssl objects of Python's standard library for which FakeSocket
+    was written, so FakeSocket has to be modified a little.
+    """
+
+    def sendall(self, stuff, flags=0):
+        return self._ssl.sendall(stuff, flags)
+
+
+def format_name(name):
+    """Format an OpenSSL X509Name object for debugging and logging output."""
+    return "/".join("%s=%s" % pair for pair in name.get_components())
+
+def verify_cb(conn, cert, errnum, depth, ok):
+    """Verify callback for OpenSSL certificate checking.
+    This implementation simply checks whether the ok parameter is false
+    and logs an error in that case. The return value is always the value
+    of the ok parameter so that the connection attempt is aborted when
+    openssl found a problem with the certificate.
+    """
+    if not ok:
+        log.error("Certificate verification failed: error code %d:"
+                  " Subject %s, Serial Number %s, Issuer %s",
+                  errnum, format_name(cert.get_subject()),
+                  cert.get_serial_number(),
+                  format_name(cert.get_issuer()))
+    return ok
+
+
+class SSLHostMismatchException(Exception):
+
+    """Exception raised when the remote hostname doesn't match a certificate.
+    """
+
+
+def verify_peer_hostname(certificate, hostname):
+    """Verify that the certificate is actually a certificate for hostname.
+
+    This function currently only checks whether any of the commonName
+    (CN) parts of the certificate's subject are equal to the
+    hostname. It doesn't check any extend attributes and it doesn't
+    support wildcards in domain names in the CN.
+
+    If the certificate does not match the hostname, an
+    SSLHostMismatchException is raised.
+    """
+    name_components = certificate.get_subject().get_components()
+    for key, value in name_components:
+        if key == "CN" and value == hostname:
+            return
+    raise SSLHostMismatchException("No common name in %r matches %r"
+                                   % (name_components, hostname))
+
+
+def get_ssl_context(cacert_file, client_cert_file, client_key_file):
+    """Create and return an SSL context.
+    The context can be used with connect_ssl.
+    """
+    ctx = SSL.Context(SSL.SSLv23_METHOD)
+    ctx.set_verify(SSL.VERIFY_PEER, verify_cb)
+    if client_cert_file:
+        ctx.use_certificate_file(client_cert_file)
+    if client_key_file:
+        ctx.use_privatekey_file(client_key_file)
+    if cacert_file:
+        ctx.load_verify_locations(cacert_file)
+    return ctx
+
+
+def connect_ssl(ssl_context, sock, hostname):
+    """Start an SSL connection on the socket sock.
+    The socket has to be connected already. After a successful SSL
+    handshake verify_peer_hostname is used to make sure that the
+    certificate matches the hostname given in the hostname argument.
+    """
+    ssl = SSL.Connection(ssl_context, sock)
+    ssl.set_connect_state()
+    ssl.do_handshake()
+    verify_peer_hostname(ssl.get_peer_certificate(), hostname)
+    return OpenSSLSocket(sock, ssl)
+
+
+def open_http_connection(host, port, ssl_context):
+    """Open an HTTP connection.
+    The return value is a SocketHTTPConnection instance.
+    """
+    sock = connect_tcp(host, port)
+    if ssl_context is not None:
+        sock = connect_ssl(ssl_context, sock, host)
+    return SocketHTTPConnection(sock, host, port)
+
+
+class MetaClient(object):
+
+    """AiR Meta Client"""
+
+    def __init__(self, host, port, ssl_context, dbname, user, password,
+                 client_user):
+        """Initialize the meta client.
+
+        The parameters host and port should be the hotname and port for
+        the meta server. The ssl_context parameter should be the SSL
+        context to use for the connection, usually instantiated by the
+        get_ssl_context function. If the ssl_context parameter is None,
+        no SSL layer will be used.
+
+        The parameters dbname, user and password give the database name
+        on the meta server and the credentials for the access to the
+        meta server.
+
+        The client_user parameter should be the username used in the
+        client mpuls application. It is transmitted to the meta server
+        for logging purposes.
+        """
+        self.host = host
+        self.port = port
+        self.ssl_context = ssl_context
+        self.dbname = dbname
+        self.user = user
+        self.password = password
+        self.client_user = client_user
+
+    def basic_auth_header(self):
+        return "Basic " + base64.b64encode(self.user + ":" + self.password)
+
+    def build_url_path(self, path):
+        return "/" + self.dbname + path
+
+    def perform_request(self, method, path, body=None, content_type=None):
+        try:
+            connection = open_http_connection(self.host, self.port,
+                                              self.ssl_context)
+        except:
+            log.exception("Error while trying to connect to meta server at"
+                          " %s:%s. Raising MetaConnectionError.",
+                          self.host, self.port)
+            raise MetaConnectionError(_("The connection to the meta server"
+                                        " cannot be established"))
+        headers = {"Authorization": self.basic_auth_header(),
+                   "X-mpuls-client-user": self.client_user}
+        if content_type:
+            headers["Content-type"] = content_type
+        connection.request(method, self.build_url_path(path), body,
+                           headers)
+        response = connection.getresponse()
+        if 200 <= response.status < 300:
+            return response
+
+        if response.status == 401:
+            raise MetaUnauthorized(_("The meta-server did not allow the request"
+                                     " because of missing or incorrect"
+                                     " credentials"))
+        elif response.status == 404:
+            raise UnknownMetaCase(_("The meta case does not seem to exist on"
+                                    " the meta server"))
+        elif response.status == 403:
+            raise UnknownProjectPart(_("The project specific part of the"
+                                       " meta case does not seem to exist on"
+                                       " the meta server"))
+        elif response.status == 409:
+            raise MetaCasePending(_("The meta case has been marked for deletion"
+                                    " or anonymization"))
+        elif response.status == 410:
+            raise MetaCaseAnonymized(_("The meta case has already been"
+                                       " anonymized"))
+
+        raise IOError("Error response from Meta server: %d %s\n%s",
+                      response.status, response.reason, response.read())
+
+    def perform_get_request(self, path):
+        return self.perform_request("GET", path).read()
+
+
+    def search_cases_by_hash(self, hash):
+        """Search for cases by the meta hash value.
+        The return value is a list containing the uuids of the meta
+        datasets match the hash value. If no dataset matches the hash,
+        the list is empty.
+        """
+        json = self.perform_get_request("/meta/cases/hash/%s" % hash)
+        parsed = simplejson.loads(json)
+        return parsed["cases"]
+
+    def meta_case_as_html(self, uuid):
+        """Return the meta dataset for the given uuid.
+        The return value is a string containing the dataset rendered as
+        HTML.
+        """
+        return self.perform_get_request("/meta/cases/%s" % uuid)
+
+    def upload_new_case(self, hash, xml):
+        response = self.perform_request("POST", "/meta/cases/hash/%s" % hash,
+                                        body=xml,
+                                        content_type="application/xml")
+        return self.parse_location_header(response)
+
+    def upload_project_data(self, master_uuid, project_uuid, xml):
+        if project_uuid:
+            method = "PUT"
+            path = "/meta/cases/%s/project/%s" % (master_uuid, project_uuid)
+            expect_location = False
+        else:
+            method = "POST"
+            path = "/meta/cases/%s/project/" % (master_uuid,)
+            expect_location = True
+
+        response = self.perform_request(method, path, body=xml,
+                                        content_type="application/xml")
+
+        if expect_location:
+            return self.parse_location_header(response)
+
+        return master_uuid, project_uuid
+
+
+    def parse_location_header(self, response):
+        location = response.getheader("Location", "")
+        match = re.search("/meta/cases/(?P<uuid>[^/]+)/"
+                          "project/(?P<project_uuid>[^/]+)",
+                          location)
+        if match:
+            return (match.group("uuid"), match.group("project_uuid"))
+
+        raise ValueError("Could not parse location header value %r"
+                         % (location,))
+
+    def delete_project_data(self, master_uuid, project_uuid):
+        self.perform_request("DELETE",
+                             "/meta/cases/%s/project/%s" % (master_uuid,
+                                                            project_uuid))
+
+    def delete_case(self, master_uuid, project_uuid):
+        if project_uuid:
+            query = "?project_uuid=%s" % (project_uuid,)
+        else:
+            query = ""
+        self.perform_request("DELETE",
+                             "/meta/cases/%s%s" % (master_uuid, query))


Property changes on: base/trunk/mpulsweb/lib/metaclient.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native



More information about the Mpuls-commits mailing list