Merge in latest rpcproxy changes / refactoring from sogo branch
authorJulien Kerihuel <j.kerihuel@openchange.org>
Fri, 1 Jun 2012 11:59:45 +0000 (11:59 +0000)
committerJulien Kerihuel <j.kerihuel@openchange.org>
Fri, 1 Jun 2012 11:59:45 +0000 (11:59 +0000)
15 files changed:
mapiproxy/services/ocsmanager/ocsmanager/config/NTLMAuthHandler.py [deleted file]
mapiproxy/services/ocsmanager/ocsmanager/config/middleware.py
mapiproxy/services/ocsmanager/ocsmanager/controllers/autodiscover.py
mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/RPCProxyApplication.py [deleted file]
mapiproxy/services/web/rpcproxy/RPCProxyApplication.py [new file with mode: 0644]
mapiproxy/services/web/rpcproxy/channels.py [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/channels.py with 69% similarity]
mapiproxy/services/web/rpcproxy/rpcproxy.conf [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy.conf with 100% similarity]
mapiproxy/services/web/rpcproxy/rpcproxy.wsgi [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy.wsgi with 64% similarity]
mapiproxy/services/web/rpcproxy/utils.py [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/utils.py with 100% similarity]
python/openchange/utils/__init__.py [new file with mode: 0644]
python/openchange/utils/fdunix.py [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/fdunix.py with 100% similarity]
python/openchange/utils/packets.py [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/packets.py with 99% similarity]
python/openchange/web/__init__.py [new file with mode: 0644]
python/openchange/web/auth/NTLMAuthHandler.py [moved from mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/NTLMAuthHandler.py with 88% similarity]
python/openchange/web/auth/__init__.py [new file with mode: 0644]

diff --git a/mapiproxy/services/ocsmanager/ocsmanager/config/NTLMAuthHandler.py b/mapiproxy/services/ocsmanager/ocsmanager/config/NTLMAuthHandler.py
deleted file mode 100644 (file)
index 388f73d..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-import httplib
-import socket
-import uuid
-
-from samba.gensec import Security
-from samba.auth import AuthContext
-
-
-__all__ = ['NTLMAuthHandler']
-
-COOKIE_NAME="ocs-ntlm-auth"
-
-
-class NTLMAuthHandler(object):
-    """
-    HTTP/1.0 ``NTLM`` authentication middleware
-
-    Parameters: application -- the application object that is called only upon
-    successful authentication.
-
-    """
-
-    def __init__(self, application):
-        # TODO: client expiration and/or cleanup
-        self.client_status = {}
-        self.application = application
-
-    def _in_progress_response(self, start_response, ntlm_data=None, client_id=None):
-        status = "401 %s" % httplib.responses[401]
-        content = "More data needed..."
-        headers = [("Content-Type", "text/plain"),
-                   ("Content-Length", "%d" % len(content))]
-        if ntlm_data is None:
-            www_auth_value = "NTLM"
-        else:
-            enc_ntlm_data = ntlm_data.encode("base64")
-            www_auth_value = ("NTLM %s"
-                              % enc_ntlm_data.strip().replace("\n", ""))
-        if client_id is not None:
-            # MUST occur when ntlm_data is None, can still occur otherwise
-            headers.append(("Set-Cookie", "%s=%s" % (COOKIE_NAME, client_id)))
-
-        headers.append(("WWW-Authenticate", www_auth_value))
-        start_response(status, headers)
-
-        return [content]
-
-    def _failure_response(self, start_response, explanation=None):
-        status = "403 %s" % httplib.responses[403]
-        content = "Authentication failure"
-        if explanation is not None:
-            content = content + " (%s)" % explanation
-        headers = [("Content-Type", "text/plain"),
-                   ("Content-Length", "%d" % len(content))]
-        start_response(status, headers)
-
-        return [content]
-
-    def _get_cookies(self, env):
-        cookies = {}
-        if "HTTP_COOKIE" in env:
-            cookie_str = env["HTTP_COOKIE"]
-            for pair in cookie_str.split(";"):
-                (key, value) = pair.strip().split("=")
-                cookies[key] = value
-
-        return cookies
-
-    def __call__(self, env, start_response):
-        cookies = self._get_cookies(env)
-        if COOKIE_NAME in cookies:
-            client_id = cookies[COOKIE_NAME]
-        else:
-            client_id = None
-
-        # old model that only works with mod_wsgi:
-        # if "REMOTE_ADDR" in env and "REMOTE_PORT" in env:
-        #     client_id = "%(REMOTE_ADDR)s:%(REMOTE_PORT)s".format(env)
-
-        if client_id is None or client_id not in self.client_status:
-            # first stage
-            server = Security.start_server(auth_context=AuthContext())
-            server.start_mech_by_name("ntlmssp")
-            client_id = str(uuid.uuid4())
-
-            if "HTTP_AUTHORIZATION" in env:
-                # Outlook may directly have sent a NTLM payload
-                auth = env["HTTP_AUTHORIZATION"]
-                auth_msg = server.update(auth[5:].decode('base64'))
-                response = self._in_progress_response(start_response,
-                                                      auth_msg[1],
-                                                      client_id)
-                self.client_status[client_id] = {"stage": "stage1",
-                                                 "server": server}
-            else:
-                self.client_status[client_id] = {"stage": "stage0",
-                                                 "server": server}
-                response = self._in_progress_response(start_response, None, client_id)
-        else:
-            status_stage = self.client_status[client_id]["stage"]
-
-            if status_stage == "ok":
-                # client authenticated previously
-                response = self.application(env, start_response)
-            elif status_stage == "stage0":
-                # test whether client supports "NTLM"
-                if "HTTP_AUTHORIZATION" in env:
-                    auth = env["HTTP_AUTHORIZATION"]
-                    server = self.client_status[client_id]["server"]
-                    auth_msg = server.update(auth[5:].decode('base64'))
-                    response = self._in_progress_response(start_response,
-                                                          auth_msg[1])
-                    self.client_status[client_id]["stage"] = "stage1"
-                else:
-                    del(self.client_status[client_id])
-                    response = self._failure_response(start_response,
-                                                      "failure at '%s'"
-                                                      % status_stage)
-            elif status_stage == "stage1":
-                if "HTTP_AUTHORIZATION" in env:
-                    auth = env["HTTP_AUTHORIZATION"]
-                    server = self.client_status[client_id]["server"]
-                    try:
-                        auth_msg = server.update(auth[5:].decode('base64'))
-                    except RuntimeError: # a bit violent...
-                        auth_msg = (0,)
-
-                    if auth_msg[0] == 1:
-                        # authentication completed
-                        self.client_status[client_id]["stage"] = "ok"
-                        del(self.client_status[client_id]["server"])
-                        response = self.application(env, start_response)
-                    else:
-                        # we start over with the whole process
-
-                        server = Security.start_server(auth_context=AuthContext())
-                        server.start_mech_by_name("ntlmssp")
-                        self.client_status[client_id] = {"stage": "stage0",
-                                                         "server": server}
-                        response = self._in_progress_response(start_response)
-                else:
-                    del(self.client_status[client_id])
-                    response = self._failure_response(start_response,
-                                                      "failure at '%s'"
-                                                      % status_stage)
-            else:
-                raise RuntimeError("none shall pass!")
-
-        return response
-
-
-middleware = NTLMAuthHandler
index 3210695..0afe86b 100644 (file)
@@ -9,13 +9,13 @@ from pylons.middleware import ErrorHandler, StatusCodeRedirect
 from pylons.wsgiapp import PylonsApp
 from routes.middleware import RoutesMiddleware
 
