Introduced method mapping so that XMLRPC calls can be forwarded to alternate methods.
[sfa.git] / sfa / util / server.py
1 ##
2 # This module implements a general-purpose server layer for sfa.
3 # The same basic server should be usable on the registry, component, or
4 # other interfaces.
5 #
6 # TODO: investigate ways to combine this with existing PLC server?
7 ##
8
9 ### $Id$
10 ### $URL$
11
12 import sys
13 import traceback
14 import threading
15 import socket, os
16 import SocketServer
17 import BaseHTTPServer
18 import SimpleHTTPServer
19 import SimpleXMLRPCServer
20 from OpenSSL import SSL
21 from Queue import Queue
22 from sfa.trust.certificate import Keypair, Certificate
23 from sfa.trust.credential import *
24 from sfa.util.faults import *
25 from sfa.plc.api import SfaAPI 
26 from sfa.util.debug import log
27
28 ##
29 # Verification callback for pyOpenSSL. We do our own checking of keys because
30 # we have our own authentication spec. Thus we disable several of the normal
31 # prohibitions that OpenSSL places on certificates
32
33 def verify_callback(conn, x509, err, depth, preverify):
34     # if the cert has been preverified, then it is ok
35     if preverify:
36        #print "  preverified"
37        return 1
38
39     # we're only passing single certificates, not chains
40     if depth > 0:
41        #print "  depth > 0 in verify_callback"
42        return 0
43
44     # the certificate verification done by openssl checks a number of things
45     # that we aren't interested in, so we look out for those error messages
46     # and ignore them
47
48     # XXX SMBAKER: I don't know what this error is, but it's being returned
49     # by newer pl nodes.
50     if err == 9:
51        #print "  X509_V_ERR_CERT_NOT_YET_VALID"
52        return 1
53
54     # allow self-signed certificates
55     if err == 18:
56        #print "  X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT"
57        return 1
58
59     # allow certs that don't have an issuer
60     if err == 20:
61        #print "  X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
62        return 1
63
64     # allow certs that are untrusted
65     if err == 21:
66        #print "  X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE"
67        return 1
68
69     # allow certs that are untrusted
70     if err == 27:
71        #print "  X509_V_ERR_CERT_UNTRUSTED"
72        return 1
73
74     print "  error", err, "in verify_callback"
75
76     return 0
77
78 ##
79 # taken from the web (XXX find reference). Implents HTTPS xmlrpc request handler
80 class SecureXMLRpcRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
81     """Secure XML-RPC request handler class.
82
83     It it very similar to SimpleXMLRPCRequestHandler but it uses HTTPS for transporting XML data.
84     """
85     def setup(self):
86         self.connection = self.request
87         self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
88         self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
89
90     def do_POST(self):
91         """Handles the HTTPS POST request.
92
93         It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly.
94         """
95         try:
96             peer_cert = Certificate()
97             peer_cert.load_from_pyopenssl_x509(self.connection.get_peer_certificate())
98             self.api = SfaAPI(peer_cert = peer_cert, 
99                               interface = self.server.interface, 
100                               key_file = self.server.key_file, 
101                               cert_file = self.server.cert_file)
102             # get arguments
103             request = self.rfile.read(int(self.headers["content-length"]))
104             remote_addr = (remote_ip, remote_port) = self.connection.getpeername()
105             self.api.remote_addr = remote_addr            
106             response = self.api.handle(remote_addr, request, self.server.method_map)
107
108         
109         except Exception, fault:
110             # This should only happen if the module is buggy
111             # internal error, report as HTTP server error
112             self.send_response(500)
113             self.end_headers()
114             traceback.print_exc()
115         else:
116             # got a valid XML RPC response
117             self.send_response(200)
118             self.send_header("Content-type", "text/xml")
119             self.send_header("Content-length", str(len(response)))
120             self.end_headers()
121             self.wfile.write(response)
122
123             # shut down the connection
124             self.wfile.flush()
125             self.connection.shutdown() # Modified here!
126
127 ##
128 # Taken from the web (XXX find reference). Implements an HTTPS xmlrpc server
129 class SecureXMLRPCServer(BaseHTTPServer.HTTPServer,SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
130     def __init__(self, server_address, HandlerClass, key_file, cert_file, logRequests=True):
131         """Secure XML-RPC server.
132
133         It it very similar to SimpleXMLRPCServer but it uses HTTPS for transporting XML data.
134         """
135         self.logRequests = logRequests
136         self.interface = None
137         self.key_file = key_file
138         self.cert_file = cert_file
139         self.method_map = {}
140
141         #for compatibility with python 2.4 (centos53)
142         if sys.version_info < (2, 5):
143             SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self)
144         else:
145            SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, True, None)
146         SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
147         ctx = SSL.Context(SSL.SSLv23_METHOD)
148         ctx.use_privatekey_file(key_file)
149         ctx.use_certificate_file(cert_file)
150         ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback)
151         ctx.set_app_data(self)
152         self.socket = SSL.Connection(ctx, socket.socket(self.address_family,
153                                                         self.socket_type))
154         self.server_bind()
155         self.server_activate()
156
157     # _dispatch
158     #
159     # Convert an exception on the server to a full stack trace and send it to
160     # the client.
161
162     def _dispatch(self, method, params):
163         try:
164             return SimpleXMLRPCServer.SimpleXMLRPCDispatcher._dispatch(self, method, params)
165         except:
166             # can't use format_exc() as it is not available in jython yet
167             # (evein in trunk).
168             type, value, tb = sys.exc_info()
169             raise xmlrpclib.Fault(1,''.join(traceback.format_exception(type, value, tb)))
170
171 ## From Active State code: http://code.activestate.com/recipes/574454/
172 # This is intended as a drop-in replacement for the ThreadingMixIn class in 
173 # module SocketServer of the standard lib. Instead of spawning a new thread 
174 # for each request, requests are processed by of pool of reusable threads.
175 class ThreadPoolMixIn(SocketServer.ThreadingMixIn):
176     """
177     use a thread pool instead of a new thread on every request
178     """
179     # XX TODO: Make this configurable
180     # config = Config()
181     # numThreads = config.SFA_SERVER_NUM_THREADS
182     numThreads = 25
183     allow_reuse_address = True  # seems to fix socket.error on server restart
184
185     def serve_forever(self):
186         """
187         Handle one request at a time until doomsday.
188         """
189         # set up the threadpool
190         self.requests = Queue()
191
192         for x in range(self.numThreads):
193             t = threading.Thread(target = self.process_request_thread)
194             t.setDaemon(1)
195             t.start()
196
197         # server main loop
198         while True:
199             self.handle_request()
200             
201         self.server_close()
202
203     
204     def process_request_thread(self):
205         """
206         obtain request from queue instead of directly from server socket
207         """
208         while True:
209             SocketServer.ThreadingMixIn.process_request_thread(self, *self.requests.get())
210
211     
212     def handle_request(self):
213         """
214         simply collect requests and put them on the queue for the workers.
215         """
216         try:
217             request, client_address = self.get_request()
218         except socket.error:
219             return
220         if self.verify_request(request, client_address):
221             self.requests.put((request, client_address))
222
223 class ThreadedServer(ThreadPoolMixIn, SecureXMLRPCServer):
224     pass
225 ##
226 # Implements an HTTPS XML-RPC server. Generally it is expected that SFA
227 # functions will take a credential string, which is passed to
228 # decode_authentication. Decode_authentication() will verify the validity of
229 # the credential, and verify that the user is using the key that matches the
230 # GID supplied in the credential.
231
232 class SfaServer(threading.Thread):
233
234     ##
235     # Create a new SfaServer object.
236     #
237     # @param ip the ip address to listen on
238     # @param port the port to listen on
239     # @param key_file private key filename of registry
240     # @param cert_file certificate filename containing public key 
241     #   (could be a GID file)
242
243     def __init__(self, ip, port, key_file, cert_file):
244         threading.Thread.__init__(self)
245         self.key = Keypair(filename = key_file)
246         self.cert = Certificate(filename = cert_file)
247         #self.server = SecureXMLRPCServer((ip, port), SecureXMLRpcRequestHandler, key_file, cert_file)
248         self.server = ThreadedServer((ip, port), SecureXMLRpcRequestHandler, key_file, cert_file)
249         self.trusted_cert_list = None
250         self.register_functions()
251
252
253     ##
254     # Register functions that will be served by the XMLRPC server. This
255     # function should be overridden by each descendant class.
256
257     def register_functions(self):
258         self.server.register_function(self.noop)
259
260     ##
261     # Sample no-op server function. The no-op function decodes the credential
262     # that was passed to it.
263
264     def noop(self, cred, anything):
265         self.decode_authentication(cred)
266
267         return anything
268
269     ##
270     # Execute the server, serving requests forever. 
271
272     def run(self):
273         self.server.serve_forever()
274
275