1# a port of xmlrpclib to json....
2#
3#
4# The JSON-RPC client interface is based on the XML-RPC client
5#
6# Copyright (c) 1999-2002 by Secret Labs AB
7# Copyright (c) 1999-2002 by Fredrik Lundh
8# Copyright (c) 2006 by Matt Harrison
9#
10# By obtaining, using, and/or copying this software and/or its
11# associated documentation, you agree that you have read, understood,
12# and will comply with the following terms and conditions:
13#
14# Permission to use, copy, modify, and distribute this software and
15# its associated documentation for any purpose and without fee is
16# hereby granted, provided that the above copyright notice appears in
17# all copies, and that both that copyright notice and this permission
18# notice appear in supporting documentation, and that the name of
19# Secret Labs AB or the author not be used in advertising or publicity
20# pertaining to distribution of the software without specific, written
21# prior permission.
22#
23# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
24# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
25# ABILITY AND FITNESS.  IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
26# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
27# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
28# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
29# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
30# OF THIS SOFTWARE.
31# --------------------------------------------------------------------
32
33import sys
34import json
35import base64
36
37PY3 = sys.version_info[0] == 3
38
39try:
40    from http.client import HTTPConnection
41    from http.client import HTTPSConnection
42except ImportError:
43    from httplib import HTTP as HTTPConnection  # NOQA
44    from httplib import HTTPS as HTTPSConnection  # NOQA
45
46try:
47    from urllib.parse import unquote
48    from urllib.parse import splithost, splittype, splituser
49except ImportError:
50    from urllib import unquote  # NOQA
51    from urllib import splithost, splittype, splituser  # NOQA
52
53__version__ = "0.0.1"
54
55ID = 1
56
57
58def _gen_id():
59    global ID
60    ID = ID + 1
61    return ID
62
63
64# --------------------------------------------------------------------
65# Exceptions
66
67##
68# Base class for all kinds of client-side errors.
69
70class Error(Exception):
71
72    """Base class for client errors."""
73
74    def __str__(self):
75        return repr(self)
76
77##
78# Indicates an HTTP-level protocol error.  This is raised by the HTTP
79# transport layer, if the server returns an error code other than 200
80# (OK).
81#
82# @param url The target URL.
83# @param errcode The HTTP error code.
84# @param errmsg The HTTP error message.
85# @param headers The HTTP header dictionary.
86
87
88class ProtocolError(Error):
89
90    """Indicates an HTTP protocol error."""
91
92    def __init__(self, url, errcode, errmsg, headers, response):
93        Error.__init__(self)
94        self.url = url
95        self.errcode = errcode
96        self.errmsg = errmsg
97        self.headers = headers
98        self.response = response
99
100    def __repr__(self):
101        return (
102            "<ProtocolError for %s: %s %s>" %
103            (self.url, self.errcode, self.errmsg)
104        )
105
106
107def getparser(encoding):
108    un = Unmarshaller(encoding)
109    par = Parser(un)
110    return par, un
111
112
113def dumps(params, methodname=None, methodresponse=None, encoding=None,
114          allow_none=0):
115    if methodname:
116        request = {}
117        request["method"] = methodname
118        request["params"] = params
119        request["id"] = _gen_id()
120        return json.dumps(request)
121
122
123class Unmarshaller(object):
124
125    def __init__(self, encoding):
126        self.data = None
127        self.encoding = encoding
128
129    def feed(self, data):
130        if self.data is None:
131            self.data = data
132        else:
133            self.data = self.data + data
134
135    def close(self):
136        # try to convert string to json
137        return json.loads(self.data.decode(self.encoding))
138
139
140class Parser(object):
141
142    def __init__(self, unmarshaller):
143        self._target = unmarshaller
144        self.data = None
145
146    def feed(self, data):
147        if self.data is None:
148            self.data = data
149        else:
150            self.data = self.data + data
151
152    def close(self):
153        self._target.feed(self.data)
154
155
156class _Method(object):
157    # some magic to bind an JSON-RPC method to an RPC server.
158    # supports "nested" methods (e.g. examples.getStateName)
159
160    def __init__(self, send, name):
161        self.__send = send
162        self.__name = name
163
164    def __getattr__(self, name):
165        return _Method(self.__send, "%s.%s" % (self.__name, name))
166
167    def __call__(self, *args):
168        return self.__send(self.__name, args)
169
170##
171# Standard transport class for JSON-RPC over HTTP.
172# <p>
173# You can create custom transports by subclassing this method, and
174# overriding selected methods.
175
176
177class Transport:
178
179    """Handles an HTTP transaction to an JSON-RPC server."""
180
181    # client identifier (may be overridden)
182    user_agent = "jsonlib.py/%s (by matt harrison)" % __version__
183
184    ##
185    # Send a complete request, and parse the response.
186    #
187    # @param host Target host.
188    # @param handler Target PRC handler.
189    # @param request_body JSON-RPC request body.
190    # @param verbose Debugging flag.
191    # @return Parsed response.
192
193    def request(self, host, handler, request_body, encoding, verbose=0):
194        # issue JSON-RPC request
195
196        h = self.make_connection(host)
197        if verbose:
198            h.set_debuglevel(1)
199
200        self.send_request(h, handler, request_body)
201        if not PY3:
202            self.send_host(h, host)
203        self.send_user_agent(h)
204        self.send_content(h, request_body)
205
206        try:
207            errcode, errmsg, headers = h.getreply()
208            r = h.getfile()
209        except AttributeError:
210            r = h.getresponse()
211            errcode = r.status
212            errmsg = r.reason
213            headers = r.getheaders()
214
215        if errcode != 200:
216            response = r.read()
217            raise ProtocolError(
218                host + handler,
219                errcode, errmsg,
220                headers,
221                response
222            )
223
224        self.verbose = verbose
225
226        try:
227            sock = h._conn.sock
228        except AttributeError:
229            sock = None
230
231        return self._parse_response(r, sock, encoding)
232
233    ##
234    # Create parser.
235    #
236    # @return A 2-tuple containing a parser and a unmarshaller.
237
238    def getparser(self, encoding):
239        # get parser and unmarshaller
240        return getparser(encoding)
241
242    ##
243    # Get authorization info from host parameter
244    # Host may be a string, or a (host, x509-dict) tuple; if a string,
245    # it is checked for a "user:pw@host" format, and a "Basic
246    # Authentication" header is added if appropriate.
247    #
248    # @param host Host descriptor (URL or (URL, x509 info) tuple).
249    # @return A 3-tuple containing (actual host, extra headers,
250    #     x509 info).  The header and x509 fields may be None.
251
252    def get_host_info(self, host):
253
254        x509 = {}
255        if isinstance(host, tuple):
256            host, x509 = host
257
258        auth, host = splituser(host)
259
260        if auth:
261            auth = base64.encodestring(unquote(auth))
262            auth = "".join(auth.split())  # get rid of whitespace
263            extra_headers = [
264                ("Authorization", "Basic " + auth)
265            ]
266        else:
267            extra_headers = None
268
269        return host, extra_headers, x509
270
271    ##
272    # Connect to server.
273    #
274    # @param host Target host.
275    # @return A connection handle.
276
277    def make_connection(self, host):
278        # create a HTTP connection object from a host descriptor
279        host, extra_headers, x509 = self.get_host_info(host)
280        return HTTPConnection(host)
281
282    ##
283    # Send request header.
284    #
285    # @param connection Connection handle.
286    # @param handler Target RPC handler.
287    # @param request_body JSON-RPC body.
288
289    def send_request(self, connection, handler, request_body):
290        connection.putrequest("POST", handler)
291
292    ##
293    # Send host name.
294    #
295    # @param connection Connection handle.
296    # @param host Host name.
297
298    def send_host(self, connection, host):
299        host, extra_headers, x509 = self.get_host_info(host)
300        connection.putheader("Host", host)
301        if extra_headers:
302            if isinstance(extra_headers, dict):
303                extra_headers = list(extra_headers.items())
304            for key, value in extra_headers:
305                connection.putheader(key, value)
306
307    ##
308    # Send user-agent identifier.
309    #
310    # @param connection Connection handle.
311
312    def send_user_agent(self, connection):
313        connection.putheader("User-Agent", self.user_agent)
314
315    ##
316    # Send request body.
317    #
318    # @param connection Connection handle.
319    # @param request_body JSON-RPC request body.
320
321    def send_content(self, connection, request_body):
322        connection.putheader("Content-Type", "text/xml")
323        connection.putheader("Content-Length", str(len(request_body)))
324        connection.endheaders()
325        if request_body:
326            connection.send(request_body)
327
328    ##
329    # Parse response.
330    #
331    # @param file Stream.
332    # @return Response tuple and target method.
333
334    def parse_response(self, file):
335        # compatibility interface
336        return self._parse_response(file, None)
337
338    ##
339    # Parse response (alternate interface).  This is similar to the
340    # parse_response method, but also provides direct access to the
341    # underlying socket object (where available).
342    #
343    # @param file Stream.
344    # @param sock Socket handle (or None, if the socket object
345    #    could not be accessed).
346    # @return Response tuple and target method.
347
348    def _parse_response(self, file, sock, encoding):
349        # read response from input file/socket, and parse it
350
351        p, u = self.getparser(encoding)
352
353        while 1:
354            if sock:
355                response = sock.recv(1024)
356            else:
357                response = file.read(1024)
358            if not response:
359                break
360            if self.verbose:
361                print("body:", repr(response))
362            p.feed(response)
363
364        file.close()
365        p.close()
366
367        return u.close()
368
369##
370# Standard transport class for JSON-RPC over HTTPS.
371
372
373class SafeTransport(Transport):
374
375    """Handles an HTTPS transaction to an JSON-RPC server."""
376
377    # FIXME: mostly untested
378
379    def make_connection(self, host):
380        # create a HTTPS connection object from a host descriptor
381        # host may be a string, or a (host, x509-dict) tuple
382        host, extra_headers, x509 = self.get_host_info(host)
383        try:
384            HTTPS = HTTPSConnection
385        except AttributeError:
386            raise NotImplementedError(
387                "your version of httplib doesn't support HTTPS"
388            )
389        else:
390            return HTTPS(host, None, **(x509 or {}))
391
392
393class ServerProxy(object):
394
395    def __init__(self, uri, transport=None, encoding=None,
396                 verbose=None, allow_none=0):
397        utype, uri = splittype(uri)
398        if utype not in ("http", "https"):
399            raise IOError("Unsupported JSONRPC protocol")
400        self.__host, self.__handler = splithost(uri)
401        if not self.__handler:
402            self.__handler = "/RPC2"
403
404        if transport is None:
405            if utype == "https":
406                transport = SafeTransport()
407            else:
408                transport = Transport()
409        self.__transport = transport
410
411        self.__encoding = encoding
412        self.__verbose = verbose
413        self.__allow_none = allow_none
414
415    def __request(self, methodname, params):
416        """call a method on the remote server
417        """
418
419        request = dumps(params, methodname, encoding=self.__encoding,
420                        allow_none=self.__allow_none)
421
422        response = self.__transport.request(
423            self.__host,
424            self.__handler,
425            request.encode(self.__encoding),
426            self.__encoding,
427            verbose=self.__verbose
428        )
429
430        if len(response) == 1:
431            response = response[0]
432
433        return response
434
435    def __repr__(self):
436        return ("<JSONProxy for %s%s>" %
437                (self.__host, self.__handler)
438                )
439
440    __str__ = __repr__
441
442    def __getattr__(self, name):
443        # dispatch
444        return _Method(self.__request, name)
445
446    # note: to call a remote object with an non-standard name, use
447    # result getattr(server, "strange-python-name")(args)
448
449
450if __name__ == "__main__":
451    s = ServerProxy("http://localhost:8080/foo/", verbose=1)
452    c = s.echo("foo bar")
453    print(c)
454    d = s.bad("other")
455    print(d)
456    e = s.echo("foo bar", "baz")
457    print(e)
458    f = s.echo(5)
459    print(f)
460