+from openchange.web.auth.NTLMAuthHandler import NTLMAuthHandler
+
 from ocsmanager.config.environment import load_environment
 
 # from paste.auth.basic import AuthBasicHandler
 # from ocsmanager.model.OCSAuthenticator import *
 
-from NTLMAuthHandler import NTLMAuthHandler
-
 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
     """Create a Pylons WSGI application and return it
 
@@ -64,7 +64,8 @@ def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
 
     # authenticator = OCSAuthenticator(config)
     # app = AuthBasicHandler(app, "OCSManager", authenticator)
-    app = NTLMAuthHandler(app)
+    fqdn = "%(hostname)s.%(dnsdomain)s" % config["samba"]
+    app = NTLMAuthHandler(app, samba_host=fqdn)
 
     # Establish the Registry for this application
     app = RegistryManager(app)
index 0c15d22..ea957ed 100644 (file)
@@ -188,8 +188,8 @@ class AutodiscoverHandler(object):
 
         response_tree = {"Type": "EXPR",
                          "Server": samba_server_name,
-                         "SSL": "On",
-                         "AuthPackage": "Basic"}
+                         "SSL": "Off",
+                         "AuthPackage": "Ntlm"}
         self._append_elements(prot_element, response_tree)
 
         """
diff --git a/mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/RPCProxyApplication.py b/mapiproxy/services/ocsmanager/rpcproxy/rpcproxy/RPCProxyApplication.py
deleted file mode 100644 (file)
index a73b7df..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-# RPCProxyApplication.py -- OpenChange RPC-over-HTTP implementation
-#
-# Copyright (C) 2012  Wolfgang Sourdeau <wsourdeau@inverse.ca>
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 3 of the License, or
-# (at your option) any later version.
-#   
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#   
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-import logging
-import sys
-
-from channels import RPCProxyInboundChannelHandler,\
-    RPCProxyOutboundChannelHandler
-
-
-class RPCProxyApplication(object):
-    def __init__(self):
-        print >>sys.stderr, "RPCProxy started"
-
-    def __call__(self, environ, start_response):
-        if "wsgi.errors" in environ:
-            log_stream = environ["wsgi.errors"]
-        else:
-            log_stream = sys.stderr
-
-        logHandler = logging.StreamHandler(log_stream)
-        fmter = logging.Formatter("[%(process)d] [%(levelname)s] %(message)s")
-        logHandler.setFormatter(fmter)
-
-        logger = logging.Logger("rpcproxy")
-        logger.setLevel(logging.INFO)
-        logger.addHandler(logHandler)
-        self.logger = logger
-
-        if "REQUEST_METHOD" in environ:
-            method = environ["REQUEST_METHOD"]
-            method_method = "_do_" + method
-            if hasattr(self, method_method):
-                method_method_method = getattr(self, method_method)
-                response = method_method_method(environ, start_response)
-            else:
-                response = self._unsupported_method(environ, start_response)
-        else:
-            response = self._unsupported_method(environ, start_response)
-
-        return response
-
-    @staticmethod
-    def _unsupported_method(environ, start_response):
-        msg = "Unsupported method"
-        start_response("501 Not Implemented", [("Content-Type", "text/plain"),
-                                               ("Content-length",
-                                                str(len(msg)))])
-
-        return [msg]
-
-    def _do_RPC_IN_DATA(self, environ, start_response):
-        handler = RPCProxyInboundChannelHandler(self.logger)
-        return handler.sequence(environ, start_response)
-
-    def _do_RPC_OUT_DATA(self, environ, start_response):
-        handler = RPCProxyOutboundChannelHandler(self.logger)
-        return handler.sequence(environ, start_response)
diff --git a/mapiproxy/services/web/rpcproxy/RPCProxyApplication.py b/mapiproxy/services/web/rpcproxy/RPCProxyApplication.py
new file mode 100644 (file)
index 0000000..26eca3e
--- /dev/null
@@ -0,0 +1,103 @@
+# RPCProxyApplication.py -- OpenChange RPC-over-HTTP implementation
+#
+# Copyright (C) 2012  Wolfgang Sourdeau <wsourdeau@inverse.ca>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#   
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#   
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import logging
+from errno import EEXIST
+from os import umask, mkdir, rmdir, listdir
+from os.path import join
+from uuid import uuid4
+import sys
+
+
+from channels import RPCProxyInboundChannelHandler,\
+    RPCProxyOutboundChannelHandler
+
+
+class RPCProxyApplication(object):
+    def __init__(self, samba_host, log_level=logging.DEBUG):
+        print >>sys.stderr, "RPCProxy started"
+
+        has_socket_dir = False
+        umask(0077)
+        while not has_socket_dir:
+            leafname = "rpcproxy-%s" % str(uuid4())
+            dirname = "/tmp/%s" % leafname
+            try:
+                mkdir(dirname)
+                has_socket_dir = True
+                self.sockets_dir = dirname
+            except OSError, e:
+                if e.errno != EEXIST:
+                    raise
+
+        self.samba_host = samba_host
+        self.log_level = log_level
+
+    def __del__(self):
+        for filename in listdir(self.sockets_dir):
+            print >>sys.stderr, \
+                "RPCProxyApplication: removing stale socket '%s'" % filename
+            unlink(join(self.sockets_dir, filename))
+        rmdir(self.sockets_dir)
+
+    def __call__(self, environ, start_response):
+        if "REQUEST_METHOD" in environ:
+            method = environ["REQUEST_METHOD"]
+            method_method = "_do_" + method
+            if hasattr(self, method_method):
+                if "wsgi.errors" in environ:
+                    log_stream = environ["wsgi.errors"]
+                else:
+                    log_stream = sys.stderr
+
+                logHandler = logging.StreamHandler(log_stream)
+                fmter = logging.Formatter("[%(process)d:%(name)s] %(levelname)s: %(message)s")
+                logHandler.setFormatter(fmter)
+
+                logger = logging.Logger(method)
+                logger.setLevel(self.log_level)
+                logger.addHandler(logHandler)
+                # logger.set_name(method)
+
+                method_method_method = getattr(self, method_method)
+                response = method_method_method(logger, environ, start_response)
+            else:
+                response = self._unsupported_method(environ, start_response)
+        else:
+            response = self._unsupported_method(environ, start_response)
+
+        return response
+
+    @staticmethod
+    def _unsupported_method(environ, start_response):
+        msg = "Unsupported method"
+        start_response("501 Not Implemented", [("Content-Type", "text/plain"),
+                                               ("Content-length",
+                                                str(len(msg)))])
+
+        return [msg]
+
+    def _do_RPC_IN_DATA(self, logger, environ, start_response):
+        handler = RPCProxyInboundChannelHandler(self.sockets_dir, logger)
+        return handler.sequence(environ, start_response)
+
+    def _do_RPC_OUT_DATA(self, logger, environ, start_response):
+        handler = RPCProxyOutboundChannelHandler(self.sockets_dir,
+                                                 self.samba_host,
+                                                 logger)
+        return handler.sequence(environ, start_response)
@@ -27,11 +27,11 @@ from time import time, sleep
 from uuid import UUID
 
 # from rpcproxy.RPCH import RPCH, RTS_FLAG_ECHO
