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