9b1e1b8c7321db1f1e4341cd3138d44ce85ccce0
[sfa.git] / sfa / server / xmlrpcapi.py
1 #
2 # SFA XML-RPC and SOAP interfaces
3 #
4
5 import string
6
7 # SOAP support is optional
8 try:
9     import SOAPpy
10     from SOAPpy.Parser import parseSOAPRPC
11     from SOAPpy.Types import faultType
12     from SOAPpy.NS import NS
13     from SOAPpy.SOAPBuilder import buildSOAP
14 except ImportError:
15     SOAPpy = None
16
17 ####################
18 #from sfa.util.faults import SfaNotImplemented, SfaAPIError, SfaInvalidAPIMethod, SfaFault
19 from sfa.util.faults import SfaInvalidAPIMethod, SfaAPIError, SfaFault
20 from sfa.util.sfalogging import logger
21 from sfa.util.py23 import xmlrpc_client
22
23 ####################
24 # See "2.2 Characters" in the XML specification:
25 #
26 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
27 # avoiding
28 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
29
30 invalid_codepoints = range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F)
31 # broke with f24, somehow we get a unicode as an incoming string to be translated
32 str_xml_escape_table = string.maketrans("".join((chr(x) for x in invalid_codepoints)),
33                                         "?" * len(invalid_codepoints))
34 # loosely inspired from
35 # http://stackoverflow.com/questions/1324067/how-do-i-get-str-translate-to-work-with-unicode-strings
36 unicode_xml_escape_table = { invalid : u"?" for invalid in invalid_codepoints}
37
38 def xmlrpclib_escape(s, replace = string.replace):
39     """
40     xmlrpclib does not handle invalid 7-bit control characters. This
41     function augments xmlrpclib.escape, which by default only replaces
42     '&', '<', and '>' with entities.
43     """
44
45     # This is the standard xmlrpclib.escape function
46     s = replace(s, "&", "&amp;")
47     s = replace(s, "<", "&lt;")
48     s = replace(s, ">", "&gt;",)
49
50     # Replace invalid 7-bit control characters with '?'
51     if isinstance(s, str):
52         return s.translate(str_xml_escape_table)
53     else:
54         return s.translate(unicode_xml_escape_table)
55
56
57 def xmlrpclib_dump(self, value, write):
58     """
59     xmlrpclib cannot marshal instances of subclasses of built-in
60     types. This function overrides xmlrpclib.Marshaller.__dump so that
61     any value that is an instance of one of its acceptable types is
62     marshalled as that type.
63
64     xmlrpclib also cannot handle invalid 7-bit control characters. See
65     above.
66     """
67
68     # Use our escape function
69     args = [self, value, write]
70     if isinstance(value, (str, unicode)):
71         args.append(xmlrpclib_escape)
72
73     try:
74         # Try for an exact match first
75         f = self.dispatch[type(value)]
76     except KeyError:
77         raise
78         # Try for an isinstance() match
79         for Type, f in self.dispatch.iteritems():
80             if isinstance(value, Type):
81                 f(*args)
82                 return
83         raise TypeError("cannot marshal %s objects" % type(value))
84     else:
85         f(*args)
86
87 # You can't hide from me!
88 # Note: not quite  sure if this will still cause
89 # the expected behaviour under python3
90 xmlrpc_client.Marshaller._Marshaller__dump = xmlrpclib_dump
91
92
93 class XmlrpcApi:
94     """
95     The XmlrpcApi class implements a basic xmlrpc (or soap) service 
96     """
97
98     protocol = None
99
100     def __init__(self, encoding="utf-8", methods='sfa.methods'):
101
102         self.encoding = encoding
103         self.source = None
104
105         # flat list of method names
106         self.methods_module = methods_module = __import__(
107             methods, fromlist=[methods])
108         self.methods = methods_module.all
109
110         self.logger = logger
111
112     def callable(self, method):
113         """
114         Return a new instance of the specified method.
115         """
116         # Look up method
117         if method not in self.methods:
118             raise SfaInvalidAPIMethod(method)
119
120         # Get new instance of method
121         try:
122             classname = method.split(".")[-1]
123             module = __import__(self.methods_module.__name__ +
124                                 "." + method, globals(), locals(), [classname])
125             callablemethod = getattr(module, classname)(self)
126             return getattr(module, classname)(self)
127         except (ImportError, AttributeError):
128             self.logger.log_exc("Error importing method: %s" % method)
129             raise SfaInvalidAPIMethod(method)
130
131     def call(self, source, method, *args):
132         """
133         Call the named method from the specified source with the
134         specified arguments.
135         """
136         function = self.callable(method)
137         function.source = source
138         self.source = source
139         return function(*args)
140
141     def handle(self, source, data, method_map):
142         """
143         Handle an XML-RPC or SOAP request from the specified source.
144         """
145         # Parse request into method name and arguments
146         try:
147             interface = xmlrpc_client
148             self.protocol = 'xmlrpc'
149             (args, method) = xmlrpc_client.loads(data)
150             if method in method_map:
151                 method = method_map[method]
152             methodresponse = True
153
154         except Exception as e:
155             if SOAPpy is not None:
156                 self.protocol = 'soap'
157                 interface = SOAPpy
158                 (r, header, body, attrs) = parseSOAPRPC(
159                     data, header=1, body=1, attrs=1)
160                 method = r._name
161                 args = r._aslist()
162                 # XXX Support named arguments
163             else:
164                 raise e
165
166         try:
167             result = self.call(source, method, *args)
168         except SfaFault as fault:
169             result = fault
170             self.logger.log_exc("XmlrpcApi.handle has caught Exception")
171         except Exception as fault:
172             self.logger.log_exc("XmlrpcApi.handle has caught Exception")
173             result = SfaAPIError(fault)
174
175         # Return result
176         response = self.prepare_response(result, method)
177         return response
178
179     def prepare_response(self, result, method=""):
180         """
181         convert result to a valid xmlrpc or soap response
182         """
183
184         if self.protocol == 'xmlrpc':
185             if not isinstance(result, SfaFault):
186                 result = (result,)
187             response = xmlrpc_client.dumps(
188                 result, methodresponse=True, encoding=self.encoding, allow_none=1)
189         elif self.protocol == 'soap':
190             if isinstance(result, Exception):
191                 result = faultParameter(
192                     NS.ENV_T + ":Server", "Method Failed", method)
193                 result._setDetail("Fault %d: %s" %
194                                   (result.faultCode, result.faultString))
195             else:
196                 response = buildSOAP(
197                     kw={'%sResponse' % method: {'Result': result}}, encoding=self.encoding)
198         else:
199             if isinstance(result, Exception):
200                 raise result
201
202         return response