[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