-from fdunix import send_socket, receive_socket
-from packets import RTS_CMD_CONNECTION_TIMEOUT, RTS_CMD_VERSION, \
-    RTS_CMD_RECEIVE_WINDOW_SIZE, RTS_CMD_CONNECTION_TIMEOUT, \
-    RTS_FLAG_ECHO, \
-    RPCPacket, RPCRTSPacket, RPCRTSOutPacket
+from openchange.utils.fdunix import send_socket, receive_socket
+from openchange.utils.packets import RTS_CMD_CONNECTION_TIMEOUT, \
+    RTS_CMD_VERSION, RTS_CMD_RECEIVE_WINDOW_SIZE, \
+    RTS_CMD_CONNECTION_TIMEOUT, RTS_FLAG_ECHO, RTS_FLAG_OTHER_CMD, \
+    RTS_CMD_DATA_LABELS, RPCPacket, RPCRTSPacket, RPCRTSOutPacket
 
 
 """Documentation:
@@ -78,13 +78,13 @@ from packets import RTS_CMD_CONNECTION_TIMEOUT, RTS_CMD_VERSION, \
 # those id must have the same length
 INBOUND_PROXY_ID = "IP"
 OUTBOUND_PROXY_ID = "OP"
-SOCKETS_DIR = "/tmp/rpcproxy"
-OC_HOST = "127.0.0.1"
 
 class RPCProxyChannelHandler(object):
-    def __init__(self, logger):
+    def __init__(self, sockets_dir, logger):
+        self.sockets_dir = sockets_dir
         self.logger = logger
 
+        self.unix_socket = None
         self.client_socket = None # placeholder for wsgi.input
 
         self.bytes_read = 0
@@ -95,7 +95,7 @@ class RPCProxyChannelHandler(object):
         self.connection_cookie = None
 
     def handle_echo_request(self, environ, start_response):
-        self.logger.info("handling echo request")
+        self.logger.debug("handling echo request")
 
         packet = RPCRTSOutPacket()
         packet.flags = RTS_FLAG_ECHO
@@ -108,14 +108,15 @@ class RPCProxyChannelHandler(object):
         return [data]
 
     def log_connection_stats(self):
-        self.logger.info("request took %f secs; %d bytes received; %d bytes sent"
+        self.logger.debug("channel keep alive during %f secs;"
+                         " %d bytes received; %d bytes sent"
                          % ((time() - self.startup_time),
                             self.bytes_read, self.bytes_written))
 
 
 class RPCProxyInboundChannelHandler(RPCProxyChannelHandler):
-    def __init__(self, logger):
-        RPCProxyChannelHandler.__init__(self, logger)
+    def __init__(self, sockets_dir, logger):
+        RPCProxyChannelHandler.__init__(self, sockets_dir, logger)
         self.oc_conn = None
         self.window_size = 0
         self.conn_timeout = 0
@@ -125,11 +126,13 @@ class RPCProxyInboundChannelHandler(RPCProxyChannelHandler):
     def _receive_conn_b1(self):
         # CONN/B1 RTS PDU (TODO: validation)
         # receive the cookie
-        self.logger.info("IN: receiving CONN/B1")
+        self.logger.debug("receiving CONN/B1")
 
         packet = RPCPacket.from_file(self.client_socket, self.logger)
         if not isinstance(packet, RPCRTSPacket):
             raise Exception("Unexpected non-rts packet received for CONN/B1")
+        self.logger.debug("packet headers = " + packet.pretty_dump())
+
         self.connection_cookie = str(UUID(bytes=packet.commands[1]["Cookie"]))
         self.channel_cookie = str(UUID(bytes=packet.commands[2]["Cookie"]))
         self.client_keepalive = packet.commands[4]["ClientKeepalive"]
@@ -142,42 +145,42 @@ class RPCProxyInboundChannelHandler(RPCProxyChannelHandler):
         # channel
 
         # connect as a client to the cookie unix socket
-        socket_name = os.path.join(SOCKETS_DIR, self.connection_cookie)
-        self.logger.info("IN: connecting to OUT via unix socket '%s'"
+        socket_name = os.path.join(self.sockets_dir, self.connection_cookie)
+        self.logger.debug("connecting to OUT via unix socket '%s'"
                          % socket_name)
-        sock = socket(AF_UNIX, SOCK_STREAM)
+        unix_socket = socket(AF_UNIX, SOCK_STREAM)
         connected = False
         attempt = 0
         while not connected:
             try:
                 attempt = attempt + 1
-                sock.connect(socket_name)
+                unix_socket.connect(socket_name)
+                self.unix_socket = unix_socket
                 connected = True
             except socket_error:
-                self.logger.info("IN: handling socket.error: %s"
+                self.logger.debug("handling socket.error: %s"
                                  % str(sys.exc_info()))
                 if attempt < 10:
-                    self.logger.warn("IN: reattempting to connect to OUT"
+                    self.logger.warn("CUICUI reattempting to connect to OUT"
                                      " channel... (%d/10)" % attempt)
                     sleep(1)
 
         if connected:
-            self.logger.info("IN: connection succeeded")
-            self.logger.info("IN: sending window size and connection timeout")
+            self.logger.debug("connection succeeded")
+            self.logger.debug("sending window size and connection timeout")
 
             # identify ourselves as the IN proxy
-            sock.sendall(INBOUND_PROXY_ID)
+            unix_socket.sendall(INBOUND_PROXY_ID)
 
             # send window_size to 256Kib (max size allowed)
             # and conn_timeout (in seconds, max size allowed)
-            sock.sendall(pack("<ll", (256 * 1024), 14400000))
+            unix_socket.sendall(pack("<ll", (256 * 1024), 14400000))
 
             # recv oc socket
-            self.oc_conn = receive_socket(sock)
+            self.oc_conn = receive_socket(unix_socket)
 
-            self.logger.info("IN: oc_conn received (fileno=%d)"
+            self.logger.debug("oc_conn received (fileno=%d)"
                              % self.oc_conn.fileno())
-            sock.close()
         else:
             self.logger.error("too many failed attempts to establish a"
                               " connection to OUT channel")
@@ -185,85 +188,49 @@ class RPCProxyInboundChannelHandler(RPCProxyChannelHandler):
         return connected
 
     def _runloop(self):
-        self.logger.info("IN: runloop")
+        self.logger.debug("runloop")
 
         status = True
         while status:
             try:
                 oc_packet = RPCPacket.from_file(self.client_socket,
                                                 self.logger)
+                self.logger.debug("packet headers = "
+                                  + oc_packet.pretty_dump())
                 self.bytes_read = self.bytes_read + oc_packet.size
 
-                self.logger.info("IN: packet headers = "
-                                 + oc_packet.pretty_dump())
-
                 if isinstance(oc_packet, RPCRTSPacket):
-                    # or oc_packet.header["ptype"] == DCERPC_PKT_AUTH3):
-                    # we do not forward rts packets
-                    self.logger.info("IN: ignored RTS packet")
+                    labels = [RTS_CMD_DATA_LABELS[command["type"]]
+                              for command in oc_packet.commands]
+                    self.logger.debug("ignored RTS packet with commands: %s"
+                                      % ", ".join(labels))
                 else:
-                    self.logger.info("IN: sending packet to OC")
+                    self.logger.debug("sending packet to OC")
                     self.oc_conn.sendall(oc_packet.data)
                     self.bytes_written = self.bytes_written + oc_packet.size
             except IOError:
                 status = False
+                self.logger.debug("handling socket.error: %s"
+                                  % str(sys.exc_info()))
                 # exc = sys.exc_info()
-                self.logger.error("IN: client connection closed")
-                self._notify_OUT_channel()
-
-    def _notify_OUT_channel(self):
-        self.logger.info("IN: notifying OUT channel of shutdown")
-
-        socket_name = os.path.join(SOCKETS_DIR, self.connection_cookie)
-        self.logger.info("IN: connecting to OUT via unix socket '%s'"
-                         % socket_name)
-        sock = socket(AF_UNIX, SOCK_STREAM)
-        connected = False
-        attempt = 0
-        while not connected:
-            try:
-                attempt = attempt + 1
-                sock.connect(socket_name)
-                connected = True
-            except socket_error:
-                self.logger.info("IN: handling socket.error: %s"
-                                 % str(sys.exc_info()))
-                if attempt < 10:
-                    self.logger.warn("IN: reattempting to connect to OUT"
-                                     " channel... (%d/10)" % attempt)
-                    sleep(1)
-
-        if connected:
-            self.logger.info("IN: connection succeeded")
-            try:
-                sock.sendall(INBOUND_PROXY_ID + "q")
-                sock.close()
-            except:
-                # UNIX socket might already have been closed by OUT channel
-                pass
-        else:
-            self.logger.error("too many failed attempts to establish a"
-                              " connection to OUT channel")
-
-    def _terminate_oc_socket(self):
-        self.oc_conn.close()
+                self.logger.error("client connection closed")
 
     def sequence(self, environ, start_response):
-        self.logger.info("IN: processing request")
+        self.logger.debug("processing request")
         if "REMOTE_PORT" in environ:
-            self.logger.info("IN: remote port = %s" % environ["REMOTE_PORT"])
-        # self.logger.info("IN: path: ' + self.path)
+            self.logger.debug("remote port = %s" % environ["REMOTE_PORT"])
+        # self.logger.debug("path: ' + self.path)
 
         content_length = int(environ["CONTENT_LENGTH"])
-        self.logger.info("IN: request size is %d" % content_length)
+        self.logger.debug("request size is %d" % content_length)
 
         # echo request
         if content_length <= 0x10:
-            self.logger.info("IN: Exiting (1) from do_RPC_IN_DATA")
             for data in self.handle_echo_request(environ, start_response):
                 yield data
+            self.logger.debug("exiting from echo request")
         elif content_length >= 128:
-            self.logger.info("IN: Processing IN channel request")
+            self.logger.debug("processing IN channel request")
 
             self.client_socket = environ["wsgi.input"]
             self._receive_conn_b1()
@@ -275,10 +242,19 @@ class RPCProxyInboundChannelHandler(RPCProxyChannelHandler):
                                 ("Content-length", "0")])
                 self._runloop()
 
-            self._terminate_oc_socket()
+                # shutting down sockets
+                self.logger.debug("notifying OUT channel of shutdown")
+                try:
+                    self.unix_socket.sendall(INBOUND_PROXY_ID + "q")
+                    self.unix_socket.close()
+                except socket_error:
+                    # OUT channel already closed the connection
+                    pass
+
+                self.oc_conn.close()
 
             self.log_connection_stats()
-            self.logger.info("IN: Exiting (2) from do_RPC_IN_DATA")
+            self.logger.debug("exiting from main sequence")
             
             # TODO: error handling
             start_response("200 Success",
@@ -302,9 +278,9 @@ class RPCProxyInboundChannelHandler(RPCProxyChannelHandler):
         # return [msg]
 
 class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
-    def __init__(self, logger):
-        RPCProxyChannelHandler.__init__(self, logger)
-        self.unix_socket = None
+    def __init__(self, sockets_dir, samba_host, logger):
+        RPCProxyChannelHandler.__init__(self, sockets_dir, logger)
+        self.samba_host = samba_host
         self.oc_conn = None
         self.in_window_size = 0
         self.in_conn_timeout = 0
@@ -312,15 +288,17 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
     def _receive_conn_a1(self):
         # receive the cookie
         # TODO: validation of CONN/A1
-        self.logger.info("OUT: receiving CONN/A1")
+        self.logger.debug("receiving CONN/A1")
         packet = RPCPacket.from_file(self.client_socket, self.logger)
         if not isinstance(packet, RPCRTSPacket):
             raise Exception("Unexpected non-rts packet received for CONN/A1")
+        self.logger.debug("packet headers = " + packet.pretty_dump())
+
         self.connection_cookie = str(UUID(bytes=packet.commands[1]["Cookie"]))
         self.channel_cookie = str(UUID(bytes=packet.commands[2]["Cookie"]))
 
     def _send_conn_a3(self):
-        self.logger.info("OUT: sending CONN/A3 to client")
+        self.logger.debug("sending CONN/A3 to client")
             # send the A3 response to the client
         packet = RPCRTSOutPacket(self.logger)
         # we set the min timeout value allowed, as we would actually need
@@ -331,7 +309,7 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
         return packet.make()
 
     def _send_conn_c2(self):
-        self.logger.info("OUT: sending CONN/C2 to client")
+        self.logger.debug("sending CONN/C2 to client")
             # send the C2 response to the client
         packet = RPCRTSOutPacket(self.logger)
         # we set the min timeout value allowed, as we would actually need
@@ -345,25 +323,30 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
 
     def _setup_oc_socket(self):
         # create IP connection to OpenChange
-        self.logger.info("OUT: connecting to OC_HOST:1024")
+        self.logger.debug("connecting to %s:1024" % self.samba_host)
         connected = False
         while not connected:
             try:
                 oc_conn = socket(AF_INET, SOCK_STREAM)
-                oc_conn.connect((OC_HOST, 1024))
+                oc_conn.connect((self.samba_host, 1024))
                 connected = True
             except socket_error:
-                self.logger.info("OUT: failure to connect, retrying...")
+                self.logger.debug("failure to connect, retrying...")
                 sleep(1)
-        self.logger.info("OUT: connection to OC succeeeded (fileno=%d)"
+        self.logger.debug("connection to OC succeeeded (fileno=%d)"
                          % oc_conn.fileno())
         self.oc_conn = oc_conn
 
     def _setup_channel_socket(self):
         # TODO: add code to create missing socket dir
         # create the corresponding unix socket
-        socket_name = os.path.join(SOCKETS_DIR, self.connection_cookie)
-        self.logger.info("OUT: creating unix socket '%s'" % socket_name)
+
+        if not os.access(self.sockets_dir, os.R_OK | os.W_OK | os.X_OK):
+            raise IOError("Socket directory '%s' does not exist or has the"
+                          " wrong permissions" % self.sockets_dir)
+
+        socket_name = os.path.join(self.sockets_dir, self.connection_cookie)
+        self.logger.debug("creating unix socket '%s'" % socket_name)
         if os.access(socket_name, os.F_OK):
             os.remove(socket_name)
         sock = socket(AF_UNIX, SOCK_STREAM)
@@ -372,7 +355,7 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
         self.unix_socket = sock
 
     def _wait_IN_channel(self):
-        self.logger.info("OUT: waiting for connection from IN")
+        self.logger.debug("waiting for connection from IN")
         # wait for the IN channel to connect as a B1 should be occurring
         # on the other side
         in_sock = self.unix_socket.accept()[0]
@@ -381,17 +364,17 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
             raise IOError("connection must be from IN proxy (1): /%s/"
                           % data)
 
-        self.logger.info("OUT: receiving window size + conn_timeout")
+        self.logger.debug("receiving window size + conn_timeout")
             # receive the WindowSize + ConnectionTimeout
         (self.in_window_size, self.in_conn_timeout) = \
             unpack_from("<ll", in_sock.recv(8, MSG_WAITALL))
             # send OC socket
-        self.logger.info("OUT: sending OC socket to IN")
+        self.logger.debug("sending OC socket to IN")
         send_socket(in_sock, self.oc_conn)
         in_sock.close()
 
     def _runloop(self):
-        self.logger.info("OUT: runloop")
+        self.logger.debug("runloop")
 
         unix_fd = self.unix_socket.fileno()
         oc_fd = self.oc_conn.fileno()
@@ -406,29 +389,29 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
             for data in fd_pool.poll(1000):
                 fd, event_no = data
                 if fd == oc_fd:
-                    # self.logger.info("received event '%d' on oc socket"
+                    # self.logger.debug("received event '%d' on oc socket"
                     #                   % event_no)
                     if event_no & POLLHUP > 0:
                         # FIXME: notify IN channel?
-                        self.logger.info("OUT: connection closed from OC")
+                        self.logger.debug("connection closed from OC")
                         status = False
                     elif event_no & POLLIN > 0:
                         oc_packet = RPCPacket.from_file(self.oc_conn,
                                                         self.logger)
-                        self.logger.info("OUT: packet headers = "
+                        self.logger.debug("packet headers = "
                                          + oc_packet.pretty_dump())
                         if isinstance(oc_packet, RPCRTSPacket):
                             raise Exception("Unexpected rts packet received")
 
-                        self.logger.info("OUT: sending data to client")
+                        self.logger.debug("sending data to client")
                         self.bytes_read = self.bytes_read + oc_packet.size
                         self.bytes_written = self.bytes_written + oc_packet.size
                         yield oc_packet.data
                         # else:
-                        # self.logger.info("ignored event '%d' on oc socket"
+                        # self.logger.debug("ignored event '%d' on oc socket"
                         #                  % event_no)
                 elif fd == unix_fd:
-                    self.logger.info("OUT: ignored event '%d' on unix socket"
+                    self.logger.debug("ignored event '%d' on unix socket"
                                      % event_no)
                     # FIXME: we should listen to what the IN channel has to say
                     status = False
@@ -436,11 +419,11 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
                     raise Exception("invalid poll event: %s" % str(data))
             # write(oc_packet.header_data)
             # write(oc_packet.data)
-            # self.logger.info("OUT: data sent to client")
+            # self.logger.debug("data sent to client")
 
     def _terminate_sockets(self):
-        socket_name = os.path.join(SOCKETS_DIR, self.connection_cookie)
-        self.logger.info("OUT: removing and closing unix socket '%s'"
+        socket_name = os.path.join(self.sockets_dir, self.connection_cookie)
+        self.logger.debug("removing and closing unix socket '%s'"
                          % socket_name)
         if os.access(socket_name, os.F_OK):
             os.remove(socket_name)
@@ -448,20 +431,20 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
         self.oc_conn.close()
 
     def sequence(self, environ, start_response):
-        self.logger.info("OUT: processing request")
+        self.logger.debug("processing request")
         if "REMOTE_PORT" in environ:
-            self.logger.info("OUT: remote port = %s" % environ["REMOTE_PORT"])
-        # self.logger.info("OUT: path: ' + self.path)
+            self.logger.debug("remote port = %s" % environ["REMOTE_PORT"])
+        # self.logger.debug("path: ' + self.path)
         content_length = int(environ["CONTENT_LENGTH"])
-        self.logger.info("OUT: request size is %d" % content_length)
+        self.logger.debug("request size is %d" % content_length)
 
         if content_length <= 0x10:
             # echo request
             for data in self.handle_echo_request(environ, start_response):
                 yield data
         elif content_length == 76:
-            self.logger.info("OUT: Processing nonreplacement Out channel"
-                             "request")
+            self.logger.debug("processing nonreplacement Out channel"
+                              "request")
 
             self.client_socket = environ["wsgi.input"]
             self._receive_conn_a1()
@@ -477,7 +460,7 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
             self._wait_IN_channel()
 
             yield self._send_conn_c2()
-            self.logger.info("OUT: total bytes sent yet: %d"
+            self.logger.debug("total bytes sent yet: %d"
                              % self.bytes_written)
             for data in self._runloop():
                 yield data
@@ -489,4 +472,4 @@ class RPCProxyOutboundChannelHandler(RPCProxyChannelHandler):
             raise Exception("This content-length is not handled")
 
         self.log_connection_stats()
-        self.logger.info("OUT: Exiting from do_RPC_OUT_DATA")
+        self.logger.debug("exiting from main sequence")
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
+# this is the WSGI starting point for rpcproxy
+
 import sys
 
-sys.path.extend(("/usr/local/samba/lib/python2.7/site-packages",
-                 "/home/wsourdeau/src/openchange/mapiproxy/services/ocsmanager/rpcproxy"))
+print >>sys.stderr, "path: %s" % ":".join(sys.path)
+
+import logging
+
+from openchange.web.auth.NTLMAuthHandler import *
+from RPCProxyApplication import *
+
 
-from rpcproxy.NTLMAuthHandler import *
-from rpcproxy.RPCProxyApplication import *
+SOCKETS_DIR = "/tmp/rpcproxy"
+SAMBA_HOST = "127.0.0.1"
+LOG_LEVEL = logging.DEBUG
 
-application = NTLMAuthHandler(RPCProxyApplication())
-# application = RPCProxyApplication()
+application = NTLMAuthHandler(RPCProxyApplication(samba_host=SAMBA_HOST,
+                                                  log_level=LOG_LEVEL),
+                              samba_host=SAMBA_HOST)
diff --git a/python/openchange/utils/__init__.py b/python/openchange/utils/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
@@ -606,7 +606,7 @@ class RPCRTSOutPacket(object):
         self.command_data = None
 
         if self.logger is not None:
-            self.logger.info("returning packet: %s" % repr(data))
+            self.logger.debug("returning packet: %s" % repr(data))
 
         return data
 
diff --git a/python/openchange/web/__init__.py b/python/openchange/web/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
 #
 
 """This module provides the NTLMAuthHandler class, a WSGI middleware that
