ignore X509_V_ERR_CERT_SIGNATURE_FAILURE
[sfa.git] / sfa / server / threadedserver.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
11 import traceback
12 import threading
13 from Queue import Queue
14 import xmlrpclib
15 import SocketServer
16 import BaseHTTPServer
17 import SimpleXMLRPCServer
18 from OpenSSL import SSL
19
20 from sfa.util.sfalogging import logger
21 from sfa.util.config import Config
22 from sfa.util.cache import Cache 
23 from sfa.trust.certificate import Certificate
24 from sfa.trust.trustedroots import TrustedRoots
25
26 # don't hard code an api class anymore here
27 from sfa.generic import Generic
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     # xxx thierry: this most likely means the cert has a validity range in the future
47     # by newer pl nodes.
48     if err == 9:
49        #print "  X509_V_ERR_CERT_NOT_YET_VALID"
50        return 1
51
52     # allow self-signed certificates
53     if err == 18:
54        #print "  X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT"
55        return 1
56
57     # allow certs that don't have an issuer
58     if err == 20:
59        #print "  X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
60        return 1
61
62     # allow chained certs with self-signed roots
63     if err == 19:
64         return 1
65     
66     # allow certs that are untrusted
67     if err == 21:
68        #print "  X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE"
69        return 1
70
71     # allow certs that are untrusted
72     if err == 27:
73        #print "  X509_V_ERR_CERT_UNTRUSTED"
74        return 1
75
76     # ignore X509_V_ERR_CERT_SIGNATURE_FAILURE
77     if err == 7:
78        return 1         
79
80     logger.debug("  error %s in verify_callback"%err)
81
82     return 0
83
84 ##
85 # taken from the web (XXX find reference). Implements HTTPS xmlrpc request handler
86 class SecureXMLRpcRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
87     """Secure XML-RPC request handler class.
88
89     It it very similar to SimpleXMLRPCRequestHandler but it uses HTTPS for transporting XML data.
90     """
91     def setup(self):
92         self.connection = self.request
93         self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
94         self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
95
96     def do_POST(self):
97         """Handles the HTTPS POST request.
98
99         It was copied out from SimpleXMLRPCServer.py and modified to shutdown 
100         the socket cleanly.
101         """
102         try:
103             peer_cert = Certificate()
104             peer_cert.load_from_pyopenssl_x509(self.connection.get_peer_certificate())
105             generic=Generic.the_flavour()
106             self.api = generic.make_api (peer_cert = peer_cert, 
107                                          interface = self.server.interface, 
108                                          key_file = self.server.key_file, 
109                                          cert_file = self.server.cert_file,
110                                          cache = self.cache)
111             #logger.info("SecureXMLRpcRequestHandler.do_POST:")
112             #logger.info("interface=%s"%self.server.interface)
113             #logger.info("key_file=%s"%self.server.key_file)
114             #logger.info("api=%s"%self.api)
115             #logger.info("server=%s"%self.server)
116             #logger.info("handler=%s"%self)
117             # get arguments
118             request = self.rfile.read(int(self.headers["content-length"]))
119             remote_addr = (remote_ip, remote_port) = self.connection.getpeername()
120             self.api.remote_addr = remote_addr            
121             response = self.api.handle(remote_addr, request, self.server.method_map)
122         except Exception, fault:
123             # This should only happen if the module is buggy
124             # internal error, report as HTTP server error
125             logger.log_exc("server.do_POST")
126             response = self.api.prepare_response(fault)
127             #self.send_response(500)
128             #self.end_headers()
129        
130         # got a valid response
131         self.send_response(200)
132         self.send_header("Content-type", "text/xml")
133         self.send_header("Content-length", str(len(response)))
134         self.end_headers()
135         self.wfile.write(response)
136
137         # shut down the connection
138         self.wfile.flush()
139         self.connection.shutdown() # Modified here!
140
141 ##
142 # Taken from the web (XXX find reference). Implements an HTTPS xmlrpc server
143 class SecureXMLRPCServer(BaseHTTPServer.HTTPServer,SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
144
145     def __init__(self, server_address, HandlerClass, key_file, cert_file, logRequests=True):
146         """Secure XML-RPC server.
147
148         It it very similar to SimpleXMLRPCServer but it uses HTTPS for transporting XML data.
149         """
150         logger.debug("SecureXMLRPCServer.__init__, server_address=%s, cert_file=%s, key_file=%s"%(server_address,cert_file,key_file))
151         self.logRequests = logRequests
152         self.interface = None
153         self.key_file = key_file
154         self.cert_file = cert_file
155         self.method_map = {}
156         # add cache to the request handler
157         HandlerClass.cache = Cache()
158         #for compatibility with python 2.4 (centos53)
159         if sys.version_info < (2, 5):
160             SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self)
161         else:
162            SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, True, None)
163         SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
164         ctx = SSL.Context(SSL.SSLv23_METHOD)
165         ctx.use_privatekey_file(key_file)        
166         ctx.use_certificate_file(cert_file)
167         # If you wanted to verify certs against known CAs.. this is how you would do it
168         #ctx.load_verify_locations('/etc/sfa/trusted_roots/plc.gpo.gid')
169         config = Config()
170         trusted_cert_files = TrustedRoots(config.get_trustedroots_dir()).get_file_list()
171         for cert_file in trusted_cert_files:
172             ctx.load_verify_locations(cert_file)
173         ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback)
174         ctx.set_verify_depth(5)
175         ctx.set_app_data(self)
176         self.socket = SSL.Connection(ctx, socket.socket(self.address_family,
177                                                         self.socket_type))
178         self.server_bind()
179         self.server_activate()
180
181     # _dispatch
182     #
183     # Convert an exception on the server to a full stack trace and send it to
184     # the client.
185
186     def _dispatch(self, method, params):
187         logger.debug("SecureXMLRPCServer._dispatch, method=%s"%method)
188         try:
189             return SimpleXMLRPCServer.SimpleXMLRPCDispatcher._dispatch(self, method, params)
190         except:
191             # can't use format_exc() as it is not available in jython yet
192             # (even in trunk).
193             type, value, tb = sys.exc_info()
194             raise xmlrpclib.Fault(1,''.join(traceback.format_exception(type, value, tb)))
195
196     # override this one from the python 2.7 code
197     # originally defined in class TCPServer
198     def shutdown_request(self, request):
199         """Called to shutdown and close an individual request."""
200         # ---------- 
201         # the std python 2.7 code just attempts a request.shutdown(socket.SHUT_WR)
202         # this works fine with regular sockets
203         # However we are dealing with an instance of OpenSSL.SSL.Connection instead
204         # This one only supports shutdown(), and in addition this does not
205         # always perform as expected
206         # ---------- std python 2.7 code
207         try:
208             #explicitly shutdown.  socket.close() merely releases
209             #the socket and waits for GC to perform the actual close.
210             request.shutdown(socket.SHUT_WR)
211         except socket.error:
212             pass #some platforms may raise ENOTCONN here
213         # ----------
214         except TypeError:
215             # we are dealing with an OpenSSL.Connection object, 
216             # try to shut it down but never mind if that fails
217             try: request.shutdown()
218             except: pass
219         # ----------
220         self.close_request(request)
221
222 ## From Active State code: http://code.activestate.com/recipes/574454/
223 # This is intended as a drop-in replacement for the ThreadingMixIn class in 
224 # module SocketServer of the standard lib. Instead of spawning a new thread 
225 # for each request, requests are processed by of pool of reusable threads.
226 class ThreadPoolMixIn(SocketServer.ThreadingMixIn):
227     """
228     use a thread pool instead of a new thread on every request
229     """
230     # XX TODO: Make this configurable
231     # config = Config()
232     # numThreads = config.SFA_SERVER_NUM_THREADS
233     numThreads = 25
234     allow_reuse_address = True  # seems to fix socket.error on server restart
235
236     def serve_forever(self):
237         """
238         Handle one request at a time until doomsday.
239         """
240         # set up the threadpool
241         self.requests = Queue()
242
243         for x in range(self.numThreads):
244             t = threading.Thread(target = self.process_request_thread)
245             t.setDaemon(1)
246             t.start()
247
248         # server main loop
249         while True:
250             self.handle_request()
251             
252         self.server_close()
253
254     
255     def process_request_thread(self):
256         """
257         obtain request from queue instead of directly from server socket
258         """
259         while True:
260             SocketServer.ThreadingMixIn.process_request_thread(self, *self.requests.get())
261
262     
263     def handle_request(self):
264         """
265         simply collect requests and put them on the queue for the workers.
266         """
267         try:
268             request, client_address = self.get_request()
269         except socket.error:
270             return
271         if self.verify_request(request, client_address):
272             self.requests.put((request, client_address))
273
274 class ThreadedServer(ThreadPoolMixIn, SecureXMLRPCServer):
275     pass