PageRenderTime 53ms CodeModel.GetById 1ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 0ms

/tests/web/jsonrpclib.py

https://bitbucket.org/prologic/circuits/
Python | 454 lines | 319 code | 56 blank | 79 comment | 9 complexity | caa317051c53564764f7608a3de1d17a MD5 | raw file
  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    """Base class for client errors."""
 72    def __str__(self):
 73        return repr(self)
 74
 75##
 76# Indicates an HTTP-level protocol error.  This is raised by the HTTP
 77# transport layer, if the server returns an error code other than 200
 78# (OK).
 79#
 80# @param url The target URL.
 81# @param errcode The HTTP error code.
 82# @param errmsg The HTTP error message.
 83# @param headers The HTTP header dictionary.
 84
 85
 86class ProtocolError(Error):
 87    """Indicates an HTTP protocol error."""
 88
 89    def __init__(self, url, errcode, errmsg, headers, response):
 90        Error.__init__(self)
 91        self.url = url
 92        self.errcode = errcode
 93        self.errmsg = errmsg
 94        self.headers = headers
 95        self.response = response
 96
 97    def __repr__(self):
 98        return (
 99            "<ProtocolError for %s: %s %s>" %
100            (self.url, self.errcode, self.errmsg)
101        )
102
103
104def getparser(encoding):
105    un = Unmarshaller(encoding)
106    par = Parser(un)
107    return par, un
108
109
110def dumps(params, methodname=None, methodresponse=None, encoding=None,
111          allow_none=0):
112    if methodname:
113        request = {}
114        request["method"] = methodname
115        request["params"] = params
116        request["id"] = _gen_id()
117        return json.dumps(request)
118
119
120class Unmarshaller(object):
121
122    def __init__(self, encoding):
123        self.data = None
124        self.encoding = encoding
125
126    def feed(self, data):
127        if self.data is None:
128            self.data = data
129        else:
130            self.data = self.data + data
131
132    def close(self):
133        #try to convert string to json
134        return json.loads(self.data.decode(self.encoding))
135
136
137class Parser(object):
138
139    def __init__(self, unmarshaller):
140        self._target = unmarshaller
141        self.data = None
142
143    def feed(self, data):
144        if self.data is None:
145            self.data = data
146        else:
147            self.data = self.data + data
148
149    def close(self):
150        self._target.feed(self.data)
151
152
153class _Method(object):
154    # some magic to bind an JSON-RPC method to an RPC server.
155    # supports "nested" methods (e.g. examples.getStateName)
156
157    def __init__(self, send, name):
158        self.__send = send
159        self.__name = name
160
161    def __getattr__(self, name):
162        return _Method(self.__send, "%s.%s" % (self.__name, name))
163
164    def __call__(self, *args):
165        return self.__send(self.__name, args)
166
167##
168# Standard transport class for JSON-RPC over HTTP.
169# <p>
170# You can create custom transports by subclassing this method, and
171# overriding selected methods.
172
173
174class Transport:
175    """Handles an HTTP transaction to an JSON-RPC server."""
176
177    # client identifier (may be overridden)
178    user_agent = "jsonlib.py/%s (by matt harrison)" % __version__
179
180    ##
181    # Send a complete request, and parse the response.
182    #
183    # @param host Target host.
184    # @param handler Target PRC handler.
185    # @param request_body JSON-RPC request body.
186    # @param verbose Debugging flag.
187    # @return Parsed response.
188
189    def request(self, host, handler, request_body, encoding, verbose=0):
190        # issue JSON-RPC request
191
192        h = self.make_connection(host)
193        if verbose:
194            h.set_debuglevel(1)
195
196        self.send_request(h, handler, request_body)
197        if not PY3:
198            self.send_host(h, host)
199        self.send_user_agent(h)
200        self.send_content(h, request_body)
201
202        try:
203            errcode, errmsg, headers = h.getreply()
204            r = h.getfile()
205        except AttributeError:
206            r = h.getresponse()
207            errcode = r.status
208            errmsg = r.reason
209            headers = r.getheaders()
210
211        if errcode != 200:
212            response = r.read()
213            raise ProtocolError(
214                host + handler,
215                errcode, errmsg,
216                headers,
217                response
218            )
219
220        self.verbose = verbose
221
222        try:
223            sock = h._conn.sock
224        except AttributeError:
225            sock = None
226
227        return self._parse_response(r, sock, encoding)
228
229    ##
230    # Create parser.
231    #
232    # @return A 2-tuple containing a parser and a unmarshaller.
233
234    def getparser(self, encoding):
235        # get parser and unmarshaller
236        return getparser(encoding)
237
238    ##
239    # Get authorization info from host parameter
240    # Host may be a string, or a (host, x509-dict) tuple; if a string,
241    # it is checked for a "user:pw@host" format, and a "Basic
242    # Authentication" header is added if appropriate.
243    #
244    # @param host Host descriptor (URL or (URL, x509 info) tuple).
245    # @return A 3-tuple containing (actual host, extra headers,
246    #     x509 info).  The header and x509 fields may be None.
247
248    def get_host_info(self, host):
249
250        x509 = {}
251        if isinstance(host, tuple):
252            host, x509 = host
253
254        auth, host = splituser(host)
255
256        if auth:
257            auth = base64.encodestring(unquote(auth))
258            auth = "".join(auth.split())  # get rid of whitespace
259            extra_headers = [
260                ("Authorization", "Basic " + auth)
261            ]
262        else:
263            extra_headers = None
264
265        return host, extra_headers, x509
266
267    ##
268    # Connect to server.
269    #
270    # @param host Target host.
271    # @return A connection handle.
272
273    def make_connection(self, host):
274        # create a HTTP connection object from a host descriptor
275        host, extra_headers, x509 = self.get_host_info(host)
276        return HTTPConnection(host)
277
278    ##
279    # Send request header.
280    #
281    # @param connection Connection handle.
282    # @param handler Target RPC handler.
283    # @param request_body JSON-RPC body.
284
285    def send_request(self, connection, handler, request_body):
286        connection.putrequest("POST", handler)
287
288    ##
289    # Send host name.
290    #
291    # @param connection Connection handle.
292    # @param host Host name.
293
294    def send_host(self, connection, host):
295        host, extra_headers, x509 = self.get_host_info(host)
296        connection.putheader("Host", host)
297        if extra_headers:
298            if isinstance(extra_headers, dict):
299                extra_headers = list(extra_headers.items())
300            for key, value in extra_headers:
301                connection.putheader(key, value)
302
303    ##
304    # Send user-agent identifier.
305    #
306    # @param connection Connection handle.
307
308    def send_user_agent(self, connection):
309        connection.putheader("User-Agent", self.user_agent)
310
311    ##
312    # Send request body.
313    #
314    # @param connection Connection handle.
315    # @param request_body JSON-RPC request body.
316
317    def send_content(self, connection, request_body):
318        connection.putheader("Content-Type", "text/xml")
319        connection.putheader("Content-Length", str(len(request_body)))
320        connection.endheaders()
321        if request_body:
322            connection.send(request_body)
323
324    ##
325    # Parse response.
326    #
327    # @param file Stream.
328    # @return Response tuple and target method.
329
330    def parse_response(self, file):
331        # compatibility interface
332        return self._parse_response(file, None)
333
334    ##
335    # Parse response (alternate interface).  This is similar to the
336    # parse_response method, but also provides direct access to the
337    # underlying socket object (where available).
338    #
339    # @param file Stream.
340    # @param sock Socket handle (or None, if the socket object
341    #    could not be accessed).
342    # @return Response tuple and target method.
343
344    def _parse_response(self, file, sock, encoding):
345        # read response from input file/socket, and parse it
346
347        p, u = self.getparser(encoding)
348
349        while 1:
350            if sock:
351                response = sock.recv(1024)
352            else:
353                response = file.read(1024)
354            if not response:
355                break
356            if self.verbose:
357                print("body:", repr(response))
358            p.feed(response)
359
360        file.close()
361        p.close()
362
363        return u.close()
364
365##
366# Standard transport class for JSON-RPC over HTTPS.
367
368
369class SafeTransport(Transport):
370    """Handles an HTTPS transaction to an JSON-RPC server."""
371
372    # FIXME: mostly untested
373
374    def make_connection(self, host):
375        # create a HTTPS connection object from a host descriptor
376        # host may be a string, or a (host, x509-dict) tuple
377        host, extra_headers, x509 = self.get_host_info(host)
378        try:
379            HTTPS = HTTPSConnection
380        except AttributeError:
381            raise NotImplementedError(
382                "your version of httplib doesn't support HTTPS"
383            )
384        else:
385            return HTTPS(host, None, **(x509 or {}))
386
387
388class ServerProxy(object):
389
390    def __init__(self, uri, transport=None, encoding=None,
391                 verbose=None, allow_none=0):
392        utype, uri = splittype(uri)
393        if utype not in ("http", "https"):
394            raise IOError("Unsupported JSONRPC protocol")
395        self.__host, self.__handler = splithost(uri)
396        if not self.__handler:
397            self.__handler = "/RPC2"
398
399        if transport is None:
400            if utype == "https":
401                transport = SafeTransport()
402            else:
403                transport = Transport()
404        self.__transport = transport
405
406        self.__encoding = encoding
407        self.__verbose = verbose
408        self.__allow_none = allow_none
409
410    def __request(self, methodname, params):
411        """call a method on the remote server
412        """
413
414        request = dumps(params, methodname, encoding=self.__encoding,
415                        allow_none=self.__allow_none)
416
417        response = self.__transport.request(
418            self.__host,
419            self.__handler,
420            request.encode(self.__encoding),
421            self.__encoding,
422            verbose=self.__verbose
423        )
424
425        if len(response) == 1:
426            response = response[0]
427
428        return response
429
430    def __repr__(self):
431        return ("<JSONProxy for %s%s>" %
432                (self.__host, self.__handler)
433                )
434
435    __str__ = __repr__
436
437    def __getattr__(self, name):
438        #dispatch
439        return _Method(self.__request, name)
440
441    # note: to call a remote object with an non-standard name, use
442    # result getattr(server, "strange-python-name")(args)
443
444
445if __name__ == "__main__":
446    s = ServerProxy("http://localhost:8080/foo/", verbose=1)
447    c = s.echo("foo bar")
448    print(c)
449    d = s.bad("other")
450    print(d)
451    e = s.echo("foo bar", "baz")
452    print(e)
453    f = s.echo(5)
454    print(f)