Ticket #45: yamlrpclib.py

File yamlrpclib.py, 16.6 KB (added by pkmurphy at postmaster dot co dot uk, 8 years ago)

The client

Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# A port of xmlrpclib to jsonrpclib to yamlrpclib....
4#
5#
6# The YAML-RPC client interface is based on the JSON-RPC client
7# which is based on the XML-RPC client.
8#
9# Copyright (c) 1999-2002 by Secret Labs AB
10# Copyright (c) 1999-2002 by Fredrik Lundh
11# Copyright (c) 2006 by Matt Harrison
12# Copyright (c) 2007 by Peter Murphy
13#
14# By obtaining, using, and/or copying this software and/or its
15# associated documentation, you agree that you have read, understood,
16# and will comply with the following terms and conditions:
17#
18# Permission to use, copy, modify, and distribute this software and
19# its associated documentation for any purpose and without fee is
20# hereby granted, provided that the above copyright notice appears in
21# all copies, and that both that copyright notice and this permission
22# notice appear in supporting documentation, and that the name of
23# Secret Labs AB or the author not be used in advertising or publicity
24# pertaining to distribution of the software without specific, written
25# prior permission.
26#
27# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
28# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
29# ABILITY AND FITNESS.  IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
30# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
31# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
32# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
33# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
34# OF THIS SOFTWARE.
35# --------------------------------------------------------------------
36
37import urllib
38import httplib
39import base64
40import yaml
41import yamlrpcbasic
42import types
43from xmlrpclib import _Method;
44from yamlrpcbasic import getAttrWay;
45
46# Implementation version.
47
48__version__ = "0.0.1"
49
50# Protocol implementation.
51
52protvers = "0.1"
53
54# We add error handling here.
55
56class YRPCException(Exception):
57    """ Base class for all YAML-RPC errors."""
58    def __str__(self):
59        return repr(self);
60    def __unicode__(self):
61        return repr(self);
62
63class YRPCProtocolException(YRPCException):
64    """ Indicates an HTTP/HTTPS protocol error. This is raised by the HTTP
65        transport layer, if the server returns an error code other than 200
66        (OK). The parameters are:
67
68        url: the target URL.
69        errcode: The HTTP or HTTPS error code.
70        errmsg: The HTTP or HTTPS error message.
71        headers The HTTP or HTTPS header dictionary.
72    """
73    def __init__(self, url, errcode, errmsg, headers):
74        YRPCException.__init__(self)
75        self.url = url
76        self.errcode = errcode
77        self.errmsg = errmsg
78        self.headers = headers
79    def __repr__(self):
80        return ("<YAML-RPC ProtocolError for %s: %s %s>" %
81            (self.url, self.errcode, self.errmsg))
82
83class YRPCErrorException(YRPCException):
84    """ This represents an YAML-RPC error object. This object is thrown when
85        the YAML-RPC server sends an error object back as YAML-RPC data.
86        For example, a called routine may not exist on the server, or there
87        are not enough parameters for the routine.
88    """
89    def __init__(self, errorObj):
90        """ One parameter. This is either an object of type YAMLRPCError, or
91            a dictionary with "name", "code", "message" and "error" members.
92        """
93        YRPCException.__init__(self);
94        self.errorObj = errorObj;
95    def __repr__(self):
96        if not self.errorObj:
97            return None;
98        obj = self.errorObj;
99        return ("<YAML-RPC %s (%s): %s>" %
100            (getAttrWay(obj, "code"), getAttrWay(obj, "name"), 
101            getAttrWay(obj, "message")))
102
103ID = 1
104def _gen_id():
105    """ Generates an ID number for each YAML-RPC procedure call. """ 
106    global ID
107    ID = ID + 1
108    return ID
109
110class YAMLTransport:
111    """ This handles an HTTP transaction to an YAML-RPC server."""
112
113    # The client identifier (may be overridden).
114    user_agent = "yamlrpclib.py/%s (by Peter Murphy)" % __version__
115
116    def __init__(self, safe_loader = True):
117        """ The initializer. There is one parameter for this function:
118           
119            safe_loader: the default is True: only allows "simple" YAML data to
120            be loaded - sequences, dictionaries and most immutable types. If
121            this parameter is False, then arbitrary YAML data can be loaded.
122            This can be a security risk.
123           
124            *** Only set this parameter to False with trusted servers! ***
125        """
126        self.loadfunc(safe_loader);
127
128    def loadfunc(self, safe_loader):
129        """ Sets the load function. """
130        self.safe_loader = safe_loader;
131        if self.safe_loader:
132            self._loadfunc = yaml.safe_load;
133        else:
134            self._loadfunc = yaml.load;
135
136    def request(self, host, handler, request_body, verbose=0):
137        """ This sends a complete request, and parses the response. The
138            parameters are:
139           
140            host: the trget host.
141            handler: the target RPC handler.
142            request_body: a YAML-RPC request body.
143            verbose: the debugging flag.
144           
145            The routine returns the parsed response.
146        """
147        h = self.make_connection(host)
148        if verbose:
149            h.set_debuglevel(1)
150
151        self.send_request(h, handler, request_body)
152        self.send_host(h, host)
153        self.send_user_agent(h)
154        self.send_content(h, request_body)
155
156        errcode, errmsg, headers = h.getreply()
157
158        if errcode != 200:
159            raise YRPCProtocolException(host + handler, errcode, errmsg,
160                headers);
161        self.verbose = verbose
162
163        try:
164            sock = h._conn.sock
165        except AttributeError:
166            sock = None
167
168        return self._parse_response(h.getfile(), sock)
169
170    def get_host_info(self, host):
171        """ This gets authorization info from the host parameter. This may
172            be a string, or a (host, x509-dict) tuple. If it is a string,
173            it is checked for a "user:pw@host" format, and a "Basic
174            Authentication" header is added if appropriate.
175   
176            The function returns a 3-tuple of the form:
177           
178            (actual host, extra headers, x509 info). 
179           
180            The header and x509 fields may be None.
181        """
182
183        x509 = {}
184        if isinstance(host, types.TupleType):
185            host, x509 = host
186
187        auth, host = urllib.splituser(host)
188
189        if auth:
190            auth = base64.encodestring(urllib.unquote(auth))
191            auth = string.join(string.split(auth), "") 
192            # Done to get rid of whitespace
193            extra_headers = [("Authorization", "Basic " + auth)]
194        else:
195            extra_headers = None
196
197        return host, extra_headers, x509
198
199    def make_connection(self, host):
200        """ This creates a HTTP connection object from a host descriptor.
201       
202            We know "host" is the target host. This returns a connection
203            handle.
204        """
205        host, extra_headers, x509 = self.get_host_info(host)
206        return httplib.HTTP(host)
207
208    def send_request(self, connection, handler, request_body):
209        """ This sends a request header. The parameters are:
210       
211            connection: the connection handle.
212            handler: the target RPC handler.
213            request_body: the YAML-RPC body.
214        """
215        connection.putrequest("POST", handler)
216
217    def send_host(self, connection, host):
218        """ Sends by host name. Parameters:
219       
220            connection: connection handle.
221            host: host name.
222        """
223        host, extra_headers, x509 = self.get_host_info(host)
224        connection.putheader("Host", host)
225        if extra_headers:
226            if isinstance(extra_headers, DictType):
227                extra_headers = extra_headers.items()
228            for key, value in extra_headers:
229                connection.putheader(key, value)
230
231    def send_user_agent(self, connection):
232        """ This sends the user-agent identifier. The sole parameter is
233            connection: the connection handle.
234        """
235        connection.putheader("User-Agent", self.user_agent)
236
237    def send_content(self, connection, request_body):
238        """ This sends the request body. There are two parameters:
239       
240            connection: the connection handle.
241            request_body: the YAML-RPC request body.
242        """
243        connection.putheader("Content-Type", "text/yaml")
244        connection.putheader("Content-Length", str(len(request_body)))
245        connection.endheaders()
246        if request_body:
247            connection.send(request_body)
248
249    def parse_response(self, file):
250        """ This parses the response from a YAML-RPC server. The sole parameter
251            is file (a stream representing the input socket).
252           
253            This function returns the YAML data loaded as an object or a dict.
254
255            Note: this is provided as a compatibility interface.
256        """
257        return self._parse_response(file, None)
258
259    def _parse_response(self, file, sock):
260        """ This is the alternative interface for parsing a response. It is
261            like the parse_response method, but also provides direct access to
262            the underlying socket object (where available). Parameters:
263           
264            file: a socket as a file stream.
265           
266            sock: the socket handle (or None, if the socket object could not
267            be accessed).
268
269            This function returns the YAML data loaded as an object or a dict.
270        """
271        fedin = "";
272        while 1:
273            if sock:
274                response = sock.recv(1024);
275            else:
276                response = file.read(1024);
277            if not response:
278                break;
279            if self.verbose:
280                print "body: ", repr(response);
281            fedin = fedin + response;
282        file.close();
283        return self._loadfunc(fedin);
284
285
286class SecureYAMLTransport(YAMLTransport):
287    """ This handles an HTTPS transaction to an YAML-RPC server.
288   
289        Warning: this is untested! The code is based on SafeTransport from
290        jsonrpclib.py.
291    """
292
293    def __init__(self, safe_loader = True):
294        """ The initializer. There is one parameter for this function:
295           
296            safe_loader: the default is True: only allows "simple" YAML data to
297            be loaded - sequences, dictionaries and most immutable types. If
298            this parameter is False, then arbitrary YAML data can be loaded.
299            This can be a security risk.
300           
301            *** Only set this parameter to False with trusted servers! ***
302        """
303        YAMLTransport.__init__(self, safe_loader);
304
305    def make_connection(self, host):
306        """ This creates a HTTPS connection object from a host descriptor.
307       
308            host may be a string, or a (host, x509-dict) tuple.
309        """
310        host, extra_headers, x509 = self.get_host_info(host)
311        try:
312            HTTPS = httplib.HTTPS
313        except AttributeError:
314            raise NotImplementedError(
315                "your version of httplib doesn't support HTTPS"
316                )
317        else:
318            return HTTPS(host, None, **(x509 or {}))
319
320
321class YAMLServerProxy(object):
322    """ This acts as a proxy for calling YAML-RPC methods on a remote server,
323        in the same way that ServerProxy allows one to access XML-RPC methods
324        on a remote server. For example:
325
326        s = YAMLServerProxy("http://www.example.com:8000", verbose = 1)
327        c = s.add(1, 2);
328       
329        Attempts to calculate the "add" function on www.example.com through port
330        8000.
331       
332        (The expected behavior is that the variable c becomes 3. If there is a
333        problem, then an exception may be thrown. See the __init__ function.
334    """
335    def __init__(self, uri, transport=None, verbose=False, safe_loader = True,
336            json_compat = True, throwexcep = True):
337        """ This initializes this class as a proxy for a remote server. The
338            parameters are:
339           
340            uri: the connection point on the server using the format.
341           
342            scheme://host:pathno/target
343           
344            (In this implementation, only http and https are supported as
345            schemes.)
346           
347            transport: An instance of YAMLTransport or YAMLSecureTranspot.
348           
349            verbose: True only if this instance should print a lot of
350            information to the screen. False otherwise.
351           
352            safe_loader: the default is True: only allows "simple" YAML data to
353            be loaded - sequences, dictionaries and most immutable types. If
354            this parameter is False, then arbitrary YAML data can be loaded.
355            This can be a security risk. (Only set this parameter to False with
356            trusted servers!)
357           
358            Note: if the transport parameter is not none, then it is modified
359            to use the same safe_load settings as this class.
360           
361            json_compat: if True, all complex objects are sent to the YAML-RPC
362            server as dictionaries; no special tagging printed out. If False,
363            YAML-RPC requests are delivered with the tag '!yamlrpccall'. The
364            default is True.
365           
366            throwexcep: if True (the default), throw an exception if there is
367            a processing error on the remove server.
368           
369            There is no encoding declaration: it is assumed that all data is
370            encoded as UTF-8.
371        """
372
373        utype, uri = urllib.splittype(uri)
374        if utype not in ("http", "https"):
375            raise IOError, "Unsupported YAMLRPC protocol"
376        self.__host, self.__handler = urllib.splithost(uri)
377        if not self.__handler:
378            self.__handler = "/RPC2"
379        if transport is None:
380            if utype == "https":
381                self.__transport = SecureYAMLTransport(safe_loader);
382            else:
383                self.__transport = YAMLTransport(safe_loader);
384        else:
385            self.__transport = transport;
386            transport.safe_loader = safe_loader;
387        self.__verbose = verbose;
388        self.json_compat = json_compat;
389        self.throwexcep = throwexcep;
390
391    def __request(self, methodname, params):
392        """ This calls a method on the remote server. The arguments:
393           
394            methodname: the name of the method. May contain dots.
395           
396            params: the parameters as a list or a tuple.
397           
398            Returns the result if successful, or None if not. May throw
399            an exception.
400        """
401        requestobj = yamlrpcbasic.YAMLRPCCall(_gen_id(), protvers, 
402            methodname, params);
403        if self.json_compat:
404            request = yaml.dump(requestobj.__dict__, default_flow_style=True);
405        else:
406            request = yaml.dump(requestobj, default_flow_style=True);
407        response = self.__transport.request(self.__host, self.__handler, 
408            request, self.__verbose);
409        errorobj = getAttrWay(response, "error");
410        if self.throwexcep and errorobj != None:
411            raise YRPCErrorException(errorobj);
412        return getAttrWay(response, "result");
413
414    def __repr__(self):
415        return ("<YAMLServerProxy for %s%s>" % (self.__host, self.__handler))
416
417    __str__ = __repr__;
418
419    def __getattr__(self, name):
420        return _Method(self.__request, name)
421
422    # note: to call a remote object with an non-standard name, use
423    # result getattr(server, "strange-python-name")(args)
424
425
426if __name__ == "__main__":
427    s = YAMLServerProxy("http://localhost:8002", None, False, False);
428    c = s.add(1, 2);
429    print c;
430    c = s.system.listMethods();
431    print c;
432    c = s.system.methodSignature("add");
433    print c;
434    c = s.system.methodHelp("pow");
435    print c;
436    c = s.system.methodHelp("add");
437    print c;
438   
439# The following code should cause errors. So we trap them.   
440   
441    try:
442        c = s.bad("other");
443        print c;
444    except YRPCErrorException, yrpc:
445        print yrpc;
446    try:
447        e = s.echo("foo bar", "baz")
448        print e
449    except YRPCErrorException, yrpc:
450        print yrpc;
451    try:
452        f = s.echo(5)
453        print f
454    except YRPCErrorException, yrpc:
455        print yrpc;
456