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