Python 2 SSL xmlrpc server with session handling -
i searched quite long time find implementation mentioned in title, not find anything. habe implemented myself. post code here other people might find usefull.
if finds mistakes willing fix them. here code:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os hashlib import sha256 import hmac import uuid import time datetime import datetime import ssl import socket import socketserver import basehttpserver import simplehttpserver import simplexmlrpcserver xmlrpclib import fault # configure below listen_host = '127.0.0.1' # should not use '' here, unless have real fqdn. listen_port = 2048 keyfile = os.path.join('certs', 'server.key') # replace pem formatted key file certfile = os.path.join('certs', 'server.crt') # replace pem formatted certificate file # 2011/01/01 in utc epoch = 1293840000 def require_login(decorated_function): """ decorator prevents access action if not logged in. if login check failed xmlrpclib.fault exception raised """ def wrapper(self, session_id, *args, **kwargs): """ decorated methods must have self , session_id """ # check if valid session available if not self.sessions.has_key(session_id): self._clear_expired_sessions() # clean session dict raise fault("session id invalid", "call login(user, pass) aquire valid session") last_visit = self.sessions[session_id]["last_visit"] # check if timestamp valid if is_timestamp_expired(last_visit): self._clear_expired_sessions() # clean session dict raise fault("session id expired", "call login(user, pass) aquire valid session") self.sessions[session_id]["last_visit"] = get_timestamp() return decorated_function(self, session_id, *args, **kwargs) return wrapper def timestamp_to_datetime(timestamp): """ convert timestamp 'get_timestamp' datetime object args: ts: integer timestamp returns: datetime object """ return datetime.utcfromtimestamp(timestamp + epoch) def get_timestamp(): """ returns seconds since 1/1/2011. returns: integer timestamp """ return int(time.time() - epoch) def is_timestamp_expired(timestamp, max_age = 2700): # maxage in seconds (here: 2700 = 45 min) """ checks if given timestamp expired args: timestamp: integer timestamp max_age : maximal allowd age of timestamp in seconds returns: true if timestamp expired or false if timestamp valid """ age = get_timestamp() - timestamp if age > max_age: return true return false class securexmlrpcserver(basehttpserver.httpserver,simplexmlrpcserver.simplexmlrpcdispatcher): def __init__(self, server_address, handlerclass, logrequests=true, allow_none=false): """ secure xml-rpc server. it similar simplexmlrpcserver uses https transporting xml data. """ self.logrequests = logrequests self.allow_none = true simplexmlrpcserver.simplexmlrpcdispatcher.__init__(self, self.allow_none, none) socketserver.baseserver.__init__(self, server_address, handlerclass) self.socket = ssl.wrap_socket(socket.socket(), server_side=true, certfile=certfile, keyfile=keyfile, ssl_version=ssl.protocol_sslv23) self.server_bind() self.server_activate() class securexmlrpcrequesthandler(simplexmlrpcserver.simplexmlrpcrequesthandler): """ secure xml-rpc request handler class. it similar simplexmlrpcrequesthandler uses https transporting xml data. """ def setup(self): self.connection = self.request self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) def do_post(self): """handles https post request. copied out simplexmlrpcserver.py , modified shutdown socket cleanly. """ try: # arguments data = self.rfile.read(int(self.headers["content-length"])) # in previous versions of simplexmlrpcserver, _dispatch # overridden in class, instead of in # simplexmlrpcdispatcher. maintain backwards compatibility, # check see if subclass implements _dispatch , dispatch # using method if present. response = self.server._marshaled_dispatch( data, getattr(self, '_dispatch', none) ) except exception e: # should happen if module buggy # internal error, report http server error self.send_response(500) self.end_headers() else: # got valid xml rpc response self.send_response(200) self.send_header("content-type", "text/xml") self.send_header("content-length", str(len(response))) self.end_headers() self.wfile.write(response) # shut down connection self.wfile.flush() #modified of http://docs.python.org/library/ssl.html self.connection.shutdown(socket.shut_rdwr) self.connection.close() class xmlrpchandler: """ example implementation login handling """ def __init__(self): self.users = {"test": "test", "foo": "bar"} # replace own authentication self.sessions = dict() self.session_key = os.urandom(32) def _find_session_by_username(self, username): """ try find valid session username. args: username: username search returns: if session found returned otherwise none returned """ session in self.sessions.itervalues(): if session["username"] == username: return session def _invalidate_session_id(self, session_id): """ remove session. args: session_id: session should removed """ try: del self.sessions[session_id] except keyerror: pass def _clear_expired_sessions(self): """ clear expired sessions """ session_id in self.sessions.keys(): last_visit = self.sessions[session_id]["last_visit"] if is_timestamp_expired(last_visit): self._invalidate_session_id(session_id) def _generate_session_id(self, username): """ generates new session id returns: new unique session_id """ return hmac.new(self.session_key, username + str(uuid.uuid4()), sha256).hexdigest() def login(self, username, password): """ handle login procedure. if login successfull session id returned otherwise xmlrpclib.fault exception raised. args: username: username password: password returns: valid session id raises: xmlrpclib.fault exception raised """ # check username , password if self.users.has_key(username): if self.users[username] == password: # check if session username exists session = self._find_session_by_username(username) if session: if is_timestamp_expired(session["last_visit"]): self._invalidate_session_id(session["session_id"]) else: return session["session_id"] # generate session id , save session_id = self._generate_session_id(username) self.sessions[session_id] = {"username" : username, "session_id": session_id, "last_visit": get_timestamp()} return session_id raise fault("unknown username or password", "please check username , password") @require_login def hello(self, session_id, name): """ example method requires login """ if not name: raise fault("unknown recipient", "i need greet!") return "hello, %s!" % name def test(): server_address = (listen_host, listen_port) server = securexmlrpcserver(server_address, securexmlrpcrequesthandler) server.register_introspection_functions() server.register_instance(xmlrpchandler()) sa = server.socket.getsockname() print "serving https on", sa[0], "port", sa[1] server.serve_forever() if __name__ == "__main__": test() """ testcode example client """ import time def continue_xmlrpc_call(func, *args): try: ret = func(*args) print ret return ret except xmlrpclib.fault e: print e server = xmlrpclib.serverproxy("https://localhost:2048") print server print server.system.listmethods() sid = continue_xmlrpc_call(server.login, "foo", "bar") sid = continue_xmlrpc_call(server.login, "foo", "bar") continue_xmlrpc_call(server.hello, sid, "world") time.sleep(2) continue_xmlrpc_call(server.hello, sid, "invalid") continue_xmlrpc_call(server.hello, "193", "")
@philipp nice setup. find loginfunction bit weird. seems knows username logged in can session id, , stuff. looks better if move session code existing session id returned if correct username , password entered.
# check username , password if self.users.has_key(username): if self.users[username] == password: # check if session username exists session = self._find_session_by_username(username) if session: if is_timestamp_expired(session["last_visit"]): self._invalidate_session_id(session["session_id"]) else: return session["session_id"] # generate session id , save session_id = self._generate_session_id(username) self.sessions[session_id] = {"username" : username, "session_id": session_id, "last_visit": get_timestamp()} return session_id
Comments
Post a Comment