added SOAP support
[sfa.git] / geni / util / geniserver.py
1 ##
2 # This module implements a general-purpose server layer for geni.
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 SimpleXMLRPCServer
10
11 import sys
12 import traceback
13 import threading
14 import SocketServer
15 import BaseHTTPServer
16 import SimpleHTTPServer
17 import SimpleXMLRPCServer
18 import xmlrpclib
19 import string
20 # SOAP support is optional
21 try:
22     import SOAPpy
23     from SOAPpy.Parser import parseSOAPRPC
24     from SOAPpy.Types import faultType
25     from SOAPpy.NS import NS
26     from SOAPpy.SOAPBuilder import buildSOAP
27 except ImportError:
28     SOAPpy = None
29
30 from cert import *
31 from credential import *
32
33 import socket, os
34 from OpenSSL import SSL
35
36
37 # See "2.2 Characters" in the XML specification:
38 #
39 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
40 # avoiding
41 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
42
43 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
44 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
45
46 def xmlrpclib_escape(s, replace = string.replace):
47     """
48     xmlrpclib does not handle invalid 7-bit control characters. This
49     function augments xmlrpclib.escape, which by default only replaces
50     '&', '<', and '>' with entities.
51     """
52
53     # This is the standard xmlrpclib.escape function
54     s = replace(s, "&", "&amp;")
55     s = replace(s, "<", "&lt;")
56     s = replace(s, ">", "&gt;",)
57
58     # Replace invalid 7-bit control characters with '?'
59     return s.translate(xml_escape_table)
60
61 def xmlrpclib_dump(self, value, write):
62     """
63     xmlrpclib cannot marshal instances of subclasses of built-in
64     types. This function overrides xmlrpclib.Marshaller.__dump so that
65     any value that is an instance of one of its acceptable types is
66     marshalled as that type.
67
68     xmlrpclib also cannot handle invalid 7-bit control characters. See
69     above.
70     """
71
72     # Use our escape function
73     args = [self, value, write]
74     if isinstance(value, (str, unicode)):
75         args.append(xmlrpclib_escape)
76
77     try:
78         # Try for an exact match first
79         f = self.dispatch[type(value)]
80     except KeyError:
81         # Try for an isinstance() match
82         for Type, f in self.dispatch.iteritems():
83             if isinstance(value, Type):
84                 f(*args)
85                 return
86         raise TypeError, "cannot marshal %s objects" % type(value)
87     else:
88         f(*args)
89
90 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
91
92 ##
93 # Verification callback for pyOpenSSL. We do our own checking of keys because
94 # we have our own authentication spec. Thus we disable several of the normal
95 # prohibitions that OpenSSL places on certificates
96
97 def verify_callback(conn, x509, err, depth, preverify):
98     # if the cert has been preverified, then it is ok
99     if preverify:
100        #print "  preverified"
101        return 1
102
103     # we're only passing single certificates, not chains
104     if depth > 0:
105        #print "  depth > 0 in verify_callback"
106        return 0
107
108     # create a Certificate object and load it from the client's x509
109     ctx = conn.get_context()
110     server = ctx.get_app_data()
111     server.peer_cert = Certificate()
112     server.peer_cert.load_from_pyopenssl_x509(x509)
113
114     # the certificate verification done by openssl checks a number of things
115     # that we aren't interested in, so we look out for those error messages
116     # and ignore them
117
118     # XXX SMBAKER: I don't know what this error is, but it's being returned
119     # by newer pl nodes.
120     if err == 9:
121        #print "  X509_V_ERR_CERT_NOT_YET_VALID"
122        return 1
123
124     # allow self-signed certificates
125     if err == 18:
126        #print "  X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT"
127        return 1
128
129     # allow certs that don't have an issuer
130     if err == 20:
131        #print "  X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY"
132        return 1
133
134     # allow certs that are untrusted
135     if err == 21:
136        #print "  X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE"
137        return 1
138
139     # allow certs that are untrusted
140     if err == 27:
141        #print "  X509_V_ERR_CERT_UNTRUSTED"
142        return 1
143
144     print "  error", err, "in verify_callback"
145
146     return 0\r
147
148 ##
149 # Taken from the web (XXX find reference). Implements an HTTPS xmlrpc server
150
151 class SecureXMLRPCServer(BaseHTTPServer.HTTPServer,SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
152     def __init__(self, server_address, HandlerClass, key_file, cert_file, logRequests=True):
153         """Secure XML-RPC server.
154
155         It it very similar to SimpleXMLRPCServer but it uses HTTPS for transporting XML data.
156         """
157         self.logRequests = logRequests
158
159         SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, True, None)
160         SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
161         ctx = SSL.Context(SSL.SSLv23_METHOD)
162         ctx.use_privatekey_file(key_file)
163         ctx.use_certificate_file(cert_file)
164         ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback)
165         ctx.set_app_data(self)
166         self.socket = SSL.Connection(ctx, socket.socket(self.address_family,
167                                                         self.socket_type))
168         self.server_bind()
169         self.server_activate()
170
171     # _dispatch
172     #
173     # Convert an exception on the server to a full stack trace and send it to
174     # the client.
175
176     def _dispatch(self, method, params):
177         try:
178             return SimpleXMLRPCServer.SimpleXMLRPCDispatcher._dispatch(self, method, params)
179         except:
180             # can't use format_exc() as it is not available in jython yet
181             # (evein in trunk).
182             type, value, tb = sys.exc_info()
183             raise xmlrpclib.Fault(1,''.join(traceback.format_exception(type, value, tb)))
184
185 ##
186 # taken from the web (XXX find reference). Implents HTTPS xmlrpc request handler
187
188 class SecureXMLRpcRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
189     """Secure XML-RPC request handler class.
190
191     It it very similar to SimpleXMLRPCRequestHandler but it uses HTTPS for transporting XML data.
192     """
193     def setup(self):
194         self.encoding = "utf-8"
195         self.connection = self.request
196         self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
197         self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
198
199     def handleXMLRPCSOAP(self, source, data):
200         """
201         Handle an XML-RPC or SOAP request from the specified source.
202         """
203         
204         # Parse request into method name and arguments
205         try:
206             interface = xmlrpclib
207             (args, method) = xmlrpclib.loads(data)
208             methodresponse = True
209         except Exception, e:
210             if SOAPpy is not None:
211                 interface = SOAPpy
212                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
213                 method = r._name
214                 args = r._aslist()
215                 # XXX Support named arguments
216             else:
217                 raise e
218
219         try:
220             result = self.server._marshaled_dispatch(data, getattr(self, '_dispatch', None))
221         except Exception, fault:
222             # Handle expected faults
223             if interface == xmlrpclib:
224                 result = fault
225                 methodresponse = None
226             elif interface == SOAPpy:
227                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
228                 result._setDetail("Fault: %s" % (fault))
229
230
231         # Return result
232         if interface == xmlrpclib:
233             # XX Shouldnt we be dumping xmlrpc here
234             #result = (result,)
235             #data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
236             data = result 
237         elif interface == SOAPpy:
238             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
239
240         return data
241
242     def do_POST(self):
243         """Handles the HTTPS POST request.
244
245         It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly.
246         """
247
248         try:
249             # get arguments
250             request = self.rfile.read(int(self.headers["content-length"]))
251             response = self.handleXMLRPCSOAP(None, request)
252
253             # In previous versions of SimpleXMLRPCServer, _dispatch
254             # could be overridden in this class, instead of in
255             # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
256             # check to see if a subclass implements _dispatch and dispatch
257             # using that method if present.
258             #response = self.server._marshaled_dispatch(request, getattr(self, '_dispatch', None))
259         except: # This should only happen if the module is buggy
260             # internal error, report as HTTP server error
261             self.send_response(500)
262
263             self.end_headers()
264         else:
265             # got a valid XML RPC response
266             self.send_response(200)
267             self.send_header("Content-type", "text/xml")
268             self.send_header("Content-length", str(len(response)))
269             self.end_headers()
270             self.wfile.write(response)
271
272             # shut down the connection
273             self.wfile.flush()
274             self.connection.shutdown() # Modified here!
275
276 ##
277 # Implements an HTTPS XML-RPC server. Generally it is expected that GENI
278 # functions will take a credential string, which is passed to
279 # decode_authentication. Decode_authentication() will verify the validity of
280 # the credential, and verify that the user is using the key that matches the
281 # GID supplied in the credential.
282
283 class GeniServer(threading.Thread):
284
285     ##
286     # Create a new GeniServer object.
287     #
288     # @param ip the ip address to listen on
289     # @param port the port to listen on
290     # @param key_file private key filename of registry
291     # @param cert_file certificate filename containing public key 
292     #   (could be a GID file)
293
294     def __init__(self, ip, port, key_file, cert_file):
295         threading.Thread.__init__(self)
296         self.key = Keypair(filename = key_file)
297         self.cert = Certificate(filename = cert_file)
298         self.server = SecureXMLRPCServer((ip, port), SecureXMLRpcRequestHandler, key_file, cert_file)
299         self.trusted_cert_list = None
300         self.register_functions()
301
302     ##
303     # Decode the credential string that was submitted by the caller. Several
304     # checks are performed to ensure that the credential is valid, and that the
305     # callerGID included in the credential matches the caller that is
306     # connected to the HTTPS connection.
307
308     def decode_authentication(self, cred_string, operation):
309         self.client_cred = Credential(string = cred_string)
310         self.client_gid = self.client_cred.get_gid_caller()
311         self.object_gid = self.client_cred.get_gid_object()
312
313         # make sure the client_gid is not blank
314         if not self.client_gid:
315             raise MissingCallerGID(self.client_cred.get_subject())
316
317         # make sure the client_gid matches client's certificate
318         peer_cert = self.server.peer_cert
319         if not peer_cert.is_pubkey(self.client_gid.get_pubkey()):
320             raise ConnectionKeyGIDMismatch(self.client_gid.get_subject())
321
322         # make sure the client is allowed to perform the operation
323         if operation:
324             if not self.client_cred.can_perform(operation):
325                 raise InsufficientRights(operation)
326
327         if self.trusted_cert_list:
328             self.client_cred.verify_chain(self.trusted_cert_list)
329             if self.client_gid:
330                 self.client_gid.verify_chain(self.trusted_cert_list)
331             if self.object_gid:
332                 self.object_gid.verify_chain(self.trusted_cert_list)
333
334     ##
335     # Register functions that will be served by the XMLRPC server. This
336     # function should be overrided by each descendant class.
337
338     def register_functions(self):
339         self.server.register_function(self.noop)
340
341     ##
342     # Sample no-op server function. The no-op function decodes the credential
343     # that was passed to it.
344
345     def noop(self, cred, anything):
346         self.decode_authentication(cred)
347
348         return anything
349
350     ##
351     # Execute the server, serving requests forever. 
352
353     def run(self):
354         self.server.serve_forever()
355
356