#!/usr/bin/python import os import sys sys.path.extend(("/usr/local/samba/lib/python2.7/site-packages", "/home/wsourdeau/src/openchange/mapiproxy/services/ocsmanager/rpcproxy")) import json import httplib import socket import uuid import time from rpcproxy.RPCH import RPCH, assemble_packet_header, RTS_FLAG_ECHO from samba.gensec import Security from samba.auth import AuthContext # logfile = open("/tmp/rpcproxy-%d.log" % os.getpid(), "w+") # logfile = open("/tmp/rpcproxy.log", "a") COOKIE_NAME="ocs-ntlm-auth" FIFO = "/tmp/fifo" OC_HOST = "127.0.0.1" PIDSTR = "[%d]" % os.getpid() class RPCProxyLogger(object): def __init__(self, channel=sys.stderr): self.channel = channel def log(self, *arg): self.channel.write(PIDSTR) for data in arg: self.channel.write(" ") self.channel.write(data) self.channel.flush() def pretty_dump_dict(self, adict): def _unhandled_objects(obj): return str(obj) s = json.dumps(adict, default=_unhandled_objects, sort_keys=True, indent=4) self.log('\n'.join([l.rstrip() for l in s.splitlines()]), "\n") 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): logger = RPCProxyLogger(env["wsgi.errors"]) logger.pretty_dump_dict(env) 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')) logger.log("auth_msg: %s\n" % str(auth_msg)) response = self._in_progress_response(start_response, auth_msg[1], client_id) self.client_status[client_id] = {"stage": "stage1", "server": server} else: logger.log("no auth yet\n") 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"] logger.log("handling stage '%s'\n" % status_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 class RPCOverHTTPProxyApplication(object): def __call__(self, environ, start_response): if "wsgi.errors" in environ: log_channel = environ["wsgi.errors"] else: log_channel = sys.stderr self.logger = RPCProxyLogger(log_channel) self.logger.pretty_dump_dict(environ) 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 def _unsupported_method(self, environ, start_response): msg = "Unsupported method" start_response("501 Not Implemented", [("Content-Type", "text/plain"), ("Content-length", "%s" % len(msg))]) return [msg] def _do_GET(self, environ, start_response): start_response("200 OK", [("Content-Type", "text/plain"), ("Content-length", "0")]) return [""] def _handle_echo_request(self, environ, start_response): self.logger.log("handling echo request") start_response("200 Success", [("Content-length", "20"), ("Content-Type", "application/rpc")]) blob = assemble_packet_header(RTS_FLAG_ECHO) return [blob] def _do_RPC_IN_DATA(self, environ, start_response): self.logger.log('command: RPC_IN_DATA') # self.logger.log('path: ' + self.path) content_length = int(environ["CONTENT_LENGTH"]) # echo request if content_length <= 0x10: self.logger.log('Exiting (1) from do_RPC_IN_DATA') return self._handle_echo_request(environ, start_response) else: self.logger.log("creating fifo") try: os.mkfifo(FIFO) self.logger.log("created fifo") except OSError, e: self.logger.log('Failed to create FIFO: %s' % e) rpch = RPCH(debug=True, method="RPC_IN_DATA", host=OC_HOST, logger=self.logger) status = True fifo = None input_file = environ["wsgi.input"] while status: self.logger.log('RPC_IN_DATA') status = rpch.pull(input_file) if status is True: rpch.proxify() if fifo is None: fifo = open(FIFO, 'wb') # Push the response to RPC_OUT_DATA listener for data in rpch.push(fifo, channel="rpc_in_data"): if data != "": fifo.write(data) fifo.flush() else: self.logger.log("status False") if fifo is not None: fifo.close() self.logger.log("removing fifo") try: os.remove(FIFO) self.logger.log("removed fifo") except OSError, e: self.logger.log("ignored OSError occuring when removing FIFO") self.logger.log('Exiting (2) from do_RPC_IN_DATA') return self._do_GET(environ, start_response) # OLD CODE # msg = "RPC_IN_DATA method" # content_length = environ["CONTENT_LENGTH"] # # echo request # if content_length <= 10: # pass # start_response("200 OK", [("Content-Type", "text/plain"), # ("Content-length", "%s" % len(msg))]) # return [msg] def _do_RPC_OUT_DATA(self, environ, start_response): self.logger.log('command: RPC_OUT_DATA') # self.logger.log('path: ' + self.path) content_length = int(environ["CONTENT_LENGTH"]) # echo request if content_length <= 0x10: for data in self._handle_echo_request(environ, start_response): yield data else: # Start the response headers = [("Content-Type", "application/rpc"), ("Content-Length", "1073741824")] start_response("200 Success", headers) rpch = RPCH(debug=True, method="RPC_OUT_DATA", host=OC_HOST, logger=self.logger) # Listen for initial RPCH RTS RPC_OUT_DATA command # self.logger.log('RPC_OUT_DATA --- here: %s\n') input_file = environ["wsgi.input"] rpch.pull(input_file) # conn_a3 self.logger.log('RPC_OUT_DATA: sending conn_a3') yield ("\x05\x00\x14\x03\x10\x00\x00\x00\x1C\x00\x00\x00\x00\x00\x00" "\x00\x00\x00\x01\x00\x02\x00\x00\x00\xC0\xD4\x01\x00") self.logger.log('RPC_OUT_DATA: sending conn_c1_c2') # conn_c1_c2 yield ("\x05\x00\x14\x03\x10\x00\x00\x00\x2C\x00\x00\x00\x00\x00\x00" "\x00\x00\x00\x03\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00" "\x00\x00\x00\x00\x01\x00\x02\x00\x00\x00\x00\xD4\x01\x00") # Listen for data from the listener fifo = open(FIFO, 'rb') status = True while status: self.logger.log('RPC_OUT_DATA: before rpch.pull') status = rpch.pull(fifo) self.logger.log('RPC_OUT_DATA: after rpch.pull') if status is True: for data in rpch.push(input_file, channel='rpc_out_data'): yield data fifo.close() self.logger.log('Exiting from do_RPC_OUT_DATA') # return "" # OLD CODE # msg = "RPC_OUT_DATA method" # start_response("200 OK", [("Content-Type", "text/plain"), # ("Content-length", "%s" % len(msg))]) # return [msg] application = NTLMAuthHandler(RPCOverHTTPProxyApplication()) # application = RPCOverHTTPProxyApplication()