-enables authentication via RPC to Samba
+enables NTLM authentication via RPC to Samba.
+
+It works by proxying the NTLMSSP payload between the client and the samba
+server. Accessorily it could be used against an MS Exchange service, but this
+is untested.
 
 """
 
 import httplib
-from uuid import uuid4, UUID
 from socket import socket, _socketobject, SHUT_RDWR, AF_INET, AF_UNIX, \
     SOCK_STREAM, MSG_WAITALL, error as socket_error
 from struct import pack, error as struct_error
+import sys
+from uuid import uuid4, UUID
 
-from packets import *
+from openchange.utils.packets import *
 
 
 COOKIE_NAME = "ocs-ntlm-auth"
-SAMBA_HOST = "localhost"
 SAMBA_PORT = 1024
 
 
@@ -44,10 +48,11 @@ class NTLMAuthHandler(object):
 
     """
 
-    def __init__(self, application):
+    def __init__(self, application, samba_host="localhost"):
         # TODO: client expiration and/or cleanup
         self.client_status = {}
         self.application = application
+        self.samba_host = samba_host
 
     def _in_progress_response(self, start_response,
                               ntlm_data=None, client_id=None):
@@ -81,15 +86,22 @@ class NTLMAuthHandler(object):
 
         return cookies
 
-    def _stage0(self, client_id, env, start_response):
+    def _handle_negotiate(self, client_id, env, start_response):
         # print >>sys.stderr, "* client auth stage0"
 
         auth = env["HTTP_AUTHORIZATION"]
         ntlm_payload = auth[5:].decode("base64")
 
         # print >> sys.stderr, "connecting to host"
