Python xmlrpcserver with HTTPS support and basic authentication
For a project I’m working on at Formatics.nl I needed a Python based multithreaded xmlrpcserver with HTTPS support, HTTP basic authentication and capable of running on any port. After looking into solutions which required some sort of server for the HTTPS and basic autentication parts I decided to go for a better solution to try to add the HTTPS support and basic authentication to the Python xmlrpsserver itself. That way we would have a simple and fast solution which could run from any box on any port. Looking around on the net I found various solutions for every requirement but nothing which combined it all into one solution. So after combining various bits and pieces I came up with a Python xmlrpcserver with HTTPS and basic authentication support.
As the code used came from various free-software projects and locations I decided to share the server with the world so that it might benefit others.
For the HTTPS you need OpenSSL certificates, use your existing PEM formatted one or just create one yourself.
$ openssl genrsa -out privkey.pem 2048 $ openssl req -new -key privkey.pem -out cert.csr $ openssl req -new -x509 -key privkey.pem -out cacert.pem -days 1095
And here’s the server
from SocketServer import ThreadingMixIn, BaseServer from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler, SimpleXMLRPCDispatcher import socket from OpenSSL import SSL from base64 import b64decode from threading import Thread, Condition from thread import start_new_thread import traceback import time # static stuff DEFAULTKEYFILE = 'privkey.pem' # Replace with your PEM formatted key file DEFAULTCERTFILE = 'cacert.pem' # Replace with your PEM formatted certificate file class Services: def give_me_time(self): return time.asctime() class CustomThreadingMixIn: """Mix-in class to handle each request in a new thread.""" # Decides how threads will act upon termination of the main process daemon_threads = True def process_request_thread(self, request, client_address): """Same as in BaseServer but as a thread. In addition, exception handling is done here. """ try: self.finish_request(request, client_address) self.close_request(request) except (socket.error, SSL.SysCallError), why: print 'socket.error finishing request from "%s"; Error: %s' % (client_address, str(why)) self.close_request(request) except: self.handle_error(request, client_address) self.close_request(request) def process_request(self, request, client_address): """Start a new thread to process the request.""" t = Thread(target=self.process_request_thread, args=(request, client_address)) if self.daemon_threads: t.setDaemon(1) t.start() class MyXMLRPCServer(CustomThreadingMixIn, SimpleXMLRPCServer): def __init__(self, ip, port, keyFile=DEFAULTKEYFILE, certFile=DEFAULTCERTFILE, logRequests=True): self.logRequests = logRequests class VerifyingRequestHandler(SimpleXMLRPCRequestHandler): def setup(myself): myself.connection = myself.request myself.rfile = socket._fileobject(myself.request, "rb", myself.rbufsize) myself.wfile = socket._fileobject(myself.request, "wb", myself.wbufsize) def address_string(myself): "getting 'FQDN' from host seems to stall on some ip addresses, so... just (quickly!) return raw host address" host, port = myself.client_address #return socket.getfqdn(host) return host def do_POST(myself): """Handles the HTTPS POST request. It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly. """ try: # get arguments data = myself.rfile.read(int(myself.headers["content-length"])) # In previous versions of SimpleXMLRPCServer, _dispatch # could be overridden in this class, instead of in # SimpleXMLRPCDispatcher. To maintain backwards compatibility, # check to see if a subclass implements _dispatch and dispatch # using that method if present. response = myself.server._marshaled_dispatch(data, getattr(myself, '_dispatch', None)) except Exception, info: # This should only happen if the module is buggy print "ERROR do_POST: ", info print "Traceback follows:", traceback.print_exc() # internal error, report as HTTP server error myself.send_response(500) myself.end_headers() else: # got a valid XML RPC response myself.send_response(200) myself.send_header("Content-type", "text/xml") myself.send_header("Content-length", str(len(response))) myself.end_headers() myself.wfile.write(response) # shut down the connection myself.wfile.flush() myself.connection.shutdown() # Modified here! def do_GET(myself): """Handles the HTTP GET request. Interpret all HTTP GET requests as requests for server documentation. """ # Check that the path is legal if not myself.is_rpc_path_valid(): myself.report_404() return response = myself.server.generate_html_documentation() myself.send_response(200) myself.send_header("Content-type", "text/html") myself.send_header("Content-length", str(len(response))) myself.end_headers() myself.wfile.write(response) # shut down the connection myself.wfile.flush() myself.connection.shutdown() # Modified here! def report_404(myself): # Report a 404 error myself.send_response(404) response = 'No such page' myself.send_header("Content-type", "text/plain") myself.send_header("Content-length", str(len(response))) myself.end_headers() myself.wfile.write(response) # shut down the connection myself.wfile.flush() myself.connection.shutdown() # Modified here! def parse_request(myself): if SimpleXMLRPCRequestHandler.parse_request(myself): basic, foo, encoded = myself.headers.get('Authorization').partition(' ') username, foo, password = b64decode(encoded).partition(':') #print username, foo, password if username == 'admin': return True else: myself.send_error(401, 'Authentication failed') return False SimpleXMLRPCDispatcher.__init__(self, False, None) BaseServer.__init__(self, (ip, port), VerifyingRequestHandler) # SSL socket stuff ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_privatekey_file(keyFile) ctx.use_certificate_file(certFile) self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type)) self.server_bind() self.server_activate() self.funcs = {} self.register_introspection_functions() self.register_instance(Services()) # requests count and condition, to allow for keyboard quit via CTL-C self.requests = 0 self.rCondition = Condition() def startup(self): #run until quit signaled from keyboard print 'server starting; hit CTRL-C to quit...' while True: try: self.rCondition.acquire() start_new_thread(self.handle_request, ()) # we do this async, because handle_request blocks! while not self.requests: self.rCondition.wait(timeout=3.0) if self.requests: self.requests -= 1 self.rCondition.release() except KeyboardInterrupt: print "quit signaled, i'm done." return def get_request(self): request, client_address = self.socket.accept() self.rCondition.acquire() self.requests += 1 self.rCondition.notifyAll() self.rCondition.release() return (request, client_address) def listMethods(self): """return list of method names (strings)""" methodNames = self.funcs.keys() methodNames.sort() return methodNames def methodHelp(self, methodName): """method help""" if methodName in self.funcs: return self.funcs[methodName].__doc__ else: raise Exception('method "%s" is not supported' % methodName) if __name__ == '__main__': import sys if len(sys.argv) == 3: host = sys.argv[1] port = int(sys.argv[2]) else: host = 'localhost' port = 8111 server = MyXMLRPCServer(host, port, logRequests=True) # Run the server's main loop sa = server.socket.getsockname() print "Serving HTTPS on", sa[0], "port", sa[1] server.startup()
For a client use someting like this
from xmlrpclib import ServerProxy user = 'stas' pas = 'stas' p = ServerProxy('https://%s:%s@localhost:8111' % (user, pas)) print p.give_me_time()
Hello Stas,
thank’s for your great Python script. After some little modification and the SSL thing, it works fine, as full x64 version.
I have a question – is there a way to modify it, so the server will accept custom paths, not only (‘/’, ‘/RPC2’) ?
I tried to debug it, and tried some other stuff, but nothing works, and on hint on the web could help. I suppose I do not understand fully where the rpc_paths tuple is intiated or reinitiated. I suggest it is due to threading or I need to overwrite an other method of a subclass… ?
Could you help?
Next step for me is to merge it with this:
http://stackoverflow.com/questions/8409608/python-xml-rpc-server-as-a-windows-service
– so it will be a fully installable Python service.
Thanks, Best Regards from Rosenheim, Germany
Peter
Hmmmm, first result of a quick googling reveals a lot.
https://www.google.com/search?q=python+xmlrpc+server+rpc2
And just in case google decides to show you different results:
http://stackoverflow.com/questions/15507552/how-to-get-the-output-of-xmlrpc-server-python-2-7-3
And ofcourse the Python docs:
http://docs.python.org/2/library/simplexmlrpcserver.html
Section: 20.24.1
SimpleXMLRPCRequestHandler.rpc_paths
“””
An attribute value that must be a tuple listing valid path portions of the URL for receiving XML-RPC requests. Requests posted to other paths will result in a 404 “no such page” HTTP error. If this tuple is empty, all paths will be considered valid. The default value is (‘/’, ‘/RPC2’).
“””
Hello Stas!
Thank’s for your hints 🙂 But even after lot of examination nothing works – connection is refused, because it’s not possible to set an own attribute… only the default paths are working. I suggest, this has something to do with threading extension.
Anyway, this code with your SSL + basic auth and with threading from stackoverflow.com/questions/8409608/python-xml-rpc-server-as-a-windows-service works so far – it’s not a stable version!
################################################################################
# scxrs_threading
################################################################################
__updated__ = “2013-12-11”
from threading import Thread
from OpenSSL.SSL import SysCallError as SSL_SysCallError
from socket import error as socket_error
class ThreadingMixInCustom( object ):
“””Mix-in class to handle each request in a new thread.”””
# Decides how threads will act upon termination of the main process
daemon_threads = False
# rpc_paths = ( “/”, “/RPC2”, “SCXRS” )
def process_request_thread( self, request, client_address ):
“””Same as in BaseServer but as a thread.
In addition, exception handling is done here.
“””
try:
self.finish_request( request, client_address )
self.close_request( request )
except ( socket_error, SSL_SysCallError ), why:
print ‘socket.error finishing request from “%s”; Error: %s’ % ( client_address, str( why ) )
self.close_request( request )
except:
self.handle_error( request, client_address )
self.close_request( request )
def process_request( self, request, client_address ):
“””Start a new thread to process the request.”””
t = Thread( target = self.process_request_thread, args = ( request, client_address ) )
if self.daemon_threads:
t.setDaemon( 1 )
t.start()
################################################################################
# scxrs_services
################################################################################
__updated__ = “2013-12-11”
from SimpleXMLRPCServer import list_public_methods
import inspect
import os
import time
from xmlrpclib import Binary as xmlrpc_binary
class XMLRPCServices( object ):
def _list_public_methods( self ):
return list_public_methods( self )
def _method_help( self, method ):
f = getattr( self, method )
return inspect.getdoc( f )
def list( self, dir_name ):
“””list(dir_name) => []
Returns a list containing the contents of the named directory.
“””
return os.listdir( dir_name )
def put( self, filename, filedata ):
try:
with open( filename, “wb” ) as handle:
handle.write( filedata.data )
handle.close()
return ‘ok’
except Exception, ex:
return ‘error’
def get( self, filepath ):
try:
handle = open( filepath )
return xmlrpc_binary( handle.read() )
handle.close()
except:
return ‘error’
def system( self, command ):
result = os.system( command )
return result
def give_me_time( self ):
return time.asctime()
def run_script( self, filename, globals_ = globals(), locals_ = locals() ):
execfile( filename, globals_, locals_ )
################################################################################
# scxrs (the main file)
################################################################################
__updated__ = “2014-01-09”
import socket
import traceback
from thread import start_new_thread
from threading import Condition
from SocketServer import BaseServer
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler, SimpleXMLRPCDispatcher
from base64 import b64decode
import win32api
# import win32con
# import win32evtlogutil
import win32event
import win32service
import win32serviceutil
from OpenSSL.SSL import Connection as SSL_Connection
from OpenSSL.SSL import Context as SSL_Context
from OpenSSL.SSL import SSLv23_METHOD
from scxrs_threading import ThreadingMixInCustom
from scxrs_services import XMLRPCServices
# assuming cert- and key file are in same directory as this script
import os, inspect
filename = inspect.getframeinfo( inspect.currentframe() ).filename
my_path = os.path.dirname( os.path.abspath( filename ) )
DEFAULT_CERTFILE = os.path.join( my_path, “cacert.pem” ) # PEM formatted certificate file
DEFAULT_KEYFILE = os.path.join( my_path, “privkey.pem” ) # PEM formatted key file
DEFAULT_SERVICE_NAME = filename.split( “.” )[:1] or “SCXPCSVC”
LISTEN_HOST, LISTEN_PORT = ‘0.0.0.0’, 8443
#===========================================================================================
# REQUEST HANDLER
#===========================================================================================
class XMLRPCRequestHandlerAuth( SimpleXMLRPCRequestHandler ):
def address_string( self ):
“””getting ‘FQDN’ from host seems to stall on some ip addresses, so…
just (quickly!) return raw host address”””
host, port = self.client_address
# return socket.getfqdn(host)
return host
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_GET( self ):
“””Handles the HTTP GET request.
Interpret all HTTP GET requests as requests for server
documentation.
“””
# Check that the path is legal
if not self.is_rpc_path_valid():
# TODO: implement rpc_paths validation
self.report_404()
return
response = self.server.generate_html_documentation()
self.send_response( 200 )
self.send_header( “Content-type”, “text/html” )
self.send_header( “Content-length”, str( len( response ) ) )
self.end_headers()
self.wfile.write( response )
# shut down the connection
self.wfile.flush()
self.connection.shutdown() # Modified here!
return
def do_POST( self ):
“””Handles the HTTPS POST request.
It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly.
“””
try:
# get arguments
data = self.rfile.read( int( self.headers[“content-length”] ) )
# In previous versions of SimpleXMLRPCServer, _dispatch
# could be overridden in this class, instead of in
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
# check to see if a subclass implements _dispatch and dispatch
# using that method if present.
response = self.server._marshaled_dispatch( data, getattr( self, ‘_dispatch’, None ) )
except Exception, info: # This should only happen if the module is buggy
print “ERROR do_POST: “, info
print “Traceback follows:”, traceback.print_exc()
# internal error, report as HTTP server error
self.send_response( 500 )
self.end_headers()
else:
# got a 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 the connection
self.wfile.flush()
self.connection.shutdown() # Modified here!
return
def parse_request( self ):
if SimpleXMLRPCRequestHandler.parse_request( self ):
basic, foo, encoded = self.headers.get( ‘Authorization’ ).partition( ‘ ‘ )
username, foo, password = b64decode( encoded ).partition( ‘:’ )
# print user_name, f_o_o, password
# TODO: log authentication requests here
# TODO: implement authentication back end here
if username == ‘admin’:
return True
else:
self.send_error( 401, ‘Authentication failed’ )
return False
else:
return False
def report_404( self ):
# Report a 404 error
self.send_response( 404 )
response = ‘No such page’
self.send_header( “Content-type”, “text/plain” )
self.send_header( “Content-length”, str( len( response ) ) )
self.end_headers()
self.wfile.write( response )
# shut down the connection
self.wfile.flush()
self.connection.shutdown() # Modified here!
return
#===========================================================================================
# SERVER
#===========================================================================================
# class XMLRPCServer( BaseHTTPServer.HTTPServer, SimpleXMLRPCDispatcher ):
# def __init__( self, server_address, HandlerClass, logRequests = True ):
# “”” XML-RPC server. “””
# self.logRequests = logRequests
#
# SimpleXMLRPCDispatcher.__init__( self, False, None )
# BaseServer.__init__( self, server_address, HandlerClass )
#
# self.socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
#
# self.server_bind()
# self.server_activate()
class XMLRPCServerSSL( ThreadingMixInCustom, SimpleXMLRPCServer, XMLRPCRequestHandlerAuth ):
def __init__( self, ip, port, keyFile = DEFAULT_KEYFILE, certFile = DEFAULT_CERTFILE,
logRequests = True, allow_none = False, encoding = None, allow_dotted_names = True ):
self.logRequests = logRequests
self.allow_none = allow_none
self.allow_dotted_names = allow_dotted_names
self.encoding = encoding
#————————————————————————————————————–
SimpleXMLRPCDispatcher.__init__( self, self.allow_none, self.encoding )
BaseServer.__init__( self, ( ip, port ), XMLRPCRequestHandlerAuth )
#————————————————————————————————————–
# SSL socket stuff
ctx = SSL_Context( SSLv23_METHOD )
ctx.use_privatekey_file( keyFile )
ctx.use_certificate_file( certFile )
self.socket = SSL_Connection( ctx, socket.socket( self.address_family, self.socket_type ) )
self.server_bind()
self.server_activate()
#————————————————————————————————————–
self.funcs = {}
self.register_introspection_functions()
self.register_instance( XMLRPCServices() )
#————————————————————————————————————–
# requests count and condition, to allow for keyboard quit via CTL-C
self.requests = 0
self.rCondition = Condition()
def get_request( self ):
request, client_address = self.socket.accept()
self.rCondition.acquire()
self.requests += 1
self.rCondition.notifyAll()
self.rCondition.release()
return ( request, client_address )
def listMethods( self ):
“””return list of method names (strings)”””
methodNames = self.funcs.keys()
methodNames.sort()
return methodNames
def methodHelp( self, methodName ):
“””method help”””
if methodName in self.funcs:
return self.funcs[methodName].__doc__
else:
raise Exception( ‘method “%s” is not supported’ % methodName )
def startup( self ):
# run until quit signaled from keyboard
print ‘server starting; hit CTRL-C to quit…’
while True:
try:
self.rCondition.acquire()
start_new_thread( self.handle_request, () ) # we do this async, because handle_request blocks!
while not self.requests:
self.rCondition.wait( timeout = 3.0 )
if self.requests:
self.requests -= 1
self.rCondition.release()
except KeyboardInterrupt:
print “quit signaled, i’m done.”
return
def XMLRPCServerGet( HandlerClass = XMLRPCRequestHandlerAuth, ServerClass = XMLRPCServerSSL ):
“””xml rpc over https server”””
# def XMLRPCServerSSL.__init__(self,
# ip, port,
# keyFile=DEFAULT_KEYFILE, certFile=DEFAULT_CERTFILE,
# logRequests=True,
# allow_none=False, encoding=None, allow_dotted_names=True)
server = ServerClass( LISTEN_HOST, LISTEN_PORT,
keyFile = DEFAULT_KEYFILE, certFile = DEFAULT_CERTFILE,
allow_none = True )
# server.register_introspection_functions()
# server.register_instance( XMLRPCServices() )
# sa = server.socket.getsockname()
return server
# server.serve_forever()
# print “Serving HTTP on”, sa[0], “port”, sa[1]
# try:
# #print ‘Use Control-C to exit’
# server.serve_forever()
# except KeyboardInterrupt:
# #print ‘Exiting’
class XMLRPCServerService( win32serviceutil.ServiceFramework ):
_svc_name_ = DEFAULT_SERVICE_NAME # “XMLRPCServerService”
_svc_display_name_ = DEFAULT_SERVICE_NAME # “XMLRPCServerService”
_svc_description_ = “XML RPC SERVER SERVICE OVER SSL”
def __init__( self, args ):
win32serviceutil.ServiceFramework.__init__( self, args )
self.hWaitStop = win32event.CreateEvent( None, 0, 0, None )
def SvcStop( self ):
self.ReportServiceStatus( win32service.SERVICE_STOP_PENDING )
self.ReportServiceStatus( win32service.SERVICE_STOPPED )
win32event.SetEvent( self.hWaitStop )
def SvcDoRun( self ):
import servicemanager
servicemanager.LogMsg( servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, ( self._svc_name_, ” ) )
self.timeout = 100
server = XMLRPCServerGet()
# server.serve_forever()
while 1:
# Wait for service stop signal, if I timeout, loop again
rc = win32event.WaitForSingleObject( self.hWaitStop, self.timeout )
# Check to see if self.hWaitStop happened
if rc == win32event.WAIT_OBJECT_0:
# Stop signal encountered
server.shutdown()
servicemanager.LogInfoMsg( “XMLRPCServerService – STOPPED” )
break
else:
server.handle_request()
servicemanager.LogInfoMsg( “XMLRPCServerService – is alive and well” )
def ctrlHandler( ctrlType ):
return True
#===========================================================================================
if __name__ == ‘__main__’:
win32api.SetConsoleCtrlHandler( ctrlHandler, True )
# def HandleCommandLine(cls, serviceClassString = None, argv = None, customInstallOptions = “”,
# customOptionHandler = None)
win32serviceutil.HandleCommandLine( XMLRPCServerService )
#———————————————————————————————————————-
# if __name__ == ‘__main__’:
# import sys
# if len( sys.argv ) == 3:
# host = sys.argv[1]
# port = int( sys.argv[2] )
# else:
# host = DEFAULT_LISTEN_IP
# port = DEFAULT_LISTEN_PORT
#
# server = MyXMLRPCServer( host, port, logRequests = True,
# allow_none = True )
# # Run the server’s main loop
# sa = server.socket.getsockname()
# print “Serving HTTPS on”, sa[0], “port”, sa[1]
#
# server.startup()
#===========================================================================================
Hi Peter, thanks for sharing the code 🙂
I will make new post with your code so that it’s properly formatted in case someone wants to copy/paste it.
Of course I will refer to you as the (co) author 🙂
Licensing for code? Hi Stas. Thanks for this. I updated it for Python 3. I am wondering what the license for the code is so I can add a license to the public release.
https://github.com/etopian/python3-xmlrpc-ssl-basic-auth
Thanks.
Hi Sami, thanks for sharing the pythonv3 version.
Well the license, all my code is GPLv3 but as this code was a collection code all placed in public space and I just combined the pieces and added some of my own I would say it’s “in the public domain” and not licensed.
Also the additions Peter made are considered “in the public domain” so again I would say all code is free to use and is not licensed.
http://www.gnu.org/philosophy/categories.en.html
What I do when I use “public domain” code in my projects is that I add a notice that the piece of code is “in the public domain” and taken from [url bla bla]
So that whenever someone wants to know where the code comes from they can track it down to the original url.