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