-        server = socket(AF_INET, SOCK_STREAM)
-        server.connect((SAMBA_HOST, SAMBA_PORT))
+        try:
+            server = socket(AF_INET, SOCK_STREAM)
+            server.connect((self.samba_host, SAMBA_PORT))
+        except:
+            print >>sys.stderr, \
+                ("NTLMAuthHandler: caught exception when connecting to samba"
+                 " host")
+            raise
+
         # print >> sys.stderr, "host: %s" % str(server.getsockname())
 
         # print >> sys.stderr, "building bind packet"
@@ -122,7 +134,7 @@ class NTLMAuthHandler(object):
 
         return response
 
-    def _stage1(self, client_id, env, start_response):
+    def _handle_auth(self, client_id, env, start_response):
         # print >>sys.stderr, "* client auth stage1"
 
         server = self.client_status[client_id]["server"]
@@ -207,11 +219,12 @@ class NTLMAuthHandler(object):
             if client_id is None or client_id not in self.client_status:
                 # stage 0, where the cookie has not been set yet and where we
                 # know the NTLM payload is a NEGOTIATE message
-                response = self._stage0(client_id, env, start_response)
+                response = self._handle_negotiate(client_id,
+                                                  env, start_response)
             else:
                 # stage 1, where the client has already received the challenge
                 # from the server and is now sending an AUTH message
-                response = self._stage1(client_id, env, start_response)
+                response = self._handle_auth(client_id, env, start_response)
         else:
             if client_id is None or client_id not in self.client_status:
                 # this client has never been seen
diff --git a/python/openchange/web/auth/__init__.py b/python/openchange/web/auth/__init__.py
new file mode 100644 (file)
index 0000000..e69de29