diff --git a/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/PKG-INFO b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/PKG-INFO new file mode 100644 index 000000000..035a98f8c --- /dev/null +++ b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/PKG-INFO @@ -0,0 +1,89 @@ +Metadata-Version: 1.1 +Name: backports.ssl-match-hostname +Version: 3.7.0.1 +Summary: The ssl.match_hostname() function from Python 3.5 +Home-page: http://bitbucket.org/brandon/backports.ssl_match_hostname +Author: Toshio Kuratomi +Author-email: toshio@fedoraproject.org +License: Python Software Foundation License +Description: + The ssl.match_hostname() function from Python 3.7 + ================================================= + + The Secure Sockets Layer is only actually *secure* + if you check the hostname in the certificate returned + by the server to which you are connecting, + and verify that it matches to hostname + that you are trying to reach. + + But the matching logic, defined in `RFC2818`_, + can be a bit tricky to implement on your own. + So the ``ssl`` package in the Standard Library of Python 3.2 + and greater now includes a ``match_hostname()`` function + for performing this check instead of requiring every application + to implement the check separately. + + This backport brings ``match_hostname()`` to users + of earlier versions of Python. + Simply make this distribution a dependency of your package, + and then use it like this:: + + from backports.ssl_match_hostname import match_hostname, CertificateError + [...] + sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv23, + cert_reqs=ssl.CERT_REQUIRED, ca_certs=...) + try: + match_hostname(sslsock.getpeercert(), hostname) + except CertificateError, ce: + ... + + Brandon Craig Rhodes is merely the packager of this distribution; + the actual code inside comes from Python 3.7 with small changes for + portability. + + + Requirements + ------------ + + * If you need to use this on Python versions earlier than 2.6 you will need to + install the `ssl module`_. From Python 2.6 upwards ``ssl`` is included in + the Python Standard Library so you do not need to install it separately. + + .. _`ssl module`:: https://pypi.python.org/pypi/ssl + + History + ------- + + * This function was introduced in python-3.2 + * It was updated for python-3.4a1 for a CVE + (backports-ssl_match_hostname-3.4.0.1) + * It was updated from RFC2818 to RFC 6125 compliance in order to fix another + security flaw for python-3.3.3 and python-3.4a5 + (backports-ssl_match_hostname-3.4.0.2) + * It was updated in python-3.5 to handle IPAddresses in ServerAltName fields + (something that backports.ssl_match_hostname will do if you also install the + ipaddress library from pypi). + * It was updated in python-3.7 to handle IPAddresses without the ipaddress library and dropped + support for partial wildcards + + .. _`ipaddress module`:: https://pypi.python.org/pypi/ipaddress + + .. _RFC2818: http://tools.ietf.org/html/rfc2818.html + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Programming Language :: Python :: 2.4 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.0 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Topic :: Security :: Cryptography diff --git a/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/SOURCES.txt b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/SOURCES.txt new file mode 100644 index 000000000..95b961609 --- /dev/null +++ b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README.txt +setup.cfg +backports/__init__.py +backports.ssl_match_hostname.egg-info/PKG-INFO +backports.ssl_match_hostname.egg-info/SOURCES.txt +backports.ssl_match_hostname.egg-info/dependency_links.txt +backports.ssl_match_hostname.egg-info/top_level.txt +backports/ssl_match_hostname/__init__.py \ No newline at end of file diff --git a/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/dependency_links.txt b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/installed-files.txt b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/installed-files.txt new file mode 100644 index 000000000..fd1aae367 --- /dev/null +++ b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/installed-files.txt @@ -0,0 +1,8 @@ +../backports/__init__.py +../backports/__init__.pyc +../backports/ssl_match_hostname/__init__.py +../backports/ssl_match_hostname/__init__.pyc +PKG-INFO +SOURCES.txt +dependency_links.txt +top_level.txt diff --git a/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/top_level.txt b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/top_level.txt new file mode 100644 index 000000000..99d2be5b6 --- /dev/null +++ b/backports.ssl_match_hostname-3.7.0.1-py2.7.egg-info/top_level.txt @@ -0,0 +1 @@ +backports diff --git a/backports/__init__.py b/backports/__init__.py new file mode 100644 index 000000000..612d32836 --- /dev/null +++ b/backports/__init__.py @@ -0,0 +1,3 @@ +# This is a Python "namespace package" http://www.python.org/dev/peps/pep-0382/ +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) diff --git a/backports/ssl_match_hostname/__init__.py b/backports/ssl_match_hostname/__init__.py new file mode 100644 index 000000000..cdfef013d --- /dev/null +++ b/backports/ssl_match_hostname/__init__.py @@ -0,0 +1,204 @@ +"""The match_hostname() function from Python 3.7.0, essential when using SSL.""" + +import sys +import socket as _socket + +try: + # Divergence: Python-3.7+'s _ssl has this exception type but older Pythons do not + from _ssl import SSLCertVerificationError + CertificateError = SSLCertVerificationError +except: + class CertificateError(ValueError): + pass + + +__version__ = '3.7.0.1' + + +# Divergence: Added to deal with ipaddess as bytes on python2 +def _to_text(obj): + if isinstance(obj, str) and sys.version_info < (3,): + obj = unicode(obj, encoding='ascii', errors='strict') + elif sys.version_info >= (3,) and isinstance(obj, bytes): + obj = str(obj, encoding='ascii', errors='strict') + return obj + + +def _to_bytes(obj): + if isinstance(obj, str) and sys.version_info >= (3,): + obj = bytes(obj, encoding='ascii', errors='strict') + elif sys.version_info < (3,) and isinstance(obj, unicode): + obj = obj.encode('ascii', 'strict') + return obj + + +def _dnsname_match(dn, hostname): + """Matching according to RFC 6125, section 6.4.3 + + - Hostnames are compared lower case. + - For IDNA, both dn and hostname must be encoded as IDN A-label (ACE). + - Partial wildcards like 'www*.example.org', multiple wildcards, sole + wildcard or wildcards in labels other then the left-most label are not + supported and a CertificateError is raised. + - A wildcard must match at least one character. + """ + if not dn: + return False + + wildcards = dn.count('*') + # speed up common case w/o wildcards + if not wildcards: + return dn.lower() == hostname.lower() + + if wildcards > 1: + # Divergence .format() to percent formatting for Python < 2.6 + raise CertificateError( + "too many wildcards in certificate DNS name: %s" % repr(dn)) + + dn_leftmost, sep, dn_remainder = dn.partition('.') + + if '*' in dn_remainder: + # Only match wildcard in leftmost segment. + # Divergence .format() to percent formatting for Python < 2.6 + raise CertificateError( + "wildcard can only be present in the leftmost label: " + "%s." % repr(dn)) + + if not sep: + # no right side + # Divergence .format() to percent formatting for Python < 2.6 + raise CertificateError( + "sole wildcard without additional labels are not support: " + "%s." % repr(dn)) + + if dn_leftmost != '*': + # no partial wildcard matching + # Divergence .format() to percent formatting for Python < 2.6 + raise CertificateError( + "partial wildcards in leftmost label are not supported: " + "%s." % repr(dn)) + + hostname_leftmost, sep, hostname_remainder = hostname.partition('.') + if not hostname_leftmost or not sep: + # wildcard must match at least one char + return False + return dn_remainder.lower() == hostname_remainder.lower() + + +def _inet_paton(ipname): + """Try to convert an IP address to packed binary form + + Supports IPv4 addresses on all platforms and IPv6 on platforms with IPv6 + support. + """ + # inet_aton() also accepts strings like '1' + # Divergence: We make sure we have native string type for all python versions + try: + b_ipname = _to_bytes(ipname) + except UnicodeError: + raise ValueError("%s must be an all-ascii string." % repr(ipname)) + + # Set ipname in native string format + if sys.version_info < (3,): + n_ipname = b_ipname + else: + n_ipname = ipname + + if n_ipname.count('.') == 3: + try: + return _socket.inet_aton(n_ipname) + # Divergence: OSError on late python3. socket.error earlier. + # Null bytes generate ValueError on python3(we want to raise + # ValueError anyway), TypeError # earlier + except (OSError, _socket.error, TypeError): + pass + + try: + return _socket.inet_pton(_socket.AF_INET6, n_ipname) + # Divergence: OSError on late python3. socket.error earlier. + # Null bytes generate ValueError on python3(we want to raise + # ValueError anyway), TypeError # earlier + except (OSError, _socket.error, TypeError): + # Divergence .format() to percent formatting for Python < 2.6 + raise ValueError("%s is neither an IPv4 nor an IP6 " + "address." % repr(ipname)) + except AttributeError: + # AF_INET6 not available + pass + + # Divergence .format() to percent formatting for Python < 2.6 + raise ValueError("%s is not an IPv4 address." % repr(ipname)) + + +def _ipaddress_match(ipname, host_ip): + """Exact matching of IP addresses. + + RFC 6125 explicitly doesn't define an algorithm for this + (section 1.7.2 - "Out of Scope"). + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + ip = _inet_paton(ipname.rstrip()) + return ip == host_ip + + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed. + + The function matches IP addresses rather than dNSNames if hostname is a + valid ipaddress string. IPv4 addresses are supported on all platforms. + IPv6 addresses are supported on platforms with IPv6 support (AF_INET6 + and inet_pton). + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED") + try: + # Divergence: Deal with hostname as bytes + host_ip = _inet_paton(_to_text(hostname)) + except ValueError: + # Not an IP address (common case) + host_ip = None + except UnicodeError: + # Divergence: Deal with hostname as byte strings. + # IP addresses should be all ascii, so we consider it not + # an IP address if this fails + host_ip = None + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == 'IP Address': + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + if not dnsnames: + # The subject is only checked when there is no dNSName entry + # in subjectAltName + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") diff --git a/bin/wsdump.py b/bin/wsdump.py new file mode 100755 index 000000000..246d15050 --- /dev/null +++ b/bin/wsdump.py @@ -0,0 +1,201 @@ +#!/usr/local/bin/python + +import argparse +import code +import sys +import threading +import time +import ssl + +import six +from six.moves.urllib.parse import urlparse + +import websocket + +try: + import readline +except ImportError: + pass + + +def get_encoding(): + encoding = getattr(sys.stdin, "encoding", "") + if not encoding: + return "utf-8" + else: + return encoding.lower() + + +OPCODE_DATA = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) +ENCODING = get_encoding() + + +class VAction(argparse.Action): + + def __call__(self, parser, args, values, option_string=None): + if values is None: + values = "1" + try: + values = int(values) + except ValueError: + values = values.count("v") + 1 + setattr(args, self.dest, values) + + +def parse_args(): + parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool") + parser.add_argument("url", metavar="ws_url", + help="websocket url. ex. ws://echo.websocket.org/") + parser.add_argument("-p", "--proxy", + help="proxy url. ex. http://127.0.0.1:8080") + parser.add_argument("-v", "--verbose", default=0, nargs='?', action=VAction, + dest="verbose", + help="set verbose mode. If set to 1, show opcode. " + "If set to 2, enable to trace websocket module") + parser.add_argument("-n", "--nocert", action='store_true', + help="Ignore invalid SSL cert") + parser.add_argument("-r", "--raw", action="store_true", + help="raw output") + parser.add_argument("-s", "--subprotocols", nargs='*', + help="Set subprotocols") + parser.add_argument("-o", "--origin", + help="Set origin") + parser.add_argument("--eof-wait", default=0, type=int, + help="wait time(second) after 'EOF' received.") + parser.add_argument("-t", "--text", + help="Send initial text") + parser.add_argument("--timings", action="store_true", + help="Print timings in seconds") + parser.add_argument("--headers", + help="Set custom headers. Use ',' as separator") + + return parser.parse_args() + + +class RawInput: + + def raw_input(self, prompt): + if six.PY3: + line = input(prompt) + else: + line = raw_input(prompt) + + if ENCODING and ENCODING != "utf-8" and not isinstance(line, six.text_type): + line = line.decode(ENCODING).encode("utf-8") + elif isinstance(line, six.text_type): + line = line.encode("utf-8") + + return line + + +class InteractiveConsole(RawInput, code.InteractiveConsole): + + def write(self, data): + sys.stdout.write("\033[2K\033[E") + # sys.stdout.write("\n") + sys.stdout.write("\033[34m< " + data + "\033[39m") + sys.stdout.write("\n> ") + sys.stdout.flush() + + def read(self): + return self.raw_input("> ") + + +class NonInteractive(RawInput): + + def write(self, data): + sys.stdout.write(data) + sys.stdout.write("\n") + sys.stdout.flush() + + def read(self): + return self.raw_input("") + + +def main(): + start_time = time.time() + args = parse_args() + if args.verbose > 1: + websocket.enableTrace(True) + options = {} + if args.proxy: + p = urlparse(args.proxy) + options["http_proxy_host"] = p.hostname + options["http_proxy_port"] = p.port + if args.origin: + options["origin"] = args.origin + if args.subprotocols: + options["subprotocols"] = args.subprotocols + opts = {} + if args.nocert: + opts = {"cert_reqs": ssl.CERT_NONE, "check_hostname": False} + if args.headers: + options['header'] = map(str.strip, args.headers.split(',')) + ws = websocket.create_connection(args.url, sslopt=opts, **options) + if args.raw: + console = NonInteractive() + else: + console = InteractiveConsole() + print("Press Ctrl+C to quit") + + def recv(): + try: + frame = ws.recv_frame() + except websocket.WebSocketException: + return websocket.ABNF.OPCODE_CLOSE, None + if not frame: + raise websocket.WebSocketException("Not a valid frame %s" % frame) + elif frame.opcode in OPCODE_DATA: + return frame.opcode, frame.data + elif frame.opcode == websocket.ABNF.OPCODE_CLOSE: + ws.send_close() + return frame.opcode, None + elif frame.opcode == websocket.ABNF.OPCODE_PING: + ws.pong(frame.data) + return frame.opcode, frame.data + + return frame.opcode, frame.data + + def recv_ws(): + while True: + opcode, data = recv() + msg = None + if six.PY3 and opcode == websocket.ABNF.OPCODE_TEXT and isinstance(data, bytes): + data = str(data, "utf-8") + if not args.verbose and opcode in OPCODE_DATA: + msg = data + elif args.verbose: + msg = "%s: %s" % (websocket.ABNF.OPCODE_MAP.get(opcode), data) + + if msg is not None: + if args.timings: + console.write(str(time.time() - start_time) + ": " + msg) + else: + console.write(msg) + + if opcode == websocket.ABNF.OPCODE_CLOSE: + break + + thread = threading.Thread(target=recv_ws) + thread.daemon = True + thread.start() + + if args.text: + ws.send(args.text) + + while True: + try: + message = console.read() + ws.send(message) + except KeyboardInterrupt: + return + except EOFError: + time.sleep(args.eof_wait) + return + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(e) diff --git a/json_rpc-1.12.1-py2.7.egg-info/PKG-INFO b/json_rpc-1.12.1-py2.7.egg-info/PKG-INFO new file mode 100644 index 000000000..c185d0b73 --- /dev/null +++ b/json_rpc-1.12.1-py2.7.egg-info/PKG-INFO @@ -0,0 +1,187 @@ +Metadata-Version: 1.1 +Name: json-rpc +Version: 1.12.1 +Summary: JSON-RPC transport implementation +Home-page: https://github.com/pavlov99/json-rpc +Author: Kirill Pavlov +Author-email: k@p99.io +License: MIT +Description: json-rpc + ======== + + .. image:: https://circleci.com/gh/pavlov99/json-rpc/tree/master.svg?style=svg + :target: https://circleci.com/gh/pavlov99/json-rpc/tree/master + :alt: Build Status + + .. image:: https://codecov.io/gh/pavlov99/json-rpc/branch/master/graph/badge.svg + :target: https://codecov.io/gh/pavlov99/json-rpc + :alt: Coverage Status + + .. image:: https://readthedocs.org/projects/json-rpc/badge/?version=latest + :target: http://json-rpc.readthedocs.io/en/latest/?badge=latest + + .. image:: https://img.shields.io/pypi/v/json-rpc.svg + :target: https://pypi.org/project/json-rpc/ + :alt: Latest PyPI version + + .. image:: https://img.shields.io/pypi/pyversions/json-rpc.svg + :target: https://pypi.org/project/json-rpc/ + :alt: Supported Python versions + + .. image:: https://badges.gitter.im/pavlov99/json-rpc.svg + :target: https://gitter.im/pavlov99/json-rpc + :alt: Gitter + + + .. image:: https://opencollective.com/json-rpc/tiers/backer/badge.svg?label=backer&color=brightgreen + :target: https://opencollective.com/json-rpc + :alt: Bakers + + .. image:: https://opencollective.com/json-rpc/tiers/backer/badge.svg?label=sponsor&color=brightgreen + :target: https://opencollective.com/json-rpc + :alt: Sponsors + + `JSON-RPC2.0 `_ and `JSON-RPC1.0 `_ transport specification implementation. + Supports Python 2.6+, Python 3.3+, PyPy. Has optional Django and Flask support. 200+ tests. + + Features + -------- + + This implementation does not have any transport functionality realization, only protocol. + Any client or server implementation is easy based on current code, but requires transport libraries, such as requests, gevent or zmq, see `examples `_. + + - Vanilla Python, no dependencies. + - 200+ tests for multiple edge cases. + - Optional backend support for Django, Flask. + - json-rpc 1.1 and 2.0 support. + + Install + ------- + + .. code-block:: python + + pip install json-rpc + + Tests + ----- + + Quickstart + ^^^^^^^^^^ + This is an essential part of the library as there are a lot of edge cases in JSON-RPC standard. To manage a variety of supported python versions as well as optional backends json-rpc uses `tox`: + + .. code-block:: bash + + tox + + .. TIP:: + During local development use your python version with tox runner. For example, if your are using Python 3.6 run `tox -e py36`. It is easier to develop functionality for specific version first and then expands it to all of the supported versions. + + Continuous integration + ^^^^^^^^^^^^^^^^^^^^^^ + This project uses `CircleCI `_ for continuous integration. All of the python supported versions are managed via `tox.ini` and `.circleci/config.yml` files. Master branch test status is displayed on the badge in the beginning of this document. + + Test matrix + ^^^^^^^^^^^ + json-rpc supports multiple python versions: 2.6+, 3.3+, pypy. This introduces difficulties with testing libraries and optional dependencies management. For example, python before version 3.3 does not support `mock` and there is a limited support for `unittest2`. Every dependency translates into *if-then* blocks in the source code and adds complexity to it. Hence, while cross-python support is a core feature of this library, cross-Django or cross-Flask support is limited. In general, json-rpc uses latest stable release which supports current python version. For example, python 2.6 is compatible with Django 1.6 and not compatible with any future versions. + + Below is a testing matrix: + + +--------+-------+-----------+--------+--------+ + | Python | mock | unittest | Django | Flask | + +========+=======+===========+========+========+ + | 2.6 | 2.0.0 | unittest2 | 1.6 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | 2.7 | 2.0.0 | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | 3.3 | | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | 3.4 | | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | 3.5 | | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | 3.6 | | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | pypy | 2.0.0 | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + | pypy3 | | | 1.11 | 0.12.2 | + +--------+-------+-----------+--------+--------+ + + Quickstart + ---------- + Server (uses `Werkzeug `_) + + .. code-block:: python + + from werkzeug.wrappers import Request, Response + from werkzeug.serving import run_simple + + from jsonrpc import JSONRPCResponseManager, dispatcher + + + @dispatcher.add_method + def foobar(**kwargs): + return kwargs["foo"] + kwargs["bar"] + + + @Request.application + def application(request): + # Dispatcher is dictionary {: callable} + dispatcher["echo"] = lambda s: s + dispatcher["add"] = lambda a, b: a + b + + response = JSONRPCResponseManager.handle( + request.data, dispatcher) + return Response(response.json, mimetype='application/json') + + + if __name__ == '__main__': + run_simple('localhost', 4000, application) + + Client (uses `requests `_) + + .. code-block:: python + + import requests + import json + + + def main(): + url = "http://localhost:4000/jsonrpc" + headers = {'content-type': 'application/json'} + + # Example echo method + payload = { + "method": "echo", + "params": ["echome!"], + "jsonrpc": "2.0", + "id": 0, + } + response = requests.post( + url, data=json.dumps(payload), headers=headers).json() + + assert response["result"] == "echome!" + assert response["jsonrpc"] + assert response["id"] == 0 + + if __name__ == "__main__": + main() + + Competitors + ----------- + There are `several libraries `_ implementing JSON-RPC protocol. List below represents python libraries, none of the supports python3. tinyrpc looks better than others. + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/json_rpc-1.12.1-py2.7.egg-info/SOURCES.txt b/json_rpc-1.12.1-py2.7.egg-info/SOURCES.txt new file mode 100644 index 000000000..87d35af62 --- /dev/null +++ b/json_rpc-1.12.1-py2.7.egg-info/SOURCES.txt @@ -0,0 +1,42 @@ +LICENSE.txt +MANIFEST.in +README.rst +get-pip.py +setup.cfg +setup.py +json_rpc.egg-info/PKG-INFO +json_rpc.egg-info/SOURCES.txt +json_rpc.egg-info/dependency_links.txt +json_rpc.egg-info/top_level.txt +jsonrpc/__init__.py +jsonrpc/base.py +jsonrpc/dispatcher.py +jsonrpc/exceptions.py +jsonrpc/jsonrpc.py +jsonrpc/jsonrpc1.py +jsonrpc/jsonrpc2.py +jsonrpc/manager.py +jsonrpc/six.py +jsonrpc/utils.py +jsonrpc/backend/__init__.py +jsonrpc/backend/django.py +jsonrpc/backend/flask.py +jsonrpc/tests/__init__.py +jsonrpc/tests/py35_utils.py +jsonrpc/tests/test_base.py +jsonrpc/tests/test_bug29.py +jsonrpc/tests/test_dispatcher.py +jsonrpc/tests/test_examples20.py +jsonrpc/tests/test_jsonrpc.py +jsonrpc/tests/test_jsonrpc1.py +jsonrpc/tests/test_jsonrpc2.py +jsonrpc/tests/test_jsonrpc_errors.py +jsonrpc/tests/test_manager.py +jsonrpc/tests/test_pep3107.py +jsonrpc/tests/test_utils.py +jsonrpc/tests/test_backend_django/__init__.py +jsonrpc/tests/test_backend_django/settings.py +jsonrpc/tests/test_backend_django/tests.py +jsonrpc/tests/test_backend_django/urls.py +jsonrpc/tests/test_backend_flask/__init__.py +jsonrpc/tests/test_backend_flask/tests.py \ No newline at end of file diff --git a/json_rpc-1.12.1-py2.7.egg-info/dependency_links.txt b/json_rpc-1.12.1-py2.7.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/json_rpc-1.12.1-py2.7.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/json_rpc-1.12.1-py2.7.egg-info/installed-files.txt b/json_rpc-1.12.1-py2.7.egg-info/installed-files.txt new file mode 100644 index 000000000..e0a921633 --- /dev/null +++ b/json_rpc-1.12.1-py2.7.egg-info/installed-files.txt @@ -0,0 +1,68 @@ +../jsonrpc/__init__.py +../jsonrpc/__init__.pyc +../jsonrpc/backend/__init__.py +../jsonrpc/backend/__init__.pyc +../jsonrpc/backend/django.py +../jsonrpc/backend/django.pyc +../jsonrpc/backend/flask.py +../jsonrpc/backend/flask.pyc +../jsonrpc/base.py +../jsonrpc/base.pyc +../jsonrpc/dispatcher.py +../jsonrpc/dispatcher.pyc +../jsonrpc/exceptions.py +../jsonrpc/exceptions.pyc +../jsonrpc/jsonrpc.py +../jsonrpc/jsonrpc.pyc +../jsonrpc/jsonrpc1.py +../jsonrpc/jsonrpc1.pyc +../jsonrpc/jsonrpc2.py +../jsonrpc/jsonrpc2.pyc +../jsonrpc/manager.py +../jsonrpc/manager.pyc +../jsonrpc/six.py +../jsonrpc/six.pyc +../jsonrpc/tests/__init__.py +../jsonrpc/tests/__init__.pyc +../jsonrpc/tests/py35_utils.py +../jsonrpc/tests/py35_utils.pyc +../jsonrpc/tests/test_backend_django/__init__.py +../jsonrpc/tests/test_backend_django/__init__.pyc +../jsonrpc/tests/test_backend_django/settings.py +../jsonrpc/tests/test_backend_django/settings.pyc +../jsonrpc/tests/test_backend_django/tests.py +../jsonrpc/tests/test_backend_django/tests.pyc +../jsonrpc/tests/test_backend_django/urls.py +../jsonrpc/tests/test_backend_django/urls.pyc +../jsonrpc/tests/test_backend_flask/__init__.py +../jsonrpc/tests/test_backend_flask/__init__.pyc +../jsonrpc/tests/test_backend_flask/tests.py +../jsonrpc/tests/test_backend_flask/tests.pyc +../jsonrpc/tests/test_base.py +../jsonrpc/tests/test_base.pyc +../jsonrpc/tests/test_bug29.py +../jsonrpc/tests/test_bug29.pyc +../jsonrpc/tests/test_dispatcher.py +../jsonrpc/tests/test_dispatcher.pyc +../jsonrpc/tests/test_examples20.py +../jsonrpc/tests/test_examples20.pyc +../jsonrpc/tests/test_jsonrpc.py +../jsonrpc/tests/test_jsonrpc.pyc +../jsonrpc/tests/test_jsonrpc1.py +../jsonrpc/tests/test_jsonrpc1.pyc +../jsonrpc/tests/test_jsonrpc2.py +../jsonrpc/tests/test_jsonrpc2.pyc +../jsonrpc/tests/test_jsonrpc_errors.py +../jsonrpc/tests/test_jsonrpc_errors.pyc +../jsonrpc/tests/test_manager.py +../jsonrpc/tests/test_manager.pyc +../jsonrpc/tests/test_pep3107.py +../jsonrpc/tests/test_pep3107.pyc +../jsonrpc/tests/test_utils.py +../jsonrpc/tests/test_utils.pyc +../jsonrpc/utils.py +../jsonrpc/utils.pyc +PKG-INFO +SOURCES.txt +dependency_links.txt +top_level.txt diff --git a/json_rpc-1.12.1-py2.7.egg-info/top_level.txt b/json_rpc-1.12.1-py2.7.egg-info/top_level.txt new file mode 100644 index 000000000..1d0ccdd1d --- /dev/null +++ b/json_rpc-1.12.1-py2.7.egg-info/top_level.txt @@ -0,0 +1 @@ +jsonrpc diff --git a/jsonrpc/__init__.py b/jsonrpc/__init__.py new file mode 100644 index 000000000..a4095b933 --- /dev/null +++ b/jsonrpc/__init__.py @@ -0,0 +1,11 @@ +from .manager import JSONRPCResponseManager +from .dispatcher import Dispatcher + +__version = (1, 12, 1) + +__version__ = version = '.'.join(map(str, __version)) +__project__ = PROJECT = __name__ + +dispatcher = Dispatcher() + +# lint_ignore=W0611,W0401 diff --git a/jsonrpc/backend/__init__.py b/jsonrpc/backend/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jsonrpc/backend/django.py b/jsonrpc/backend/django.py new file mode 100644 index 000000000..5357fb003 --- /dev/null +++ b/jsonrpc/backend/django.py @@ -0,0 +1,89 @@ +from __future__ import absolute_import + +from django.views.decorators.csrf import csrf_exempt +from django.conf.urls import url +from django.conf import settings +from django.http import HttpResponse, HttpResponseNotAllowed +import copy +import json +import logging +import time + +from ..exceptions import JSONRPCInvalidRequestException +from ..jsonrpc import JSONRPCRequest +from ..manager import JSONRPCResponseManager +from ..utils import DatetimeDecimalEncoder +from ..dispatcher import Dispatcher + + +logger = logging.getLogger(__name__) + + +def response_serialize(obj): + """ Serializes response's data object to JSON. """ + return json.dumps(obj, cls=DatetimeDecimalEncoder) + + +class JSONRPCAPI(object): + def __init__(self, dispatcher=None): + self.dispatcher = dispatcher if dispatcher is not None \ + else Dispatcher() + + @property + def urls(self): + urls = [ + url(r'^$', self.jsonrpc, name='endpoint'), + ] + + if getattr(settings, 'JSONRPC_MAP_VIEW_ENABLED', settings.DEBUG): + urls.append( + url(r'^map$', self.jsonrpc_map, name='map') + ) + + return urls + + @csrf_exempt + def jsonrpc(self, request): + """ JSON-RPC 2.0 handler.""" + if request.method != "POST": + return HttpResponseNotAllowed(["POST"]) + + request_str = request.body.decode('utf8') + try: + jsonrpc_request = JSONRPCRequest.from_json(request_str) + except (TypeError, ValueError, JSONRPCInvalidRequestException): + response = JSONRPCResponseManager.handle( + request_str, self.dispatcher) + else: + jsonrpc_request.params = jsonrpc_request.params or {} + jsonrpc_request_params = copy.copy(jsonrpc_request.params) + if isinstance(jsonrpc_request.params, dict): + jsonrpc_request.params.update(request=request) + + t1 = time.time() + response = JSONRPCResponseManager.handle_request( + jsonrpc_request, self.dispatcher) + t2 = time.time() + logger.info('{0}({1}) {2:.2f} sec'.format( + jsonrpc_request.method, jsonrpc_request_params, t2 - t1)) + + if response: + response.serialize = response_serialize + response = response.json + + return HttpResponse(response, content_type="application/json") + + def jsonrpc_map(self, request): + """ Map of json-rpc available calls. + + :return str: + + """ + result = "

JSON-RPC map

{0}
".format("\n\n".join([ + "{0}: {1}".format(fname, f.__doc__) + for fname, f in self.dispatcher.items() + ])) + return HttpResponse(result) + + +api = JSONRPCAPI() diff --git a/jsonrpc/backend/flask.py b/jsonrpc/backend/flask.py new file mode 100644 index 000000000..133752ceb --- /dev/null +++ b/jsonrpc/backend/flask.py @@ -0,0 +1,85 @@ +from __future__ import absolute_import + +import copy +import json +import logging +import time +from uuid import uuid4 + +from flask import Blueprint, request, Response + +from ..exceptions import JSONRPCInvalidRequestException +from ..jsonrpc import JSONRPCRequest +from ..manager import JSONRPCResponseManager +from ..utils import DatetimeDecimalEncoder +from ..dispatcher import Dispatcher + + +logger = logging.getLogger(__name__) + + +class JSONRPCAPI(object): + def __init__(self, dispatcher=None, check_content_type=True): + """ + + :param dispatcher: methods dispatcher + :param check_content_type: if True - content-type must be + "application/json" + :return: + + """ + self.dispatcher = dispatcher if dispatcher is not None \ + else Dispatcher() + self.check_content_type = check_content_type + + def as_blueprint(self, name=None): + blueprint = Blueprint(name if name else str(uuid4()), __name__) + blueprint.add_url_rule( + '/', view_func=self.jsonrpc, methods=['POST']) + blueprint.add_url_rule( + '/map', view_func=self.jsonrpc_map, methods=['GET']) + return blueprint + + def as_view(self): + return self.jsonrpc + + def jsonrpc(self): + request_str = self._get_request_str() + try: + jsonrpc_request = JSONRPCRequest.from_json(request_str) + except (TypeError, ValueError, JSONRPCInvalidRequestException): + response = JSONRPCResponseManager.handle( + request_str, self.dispatcher) + else: + response = JSONRPCResponseManager.handle_request( + jsonrpc_request, self.dispatcher) + + if response: + response.serialize = self._serialize + response = response.json + + return Response(response, content_type="application/json") + + def jsonrpc_map(self): + """ Map of json-rpc available calls. + + :return str: + + """ + result = "

JSON-RPC map

{0}
".format("\n\n".join([ + "{0}: {1}".format(fname, f.__doc__) + for fname, f in self.dispatcher.items() + ])) + return Response(result) + + def _get_request_str(self): + if self.check_content_type or request.data: + return request.data + return list(request.form.keys())[0] + + @staticmethod + def _serialize(s): + return json.dumps(s, cls=DatetimeDecimalEncoder) + + +api = JSONRPCAPI() diff --git a/jsonrpc/base.py b/jsonrpc/base.py new file mode 100644 index 000000000..f9d419673 --- /dev/null +++ b/jsonrpc/base.py @@ -0,0 +1,87 @@ +from .utils import JSONSerializable + + +class JSONRPCBaseRequest(JSONSerializable): + + """ Base class for JSON-RPC 1.0 and JSON-RPC 2.0 requests.""" + + def __init__(self, method=None, params=None, _id=None, + is_notification=None): + self.data = dict() + self.method = method + self.params = params + self._id = _id + self.is_notification = is_notification + + @property + def data(self): + return self._data + + @data.setter + def data(self, value): + if not isinstance(value, dict): + raise ValueError("data should be dict") + + self._data = value + + @property + def args(self): + """ Method position arguments. + + :return tuple args: method position arguments. + + """ + return tuple(self.params) if isinstance(self.params, list) else () + + @property + def kwargs(self): + """ Method named arguments. + + :return dict kwargs: method named arguments. + + """ + return self.params if isinstance(self.params, dict) else {} + + @property + def json(self): + return self.serialize(self.data) + + +class JSONRPCBaseResponse(JSONSerializable): + + """ Base class for JSON-RPC 1.0 and JSON-RPC 2.0 responses.""" + + def __init__(self, **kwargs): + self.data = dict() + + try: + self.result = kwargs['result'] + except KeyError: + pass + + try: + self.error = kwargs['error'] + except KeyError: + pass + + self._id = kwargs.get('_id') + + if 'result' not in kwargs and 'error' not in kwargs: + raise ValueError("Either result or error should be used") + + self.request = None # type: JSONRPCBaseRequest + + @property + def data(self): + return self._data + + @data.setter + def data(self, value): + if not isinstance(value, dict): + raise ValueError("data should be dict") + + self._data = value + + @property + def json(self): + return self.serialize(self.data) diff --git a/jsonrpc/dispatcher.py b/jsonrpc/dispatcher.py new file mode 100644 index 000000000..94e7635e9 --- /dev/null +++ b/jsonrpc/dispatcher.py @@ -0,0 +1,132 @@ +""" Dispatcher is used to add methods (functions) to the server. + +For usage examples see :meth:`Dispatcher.add_method` + +""" +import functools +import collections + + +class Dispatcher(collections.MutableMapping): + + """ Dictionary like object which maps method_name to method.""" + + def __init__(self, prototype=None): + """ Build method dispatcher. + + Parameters + ---------- + prototype : object or dict, optional + Initial method mapping. + + Examples + -------- + + Init object with method dictionary. + + >>> Dispatcher({"sum": lambda a, b: a + b}) + None + + """ + self.method_map = dict() + + if prototype is not None: + self.build_method_map(prototype) + + def __getitem__(self, key): + return self.method_map[key] + + def __setitem__(self, key, value): + self.method_map[key] = value + + def __delitem__(self, key): + del self.method_map[key] + + def __len__(self): + return len(self.method_map) + + def __iter__(self): + return iter(self.method_map) + + def __repr__(self): + return repr(self.method_map) + + def add_class(self, cls): + prefix = cls.__name__.lower() + '.' + self.build_method_map(cls(), prefix) + + def add_object(self, obj): + prefix = obj.__class__.__name__.lower() + '.' + self.build_method_map(obj, prefix) + + def add_dict(self, dict, prefix=''): + if prefix: + prefix += '.' + self.build_method_map(dict, prefix) + + def add_method(self, f=None, name=None): + """ Add a method to the dispatcher. + + Parameters + ---------- + f : callable + Callable to be added. + name : str, optional + Name to register (the default is function **f** name) + + Notes + ----- + When used as a decorator keeps callable object unmodified. + + Examples + -------- + + Use as method + + >>> d = Dispatcher() + >>> d.add_method(lambda a, b: a + b, name="sum") + > + + Or use as decorator + + >>> d = Dispatcher() + >>> @d.add_method + def mymethod(*args, **kwargs): + print(args, kwargs) + + Or use as a decorator with a different function name + >>> d = Dispatcher() + >>> @d.add_method(name="my.method") + def mymethod(*args, **kwargs): + print(args, kwargs) + + """ + if name and not f: + return functools.partial(self.add_method, name=name) + + self.method_map[name or f.__name__] = f + return f + + def build_method_map(self, prototype, prefix=''): + """ Add prototype methods to the dispatcher. + + Parameters + ---------- + prototype : object or dict + Initial method mapping. + If given prototype is a dictionary then all callable objects will + be added to dispatcher. + If given prototype is an object then all public methods will + be used. + prefix: string, optional + Prefix of methods + + """ + if not isinstance(prototype, dict): + prototype = dict((method, getattr(prototype, method)) + for method in dir(prototype) + if not method.startswith('_')) + + for attr, method in prototype.items(): + if callable(method): + self[prefix + attr] = method diff --git a/jsonrpc/exceptions.py b/jsonrpc/exceptions.py new file mode 100644 index 000000000..175ba9248 --- /dev/null +++ b/jsonrpc/exceptions.py @@ -0,0 +1,185 @@ +""" JSON-RPC Exceptions.""" +from . import six +import json + + +class JSONRPCError(object): + + """ Error for JSON-RPC communication. + + When a rpc call encounters an error, the Response Object MUST contain the + error member with a value that is a Object with the following members: + + Parameters + ---------- + code: int + A Number that indicates the error type that occurred. + This MUST be an integer. + The error codes from and including -32768 to -32000 are reserved for + pre-defined errors. Any code within this range, but not defined + explicitly below is reserved for future use. The error codes are nearly + the same as those suggested for XML-RPC at the following + url: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + + message: str + A String providing a short description of the error. + The message SHOULD be limited to a concise single sentence. + + data: int or str or dict or list, optional + A Primitive or Structured value that contains additional + information about the error. + This may be omitted. + The value of this member is defined by the Server (e.g. detailed error + information, nested errors etc.). + + """ + + serialize = staticmethod(json.dumps) + deserialize = staticmethod(json.loads) + + def __init__(self, code=None, message=None, data=None): + self._data = dict() + self.code = getattr(self.__class__, "CODE", code) + self.message = getattr(self.__class__, "MESSAGE", message) + self.data = data + + def __get_code(self): + return self._data["code"] + + def __set_code(self, value): + if not isinstance(value, six.integer_types): + raise ValueError("Error code should be integer") + + self._data["code"] = value + + code = property(__get_code, __set_code) + + def __get_message(self): + return self._data["message"] + + def __set_message(self, value): + if not isinstance(value, six.string_types): + raise ValueError("Error message should be string") + + self._data["message"] = value + + message = property(__get_message, __set_message) + + def __get_data(self): + return self._data.get("data") + + def __set_data(self, value): + if value is not None: + self._data["data"] = value + + data = property(__get_data, __set_data) + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls( + code=data["code"], message=data["message"], data=data.get("data")) + + @property + def json(self): + return self.serialize(self._data) + + +class JSONRPCParseError(JSONRPCError): + + """ Parse Error. + + Invalid JSON was received by the server. + An error occurred on the server while parsing the JSON text. + + """ + + CODE = -32700 + MESSAGE = "Parse error" + + +class JSONRPCInvalidRequest(JSONRPCError): + + """ Invalid Request. + + The JSON sent is not a valid Request object. + + """ + + CODE = -32600 + MESSAGE = "Invalid Request" + + +class JSONRPCMethodNotFound(JSONRPCError): + + """ Method not found. + + The method does not exist / is not available. + + """ + + CODE = -32601 + MESSAGE = "Method not found" + + +class JSONRPCInvalidParams(JSONRPCError): + + """ Invalid params. + + Invalid method parameter(s). + + """ + + CODE = -32602 + MESSAGE = "Invalid params" + + +class JSONRPCInternalError(JSONRPCError): + + """ Internal error. + + Internal JSON-RPC error. + + """ + + CODE = -32603 + MESSAGE = "Internal error" + + +class JSONRPCServerError(JSONRPCError): + + """ Server error. + + Reserved for implementation-defined server-errors. + + """ + + CODE = -32000 + MESSAGE = "Server error" + + +class JSONRPCException(Exception): + + """ JSON-RPC Exception.""" + + pass + + +class JSONRPCInvalidRequestException(JSONRPCException): + + """ Request is not valid.""" + + pass + + +class JSONRPCDispatchException(JSONRPCException): + + """ JSON-RPC Dispatch Exception. + + Should be thrown in dispatch methods. + + """ + + def __init__(self, code=None, message=None, data=None, *args, **kwargs): + super(JSONRPCDispatchException, self).__init__(args, kwargs) + self.error = JSONRPCError(code=code, data=data, message=message) diff --git a/jsonrpc/jsonrpc.py b/jsonrpc/jsonrpc.py new file mode 100644 index 000000000..f02126e2f --- /dev/null +++ b/jsonrpc/jsonrpc.py @@ -0,0 +1,28 @@ +""" JSON-RPC wrappers for version 1.0 and 2.0. + +Objects diring init operation try to choose JSON-RPC 2.0 and in case of error +JSON-RPC 1.0. +from_json methods could decide what format is it by presence of 'jsonrpc' +attribute. + +""" +from .utils import JSONSerializable +from .jsonrpc1 import JSONRPC10Request +from .jsonrpc2 import JSONRPC20Request + + +class JSONRPCRequest(JSONSerializable): + + """ JSONRPC Request.""" + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls.from_data(data) + + @classmethod + def from_data(cls, data): + if isinstance(data, dict) and "jsonrpc" not in data: + return JSONRPC10Request.from_data(data) + else: + return JSONRPC20Request.from_data(data) diff --git a/jsonrpc/jsonrpc1.py b/jsonrpc/jsonrpc1.py new file mode 100644 index 000000000..3fcbe9bf6 --- /dev/null +++ b/jsonrpc/jsonrpc1.py @@ -0,0 +1,151 @@ +from . import six + +from .base import JSONRPCBaseRequest, JSONRPCBaseResponse +from .exceptions import JSONRPCInvalidRequestException, JSONRPCError + + +class JSONRPC10Request(JSONRPCBaseRequest): + + """ JSON-RPC 1.0 Request. + + A remote method is invoked by sending a request to a remote service. + The request is a single object serialized using json. + + :param str method: The name of the method to be invoked. + :param list params: An Array of objects to pass as arguments to the method. + :param _id: This can be of any type. It is used to match the response with + the request that it is replying to. + :param bool is_notification: whether request notification or not. + + """ + + JSONRPC_VERSION = "1.0" + REQUIRED_FIELDS = set(["method", "params", "id"]) + POSSIBLE_FIELDS = set(["method", "params", "id"]) + + @property + def data(self): + data = dict((k, v) for k, v in self._data.items()) + data["id"] = None if self.is_notification else data["id"] + return data + + @data.setter + def data(self, value): + if not isinstance(value, dict): + raise ValueError("data should be dict") + + self._data = value + + @property + def method(self): + return self._data.get("method") + + @method.setter + def method(self, value): + if not isinstance(value, six.string_types): + raise ValueError("Method should be string") + + self._data["method"] = str(value) + + @property + def params(self): + return self._data.get("params") + + @params.setter + def params(self, value): + if not isinstance(value, (list, tuple)): + raise ValueError("Incorrect params {0}".format(value)) + + self._data["params"] = list(value) + + @property + def _id(self): + return self._data.get("id") + + @_id.setter + def _id(self, value): + self._data["id"] = value + + @property + def is_notification(self): + return self._data["id"] is None or self._is_notification + + @is_notification.setter + def is_notification(self, value): + if value is None: + value = self._id is None + + if self._id is None and not value: + raise ValueError("Can not set attribute is_notification. " + + "Request id should not be None") + + self._is_notification = value + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls.from_data(data) + + @classmethod + def from_data(cls, data): + if not isinstance(data, dict): + raise ValueError("data should be dict") + + if cls.REQUIRED_FIELDS <= set(data.keys()) <= cls.POSSIBLE_FIELDS: + return cls( + method=data["method"], params=data["params"], _id=data["id"] + ) + else: + extra = set(data.keys()) - cls.POSSIBLE_FIELDS + missed = cls.REQUIRED_FIELDS - set(data.keys()) + msg = "Invalid request. Extra fields: {0}, Missed fields: {1}" + raise JSONRPCInvalidRequestException(msg.format(extra, missed)) + + +class JSONRPC10Response(JSONRPCBaseResponse): + + JSONRPC_VERSION = "1.0" + + @property + def data(self): + data = dict((k, v) for k, v in self._data.items()) + return data + + @data.setter + def data(self, value): + if not isinstance(value, dict): + raise ValueError("data should be dict") + + self._data = value + + @property + def result(self): + return self._data.get("result") + + @result.setter + def result(self, value): + if self.error: + raise ValueError("Either result or error should be used") + self._data["result"] = value + + @property + def error(self): + return self._data.get("error") + + @error.setter + def error(self, value): + self._data.pop('value', None) + if value: + self._data["error"] = value + # Test error + JSONRPCError(**value) + + @property + def _id(self): + return self._data.get("id") + + @_id.setter + def _id(self, value): + if value is None: + raise ValueError("id could not be null for JSON-RPC1.0 Response") + self._data["id"] = value diff --git a/jsonrpc/jsonrpc2.py b/jsonrpc/jsonrpc2.py new file mode 100644 index 000000000..66ca45125 --- /dev/null +++ b/jsonrpc/jsonrpc2.py @@ -0,0 +1,267 @@ +from . import six +import json + +from .exceptions import JSONRPCError, JSONRPCInvalidRequestException +from .base import JSONRPCBaseRequest, JSONRPCBaseResponse + + +class JSONRPC20Request(JSONRPCBaseRequest): + + """ A rpc call is represented by sending a Request object to a Server. + + :param str method: A String containing the name of the method to be + invoked. Method names that begin with the word rpc followed by a + period character (U+002E or ASCII 46) are reserved for rpc-internal + methods and extensions and MUST NOT be used for anything else. + + :param params: A Structured value that holds the parameter values to be + used during the invocation of the method. This member MAY be omitted. + :type params: iterable or dict + + :param _id: An identifier established by the Client that MUST contain a + String, Number, or NULL value if included. If it is not included it is + assumed to be a notification. The value SHOULD normally not be Null + [1] and Numbers SHOULD NOT contain fractional parts [2]. + :type _id: str or int or None + + :param bool is_notification: Whether request is notification or not. If + value is True, _id is not included to request. It allows to create + requests with id = null. + + The Server MUST reply with the same value in the Response object if + included. This member is used to correlate the context between the two + objects. + + [1] The use of Null as a value for the id member in a Request object is + discouraged, because this specification uses a value of Null for Responses + with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null + for Notifications this could cause confusion in handling. + + [2] Fractional parts may be problematic, since many decimal fractions + cannot be represented exactly as binary fractions. + + """ + + JSONRPC_VERSION = "2.0" + REQUIRED_FIELDS = set(["jsonrpc", "method"]) + POSSIBLE_FIELDS = set(["jsonrpc", "method", "params", "id"]) + + @property + def data(self): + data = dict( + (k, v) for k, v in self._data.items() + if not (k == "id" and self.is_notification) + ) + data["jsonrpc"] = self.JSONRPC_VERSION + return data + + @data.setter + def data(self, value): + if not isinstance(value, dict): + raise ValueError("data should be dict") + + self._data = value + + @property + def method(self): + return self._data.get("method") + + @method.setter + def method(self, value): + if not isinstance(value, six.string_types): + raise ValueError("Method should be string") + + if value.startswith("rpc."): + raise ValueError( + "Method names that begin with the word rpc followed by a " + + "period character (U+002E or ASCII 46) are reserved for " + + "rpc-internal methods and extensions and MUST NOT be used " + + "for anything else.") + + self._data["method"] = str(value) + + @property + def params(self): + return self._data.get("params") + + @params.setter + def params(self, value): + if value is not None and not isinstance(value, (list, tuple, dict)): + raise ValueError("Incorrect params {0}".format(value)) + + value = list(value) if isinstance(value, tuple) else value + + if value is not None: + self._data["params"] = value + + @property + def _id(self): + return self._data.get("id") + + @_id.setter + def _id(self, value): + if value is not None and \ + not isinstance(value, six.string_types + six.integer_types): + raise ValueError("id should be string or integer") + + self._data["id"] = value + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls.from_data(data) + + @classmethod + def from_data(cls, data): + is_batch = isinstance(data, list) + data = data if is_batch else [data] + + if not data: + raise JSONRPCInvalidRequestException("[] value is not accepted") + + if not all(isinstance(d, dict) for d in data): + raise JSONRPCInvalidRequestException( + "Each request should be an object (dict)") + + result = [] + for d in data: + if not cls.REQUIRED_FIELDS <= set(d.keys()) <= cls.POSSIBLE_FIELDS: + extra = set(d.keys()) - cls.POSSIBLE_FIELDS + missed = cls.REQUIRED_FIELDS - set(d.keys()) + msg = "Invalid request. Extra fields: {0}, Missed fields: {1}" + raise JSONRPCInvalidRequestException(msg.format(extra, missed)) + + try: + result.append(JSONRPC20Request( + method=d["method"], params=d.get("params"), + _id=d.get("id"), is_notification="id" not in d, + )) + except ValueError as e: + raise JSONRPCInvalidRequestException(str(e)) + + return JSONRPC20BatchRequest(*result) if is_batch else result[0] + + +class JSONRPC20BatchRequest(object): + + """ Batch JSON-RPC 2.0 Request. + + :param JSONRPC20Request *requests: requests + + """ + + JSONRPC_VERSION = "2.0" + + def __init__(self, *requests): + self.requests = requests + + @classmethod + def from_json(cls, json_str): + return JSONRPC20Request.from_json(json_str) + + @property + def json(self): + return json.dumps([r.data for r in self.requests]) + + def __iter__(self): + return iter(self.requests) + + +class JSONRPC20Response(JSONRPCBaseResponse): + + """ JSON-RPC response object to JSONRPC20Request. + + When a rpc call is made, the Server MUST reply with a Response, except for + in the case of Notifications. The Response is expressed as a single JSON + Object, with the following members: + + :param str jsonrpc: A String specifying the version of the JSON-RPC + protocol. MUST be exactly "2.0". + + :param result: This member is REQUIRED on success. + This member MUST NOT exist if there was an error invoking the method. + The value of this member is determined by the method invoked on the + Server. + + :param dict error: This member is REQUIRED on error. + This member MUST NOT exist if there was no error triggered during + invocation. The value for this member MUST be an Object. + + :param id: This member is REQUIRED. + It MUST be the same as the value of the id member in the Request + Object. If there was an error in detecting the id in the Request + object (e.g. Parse error/Invalid Request), it MUST be Null. + :type id: str or int or None + + Either the result member or error member MUST be included, but both + members MUST NOT be included. + + """ + + JSONRPC_VERSION = "2.0" + + @property + def data(self): + data = dict((k, v) for k, v in self._data.items()) + data["jsonrpc"] = self.JSONRPC_VERSION + return data + + @data.setter + def data(self, value): + if not isinstance(value, dict): + raise ValueError("data should be dict") + self._data = value + + @property + def result(self): + return self._data.get("result") + + @result.setter + def result(self, value): + if self.error: + raise ValueError("Either result or error should be used") + self._data["result"] = value + + @property + def error(self): + return self._data.get("error") + + @error.setter + def error(self, value): + self._data.pop('value', None) + if value: + self._data["error"] = value + # Test error + JSONRPCError(**value) + + @property + def _id(self): + return self._data.get("id") + + @_id.setter + def _id(self, value): + if value is not None and \ + not isinstance(value, six.string_types + six.integer_types): + raise ValueError("id should be string or integer") + + self._data["id"] = value + + +class JSONRPC20BatchResponse(object): + + JSONRPC_VERSION = "2.0" + + def __init__(self, *responses): + self.responses = responses + self.request = None # type: JSONRPC20BatchRequest + + @property + def data(self): + return [r.data for r in self.responses] + + @property + def json(self): + return json.dumps(self.data) + + def __iter__(self): + return iter(self.responses) diff --git a/jsonrpc/manager.py b/jsonrpc/manager.py new file mode 100644 index 000000000..cdb8689af --- /dev/null +++ b/jsonrpc/manager.py @@ -0,0 +1,136 @@ +import json +import logging +from .utils import is_invalid_params +from .exceptions import ( + JSONRPCInvalidParams, + JSONRPCInvalidRequest, + JSONRPCInvalidRequestException, + JSONRPCMethodNotFound, + JSONRPCParseError, + JSONRPCServerError, + JSONRPCDispatchException, +) +from .jsonrpc1 import JSONRPC10Response +from .jsonrpc2 import ( + JSONRPC20BatchRequest, + JSONRPC20BatchResponse, + JSONRPC20Response, +) +from .jsonrpc import JSONRPCRequest + +logger = logging.getLogger(__name__) + + +class JSONRPCResponseManager(object): + + """ JSON-RPC response manager. + + Method brings syntactic sugar into library. Given dispatcher it handles + request (both single and batch) and handles errors. + Request could be handled in parallel, it is server responsibility. + + :param str request_str: json string. Will be converted into + JSONRPC20Request, JSONRPC20BatchRequest or JSONRPC10Request + + :param dict dispather: dict. + + """ + + RESPONSE_CLASS_MAP = { + "1.0": JSONRPC10Response, + "2.0": JSONRPC20Response, + } + + @classmethod + def handle(cls, request_str, dispatcher): + if isinstance(request_str, bytes): + request_str = request_str.decode("utf-8") + + try: + data = json.loads(request_str) + except (TypeError, ValueError): + return JSONRPC20Response(error=JSONRPCParseError()._data) + + try: + request = JSONRPCRequest.from_data(data) + except JSONRPCInvalidRequestException: + return JSONRPC20Response(error=JSONRPCInvalidRequest()._data) + + return cls.handle_request(request, dispatcher) + + @classmethod + def handle_request(cls, request, dispatcher): + """ Handle request data. + + At this moment request has correct jsonrpc format. + + :param dict request: data parsed from request_str. + :param jsonrpc.dispatcher.Dispatcher dispatcher: + + .. versionadded: 1.8.0 + + """ + rs = request if isinstance(request, JSONRPC20BatchRequest) \ + else [request] + responses = [r for r in cls._get_responses(rs, dispatcher) + if r is not None] + + # notifications + if not responses: + return + + if isinstance(request, JSONRPC20BatchRequest): + response = JSONRPC20BatchResponse(*responses) + response.request = request + return response + else: + return responses[0] + + @classmethod + def _get_responses(cls, requests, dispatcher): + """ Response to each single JSON-RPC Request. + + :return iterator(JSONRPC20Response): + + .. versionadded: 1.9.0 + TypeError inside the function is distinguished from Invalid Params. + + """ + for request in requests: + def make_response(**kwargs): + response = cls.RESPONSE_CLASS_MAP[request.JSONRPC_VERSION]( + _id=request._id, **kwargs) + response.request = request + return response + + output = None + try: + method = dispatcher[request.method] + except KeyError: + output = make_response(error=JSONRPCMethodNotFound()._data) + else: + try: + result = method(*request.args, **request.kwargs) + except JSONRPCDispatchException as e: + output = make_response(error=e.error._data) + except Exception as e: + data = { + "type": e.__class__.__name__, + "args": e.args, + "message": str(e), + } + + logger.exception("API Exception: {0}".format(data)) + + if isinstance(e, TypeError) and is_invalid_params( + method, *request.args, **request.kwargs): + output = make_response( + error=JSONRPCInvalidParams(data=data)._data) + else: + output = make_response( + error=JSONRPCServerError(data=data)._data) + else: + output = make_response(result=result) + finally: + if not request.is_notification: + yield output diff --git a/jsonrpc/six.py b/jsonrpc/six.py new file mode 100644 index 000000000..59145fed2 --- /dev/null +++ b/jsonrpc/six.py @@ -0,0 +1,584 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.4.1" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves") + + + +class Module_six_moves_urllib_parse(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse") +sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib.parse") + + +class Module_six_moves_urllib_error(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib_error") +sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib.error") + + +class Module_six_moves_urllib_request(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib_request") +sys.modules[__name__ + ".moves.urllib.request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib.request") + + +class Module_six_moves_urllib_response(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib_response") +sys.modules[__name__ + ".moves.urllib.response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib_robotparser") +sys.modules[__name__ + ".moves.urllib.robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + parse = sys.modules[__name__ + ".moves.urllib_parse"] + error = sys.modules[__name__ + ".moves.urllib_error"] + request = sys.modules[__name__ + ".moves.urllib_request"] + response = sys.modules[__name__ + ".moves.urllib_response"] + robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"] + + +sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(__name__ + ".moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + for slots_var in orig_vars.get('__slots__', ()): + orig_vars.pop(slots_var) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper \ No newline at end of file diff --git a/jsonrpc/tests/__init__.py b/jsonrpc/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jsonrpc/tests/py35_utils.py b/jsonrpc/tests/py35_utils.py new file mode 100644 index 000000000..e880150be --- /dev/null +++ b/jsonrpc/tests/py35_utils.py @@ -0,0 +1,7 @@ +# Python3.5+ code. +# This won't even parse in earlier versions, so it's kept in a separate file +# and imported when needed. + + +def distance(a: float, b: float) -> float: + return (a ** 2 + b ** 2) ** 0.5 diff --git a/jsonrpc/tests/test_backend_django/__init__.py b/jsonrpc/tests/test_backend_django/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jsonrpc/tests/test_backend_django/settings.py b/jsonrpc/tests/test_backend_django/settings.py new file mode 100644 index 000000000..987139e1b --- /dev/null +++ b/jsonrpc/tests/test_backend_django/settings.py @@ -0,0 +1,11 @@ +SECRET_KEY = 'secret' +ROOT_URLCONF = 'jsonrpc.tests.test_backend_django.urls' +ALLOWED_HOSTS = ['testserver'] +DATABASE_ENGINE = 'django.db.backends.sqlite3' +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} +JSONRPC_MAP_VIEW_ENABLED = True diff --git a/jsonrpc/tests/test_backend_django/tests.py b/jsonrpc/tests/test_backend_django/tests.py new file mode 100644 index 000000000..9a30fa0b6 --- /dev/null +++ b/jsonrpc/tests/test_backend_django/tests.py @@ -0,0 +1,89 @@ +""" Test Django Backend.""" +from __future__ import absolute_import +import os + +try: + from django.core.urlresolvers import RegexURLPattern + from django.test import TestCase +except ImportError: + import unittest + raise unittest.SkipTest('Django not found for testing') + +from ...backend.django import JSONRPCAPI, api +import json + + +class TestDjangoBackend(TestCase): + @classmethod + def setUpClass(cls): + os.environ['DJANGO_SETTINGS_MODULE'] = \ + 'jsonrpc.tests.test_backend_django.settings' + super(TestDjangoBackend, cls).setUpClass() + + def test_urls(self): + self.assertTrue(isinstance(api.urls, list)) + for api_url in api.urls: + self.assertTrue(isinstance(api_url, RegexURLPattern)) + + def test_client(self): + @api.dispatcher.add_method + def dummy(request): + return "" + + json_data = { + "id": "0", + "jsonrpc": "2.0", + "method": "dummy", + } + response = self.client.post( + '', + json.dumps(json_data), + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode('utf8')) + self.assertEqual(data['result'], '') + + def test_method_not_allowed(self): + response = self.client.get( + '', + content_type='application/json', + ) + self.assertEqual(response.status_code, 405, "Should allow only POST") + + def test_invalid_request(self): + response = self.client.post( + '', + '{', + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode('utf8')) + self.assertEqual(data['error']['code'], -32700) + self.assertEqual(data['error']['message'], 'Parse error') + + def test_resource_map(self): + response = self.client.get('/map') + self.assertEqual(response.status_code, 200) + data = response.content.decode('utf8') + self.assertIn("JSON-RPC map", data) + + def test_method_not_allowed_prefix(self): + response = self.client.get( + '/prefix/', + content_type='application/json', + ) + self.assertEqual(response.status_code, 405) + + def test_resource_map_prefix(self): + response = self.client.get('/prefix/map') + self.assertEqual(response.status_code, 200) + + def test_empty_initial_dispatcher(self): + class SubDispatcher(type(api.dispatcher)): + pass + + custom_dispatcher = SubDispatcher() + custom_api = JSONRPCAPI(custom_dispatcher) + self.assertEqual(type(custom_api.dispatcher), SubDispatcher) + self.assertEqual(id(custom_api.dispatcher), id(custom_dispatcher)) diff --git a/jsonrpc/tests/test_backend_django/urls.py b/jsonrpc/tests/test_backend_django/urls.py new file mode 100644 index 000000000..c0a22465e --- /dev/null +++ b/jsonrpc/tests/test_backend_django/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url, include +from jsonrpc.backend.django import api + +urlpatterns = [ + url(r'', include(api.urls)), + url(r'^prefix/', include(api.urls)), +] diff --git a/jsonrpc/tests/test_backend_flask/__init__.py b/jsonrpc/tests/test_backend_flask/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jsonrpc/tests/test_backend_flask/tests.py b/jsonrpc/tests/test_backend_flask/tests.py new file mode 100644 index 000000000..fdb862348 --- /dev/null +++ b/jsonrpc/tests/test_backend_flask/tests.py @@ -0,0 +1,182 @@ +import json +import sys + +if sys.version_info < (3, 3): + from mock import patch +else: + from unittest.mock import patch + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + +# Flask is supported only for python2 and python3.3+ +if sys.version_info < (3, 0) or sys.version_info >= (3, 3): + try: + from flask import Flask + except ImportError: + raise unittest.SkipTest('Flask not found for testing') + + from ...backend.flask import JSONRPCAPI, api + + @api.dispatcher.add_method + def dummy(): + return "" + + +@unittest.skipIf((3, 0) <= sys.version_info < (3, 3), + 'Flask does not support python 3.0 - 3.2') +class TestFlaskBackend(unittest.TestCase): + REQUEST = json.dumps({ + "id": "0", + "jsonrpc": "2.0", + "method": "dummy", + }) + + def setUp(self): + self.client = self._get_test_client(JSONRPCAPI()) + + def _get_test_client(self, api): + @api.dispatcher.add_method + def dummy(): + return "" + + app = Flask(__name__) + app.config["TESTING"] = True + app.register_blueprint(api.as_blueprint()) + return app.test_client() + + def test_client(self): + response = self.client.post( + '/', + data=self.REQUEST, + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['result'], '') + + def test_method_not_allowed(self): + response = self.client.get( + '/', + content_type='application/json', + ) + self.assertEqual(response.status_code, 405, "Should allow only POST") + + def test_parse_error(self): + response = self.client.post( + '/', + data='{', + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['error']['code'], -32700) + self.assertEqual(data['error']['message'], 'Parse error') + + def test_wrong_content_type(self): + response = self.client.post( + '/', + data=self.REQUEST, + content_type='application/x-www-form-urlencoded', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['error']['code'], -32700) + self.assertEqual(data['error']['message'], 'Parse error') + + def test_invalid_request(self): + response = self.client.post( + '/', + data='{"method": "dummy", "id": 1}', + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['error']['code'], -32600) + self.assertEqual(data['error']['message'], 'Invalid Request') + + def test_method_not_found(self): + data = { + "jsonrpc": "2.0", + "method": "dummy2", + "id": 1 + } + response = self.client.post( + '/', + data=json.dumps(data), + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['error']['code'], -32601) + self.assertEqual(data['error']['message'], 'Method not found') + + def test_invalid_parameters(self): + data = { + "jsonrpc": "2.0", + "method": "dummy", + "params": [42], + "id": 1 + } + response = self.client.post( + '/', + data=json.dumps(data), + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['error']['code'], -32602) + self.assertEqual(data['error']['message'], 'Invalid params') + + def test_resource_map(self): + response = self.client.get('/map') + self.assertEqual(response.status_code, 200) + self.assertTrue("JSON-RPC map" in response.data.decode('utf8')) + + def test_method_not_allowed_prefix(self): + response = self.client.get( + '/', + content_type='application/json', + ) + self.assertEqual(response.status_code, 405) + + def test_resource_map_prefix(self): + response = self.client.get('/map') + self.assertEqual(response.status_code, 200) + + def test_as_view(self): + api = JSONRPCAPI() + with patch.object(api, 'jsonrpc') as mock_jsonrpc: + self.assertIs(api.as_view(), mock_jsonrpc) + + def test_not_check_content_type(self): + client = self._get_test_client(JSONRPCAPI(check_content_type=False)) + response = client.post( + '/', + data=self.REQUEST, + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['result'], '') + + def test_check_content_type(self): + client = self._get_test_client(JSONRPCAPI(check_content_type=False)) + response = client.post( + '/', + data=self.REQUEST, + content_type="application/x-www-form-urlencoded" + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data['result'], '') + + def test_empty_initial_dispatcher(self): + class SubDispatcher(type(api.dispatcher)): + pass + + custom_dispatcher = SubDispatcher() + custom_api = JSONRPCAPI(custom_dispatcher) + self.assertEqual(type(custom_api.dispatcher), SubDispatcher) + self.assertEqual(id(custom_api.dispatcher), id(custom_dispatcher)) diff --git a/jsonrpc/tests/test_base.py b/jsonrpc/tests/test_base.py new file mode 100644 index 000000000..6a0ff85b0 --- /dev/null +++ b/jsonrpc/tests/test_base.py @@ -0,0 +1,39 @@ +""" Test base JSON-RPC classes.""" +import sys + +from ..base import JSONRPCBaseRequest, JSONRPCBaseResponse + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONRPCBaseRequest(unittest.TestCase): + + """ Test JSONRPCBaseRequest functionality.""" + + def test_data(self): + request = JSONRPCBaseRequest() + self.assertEqual(request.data, {}) + + with self.assertRaises(ValueError): + request.data = [] + + with self.assertRaises(ValueError): + request.data = None + + +class TestJSONRPCBaseResponse(unittest.TestCase): + + """ Test JSONRPCBaseResponse functionality.""" + + def test_data(self): + response = JSONRPCBaseResponse(result="") + self.assertEqual(response.data, {}) + + with self.assertRaises(ValueError): + response.data = [] + + with self.assertRaises(ValueError): + response.data = None diff --git a/jsonrpc/tests/test_bug29.py b/jsonrpc/tests/test_bug29.py new file mode 100644 index 000000000..7019bce3b --- /dev/null +++ b/jsonrpc/tests/test_bug29.py @@ -0,0 +1,34 @@ +""" Exmples of usage with tests. + +Tests in this file represent examples taken from JSON-RPC specification. +http://www.jsonrpc.org/specification#examples + +""" +import sys +import json + +from ..manager import JSONRPCResponseManager + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +def isjsonequal(json1, json2): + return json.loads(json1) == json.loads(json2) + + +class TestJSONRPCExamples(unittest.TestCase): + def setUp(self): + self.dispatcher = { + "return_none": lambda: None, + } + + def test_none_as_result(self): + req = '{"jsonrpc": "2.0", "method": "return_none", "id": 0}' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "result": null, "id": 0}' + )) diff --git a/jsonrpc/tests/test_dispatcher.py b/jsonrpc/tests/test_dispatcher.py new file mode 100644 index 000000000..888526ff6 --- /dev/null +++ b/jsonrpc/tests/test_dispatcher.py @@ -0,0 +1,142 @@ +from ..dispatcher import Dispatcher +import sys +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class Math: + + def sum(self, a, b): + return a + b + + def diff(self, a, b): + return a - b + + +class TestDispatcher(unittest.TestCase): + + """ Test Dispatcher functionality.""" + + def test_getter(self): + d = Dispatcher() + + with self.assertRaises(KeyError): + d["method"] + + d["add"] = lambda *args: sum(args) + self.assertEqual(d["add"](1, 1), 2) + + def test_in(self): + d = Dispatcher() + d["method"] = lambda: "" + self.assertIn("method", d) + + def test_add_method(self): + d = Dispatcher() + + @d.add_method + def add(x, y): + return x + y + + self.assertIn("add", d) + self.assertEqual(d["add"](1, 1), 2) + + def test_add_method_with_name(self): + d = Dispatcher() + + @d.add_method(name="this.add") + def add(x, y): + return x + y + + self.assertNotIn("add", d) + self.assertIn("this.add", d) + self.assertEqual(d["this.add"](1, 1), 2) + + def test_add_class(self): + d = Dispatcher() + d.add_class(Math) + + self.assertIn("math.sum", d) + self.assertIn("math.diff", d) + self.assertEqual(d["math.sum"](3, 8), 11) + self.assertEqual(d["math.diff"](6, 9), -3) + + def test_add_object(self): + d = Dispatcher() + d.add_object(Math()) + + self.assertIn("math.sum", d) + self.assertIn("math.diff", d) + self.assertEqual(d["math.sum"](5, 2), 7) + self.assertEqual(d["math.diff"](15, 9), 6) + + def test_add_dict(self): + d = Dispatcher() + d.add_dict({"sum": lambda *args: sum(args)}, "util") + + self.assertIn("util.sum", d) + self.assertEqual(d["util.sum"](13, -2), 11) + + def test_add_method_keep_function_definitions(self): + + d = Dispatcher() + + @d.add_method + def one(x): + return x + + self.assertIsNotNone(one) + + def test_del_method(self): + d = Dispatcher() + d["method"] = lambda: "" + self.assertIn("method", d) + + del d["method"] + self.assertNotIn("method", d) + + def test_to_dict(self): + d = Dispatcher() + + def func(): + return "" + + d["method"] = func + self.assertEqual(dict(d), {"method": func}) + + def test_init_from_object_instance(self): + + class Dummy(): + + def one(self): + pass + + def two(self): + pass + + dummy = Dummy() + + d = Dispatcher(dummy) + + self.assertIn("one", d) + self.assertIn("two", d) + self.assertNotIn("__class__", d) + + def test_init_from_dictionary(self): + + dummy = { + 'one': lambda x: x, + 'two': lambda x: x, + } + + d = Dispatcher(dummy) + + self.assertIn("one", d) + self.assertIn("two", d) + + def test_dispatcher_representation(self): + + d = Dispatcher() + self.assertEqual('{}', repr(d)) diff --git a/jsonrpc/tests/test_examples20.py b/jsonrpc/tests/test_examples20.py new file mode 100644 index 000000000..42d10fd91 --- /dev/null +++ b/jsonrpc/tests/test_examples20.py @@ -0,0 +1,206 @@ +""" Exmples of usage with tests. + +Tests in this file represent examples taken from JSON-RPC specification. +http://www.jsonrpc.org/specification#examples + +""" +import sys +import json + +from ..manager import JSONRPCResponseManager +from ..jsonrpc2 import JSONRPC20Request, JSONRPC20BatchRequest + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +def isjsonequal(json1, json2): + return json.loads(json1) == json.loads(json2) + + +class TestJSONRPCExamples(unittest.TestCase): + def setUp(self): + self.dispatcher = { + "subtract": lambda a, b: a - b, + } + + def test_rpc_call_with_positional_parameters(self): + req = '{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}' # noqa + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "result": 19, "id": 1}' + )) + + req = '{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}' # noqa + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "result": -19, "id": 2}' + )) + + def test_rpc_call_with_named_parameters(self): + def subtract(minuend=None, subtrahend=None): + return minuend - subtrahend + + dispatcher = { + "subtract": subtract, + "sum": lambda *args: sum(args), + "get_data": lambda: ["hello", 5], + } + + req = '{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}' # noqa + response = JSONRPCResponseManager.handle(req, dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "result": 19, "id": 3}' + )) + + req = '{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}' # noqa + response = JSONRPCResponseManager.handle(req, dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "result": 19, "id": 4}', + )) + + def test_notification(self): + req = '{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertEqual(response, None) + + req = '{"jsonrpc": "2.0", "method": "foobar"}' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertEqual(response, None) + + def test_rpc_call_of_non_existent_method(self): + req = '{"jsonrpc": "2.0", "method": "foobar", "id": "1"}' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}' # noqa + )) + + def test_rpc_call_with_invalid_json(self): + req = '{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}' # noqa + )) + + def test_rpc_call_with_invalid_request_object(self): + req = '{"jsonrpc": "2.0", "method": 1, "params": "bar"}' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}' # noqa + )) + + def test_rpc_call_batch_invalid_json(self): + req = """[ + {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, + {"jsonrpc": "2.0", "method" + ]""" + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}' # noqa + )) + + def test_rpc_call_with_an_empty_array(self): + req = '[]' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}' # noqa + )) + + def test_rpc_call_with_rpc_call_with_an_invalid_batch_but_not_empty(self): + req = '[1]' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isjsonequal( + response.json, + '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}' # noqa + )) + + def test_rpc_call_with_invalid_batch(self): + req = '[1,2,3]' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue( + response, + json.loads("""[ + {"jsonrpc": "2.0", "error": {"code": -32600, + "message": "Invalid Request"}, "id": null}, + {"jsonrpc": "2.0", "error": {"code": -32600, + "message": "Invalid Request"}, "id": null}, + {"jsonrpc": "2.0", "error": {"code": -32600, + "message": "Invalid Request"}, "id": null} + ]""") + ) + + def test_rpc_call_batch(self): + req = """[ + {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, + {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, + {"jsonrpc": "2.0", "method": "subtract", + "params": [42,23], "id": "2"}, + {"foo": "boo"}, + {"jsonrpc": "2.0", "method": "foo.get", + "params": {"name": "myself"}, "id": "5"}, + {"jsonrpc": "2.0", "method": "get_data", "id": "9"} + ]""" + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue( + response, + json.loads("""[ + {"jsonrpc": "2.0", "result": 7, "id": "1"}, + {"jsonrpc": "2.0", "result": 19, "id": "2"}, + {"jsonrpc": "2.0", "error": {"code": -32600, + "message": "Invalid Request"}, "id": null}, + {"jsonrpc": "2.0", "error": {"code": -32601, + "message": "Method not found"}, "id": "5"}, + {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} + ]""") + ) + + def test_rpc_call_batch_all_notifications(self): + req = """[ + {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]}, + {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]} + ]""" + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertEqual(response, None) + + def test_rpc_call_response_request(self): + req = '{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}' # noqa + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isinstance( + response.request, + JSONRPC20Request + )) + self.assertTrue(isjsonequal( + response.request.json, + req + )) + + def test_rpc_call_response_request_batch(self): + req = """[ + {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, + {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, + {"jsonrpc": "2.0", "method": "subtract", + "params": [42,23], "id": "2"}, + {"jsonrpc": "2.0", "method": "foo.get", + "params": {"name": "myself"}, "id": "5"}, + {"jsonrpc": "2.0", "method": "get_data", "id": "9"} + ]""" + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isinstance( + response.request, + JSONRPC20BatchRequest + )) + self.assertTrue(isjsonequal( + response.request.json, + req + )) diff --git a/jsonrpc/tests/test_jsonrpc.py b/jsonrpc/tests/test_jsonrpc.py new file mode 100644 index 000000000..820ee3111 --- /dev/null +++ b/jsonrpc/tests/test_jsonrpc.py @@ -0,0 +1 @@ +""" Tets base JSON-RPC structures.""" diff --git a/jsonrpc/tests/test_jsonrpc1.py b/jsonrpc/tests/test_jsonrpc1.py new file mode 100644 index 000000000..c66f04545 --- /dev/null +++ b/jsonrpc/tests/test_jsonrpc1.py @@ -0,0 +1,429 @@ +import json +import sys + +from ..exceptions import JSONRPCInvalidRequestException +from ..jsonrpc1 import ( + JSONRPC10Request, + JSONRPC10Response, +) + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONRPC10Request(unittest.TestCase): + + """ Test JSONRPC10Request functionality.""" + + def setUp(self): + self.request_params = { + "method": "add", + "params": [1, 2], + "_id": 1, + } + + def test_correct_init(self): + """ Test object is created.""" + JSONRPC10Request(**self.request_params) + + def test_validation_incorrect_no_parameters(self): + with self.assertRaises(ValueError): + JSONRPC10Request() + + def test_method_validation_str(self): + self.request_params.update({"method": "add"}) + JSONRPC10Request(**self.request_params) + + def test_method_validation_not_str(self): + self.request_params.update({"method": []}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + self.request_params.update({"method": {}}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + self.request_params.update({"method": None}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + def test_params_validation_list(self): + self.request_params.update({"params": []}) + JSONRPC10Request(**self.request_params) + + self.request_params.update({"params": [0]}) + JSONRPC10Request(**self.request_params) + + def test_params_validation_tuple(self): + self.request_params.update({"params": ()}) + JSONRPC10Request(**self.request_params) + + self.request_params.update({"params": tuple([0])}) + JSONRPC10Request(**self.request_params) + + def test_params_validation_dict(self): + self.request_params.update({"params": {}}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + self.request_params.update({"params": {"a": 0}}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + def test_params_validation_none(self): + self.request_params.update({"params": None}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + def test_params_validation_incorrect(self): + self.request_params.update({"params": "str"}) + with self.assertRaises(ValueError): + JSONRPC10Request(**self.request_params) + + def test_request_args(self): + self.assertEqual(JSONRPC10Request("add", []).args, ()) + self.assertEqual(JSONRPC10Request("add", [1, 2]).args, (1, 2)) + + def test_id_validation_string(self): + self.request_params.update({"_id": "id"}) + JSONRPC10Request(**self.request_params) + + def test_id_validation_int(self): + self.request_params.update({"_id": 0}) + JSONRPC10Request(**self.request_params) + + def test_id_validation_null(self): + self.request_params.update({"_id": "null"}) + JSONRPC10Request(**self.request_params) + + def test_id_validation_none(self): + self.request_params.update({"_id": None}) + JSONRPC10Request(**self.request_params) + + def test_id_validation_float(self): + self.request_params.update({"_id": 0.1}) + JSONRPC10Request(**self.request_params) + + def test_id_validation_list_tuple(self): + self.request_params.update({"_id": []}) + JSONRPC10Request(**self.request_params) + + self.request_params.update({"_id": ()}) + JSONRPC10Request(**self.request_params) + + def test_id_validation_default_id_none(self): + del self.request_params["_id"] + JSONRPC10Request(**self.request_params) + + def test_data_method_1(self): + r = JSONRPC10Request("add", []) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_method_2(self): + r = JSONRPC10Request(method="add", params=[]) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_params_1(self): + r = JSONRPC10Request("add", params=[], _id=None) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_params_2(self): + r = JSONRPC10Request("add", ()) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_params_3(self): + r = JSONRPC10Request("add", (1, 2)) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [1, 2], + "id": None, + }) + + def test_data_id_1(self): + r = JSONRPC10Request("add", [], _id="null") + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": "null", + }) + + def test_data_id_1_notification(self): + r = JSONRPC10Request("add", [], _id="null", is_notification=True) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_id_2(self): + r = JSONRPC10Request("add", [], _id=None) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_id_2_notification(self): + r = JSONRPC10Request("add", [], _id=None, is_notification=True) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_id_3(self): + r = JSONRPC10Request("add", [], _id="id") + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": "id", + }) + + def test_data_id_3_notification(self): + r = JSONRPC10Request("add", [], _id="id", is_notification=True) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_data_id_4(self): + r = JSONRPC10Request("add", [], _id=0) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": 0, + }) + + def test_data_id_4_notification(self): + r = JSONRPC10Request("add", [], _id=0, is_notification=True) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "method": "add", + "params": [], + "id": None, + }) + + def test_is_notification(self): + r = JSONRPC10Request("add", []) + self.assertTrue(r.is_notification) + + r = JSONRPC10Request("add", [], _id=None) + self.assertTrue(r.is_notification) + + r = JSONRPC10Request("add", [], _id="null") + self.assertFalse(r.is_notification) + + r = JSONRPC10Request("add", [], _id=0) + self.assertFalse(r.is_notification) + + r = JSONRPC10Request("add", [], is_notification=True) + self.assertTrue(r.is_notification) + + r = JSONRPC10Request("add", [], is_notification=True, _id=None) + self.assertTrue(r.is_notification) + + r = JSONRPC10Request("add", [], is_notification=True, _id=0) + self.assertTrue(r.is_notification) + + def test_set_unset_notification_keep_id(self): + r = JSONRPC10Request("add", [], is_notification=True, _id=0) + self.assertTrue(r.is_notification) + self.assertEqual(r.data["id"], None) + + r.is_notification = False + self.assertFalse(r.is_notification) + self.assertEqual(r.data["id"], 0) + + def test_error_if_notification_true_but_id_none(self): + r = JSONRPC10Request("add", [], is_notification=True, _id=None) + with self.assertRaises(ValueError): + r.is_notification = False + + def test_from_json_invalid_request_method(self): + str_json = json.dumps({ + "params": [1, 2], + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC10Request.from_json(str_json) + + def test_from_json_invalid_request_params(self): + str_json = json.dumps({ + "method": "add", + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC10Request.from_json(str_json) + + def test_from_json_invalid_request_id(self): + str_json = json.dumps({ + "method": "add", + "params": [1, 2], + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC10Request.from_json(str_json) + + def test_from_json_invalid_request_extra_data(self): + str_json = json.dumps({ + "method": "add", + "params": [1, 2], + "id": 0, + "is_notification": True, + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC10Request.from_json(str_json) + + def test_from_json_request(self): + str_json = json.dumps({ + "method": "add", + "params": [1, 2], + "id": 0, + }) + + request = JSONRPC10Request.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPC10Request)) + self.assertEqual(request.method, "add") + self.assertEqual(request.params, [1, 2]) + self.assertEqual(request._id, 0) + self.assertFalse(request.is_notification) + + def test_from_json_request_notification(self): + str_json = json.dumps({ + "method": "add", + "params": [1, 2], + "id": None, + }) + + request = JSONRPC10Request.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPC10Request)) + self.assertEqual(request.method, "add") + self.assertEqual(request.params, [1, 2]) + self.assertEqual(request._id, None) + self.assertTrue(request.is_notification) + + def test_from_json_string_not_dict(self): + with self.assertRaises(ValueError): + JSONRPC10Request.from_json("[]") + + with self.assertRaises(ValueError): + JSONRPC10Request.from_json("0") + + def test_data_setter(self): + request = JSONRPC10Request(**self.request_params) + with self.assertRaises(ValueError): + request.data = [] + + with self.assertRaises(ValueError): + request.data = "" + + with self.assertRaises(ValueError): + request.data = None + + +class TestJSONRPC10Response(unittest.TestCase): + + """ Test JSONRPC10Response functionality.""" + + def setUp(self): + self.response_success_params = { + "result": "", + "error": None, + "_id": 1, + } + self.response_error_params = { + "result": None, + "error": { + "code": 1, + "message": "error", + }, + "_id": 1, + } + + def test_correct_init(self): + """ Test object is created.""" + JSONRPC10Response(**self.response_success_params) + JSONRPC10Response(**self.response_error_params) + + def test_validation_incorrect_no_parameters(self): + with self.assertRaises(ValueError): + JSONRPC10Response() + + def test_validation_success_incorrect(self): + wrong_params = self.response_success_params + del wrong_params["_id"] + with self.assertRaises(ValueError): + JSONRPC10Response(**wrong_params) + + def test_validation_error_incorrect(self): + wrong_params = self.response_error_params + del wrong_params["_id"] + with self.assertRaises(ValueError): + JSONRPC10Response(**wrong_params) + + def _test_validation_incorrect_result_and_error(self): + # @todo: remove + # It is OK because result is an mepty string, it is still result + with self.assertRaises(ValueError): + JSONRPC10Response(result="", error="", _id=0) + + response = JSONRPC10Response(error="", _id=0) + with self.assertRaises(ValueError): + response.result = "" + + def test_data(self): + r = JSONRPC10Response(result="", _id=0) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "result": "", + "id": 0, + }) + + def test_data_setter(self): + response = JSONRPC10Response(**self.response_success_params) + with self.assertRaises(ValueError): + response.data = [] + + with self.assertRaises(ValueError): + response.data = "" + + with self.assertRaises(ValueError): + response.data = None + + def test_validation_id(self): + response = JSONRPC10Response(**self.response_success_params) + self.assertEqual(response._id, self.response_success_params["_id"]) diff --git a/jsonrpc/tests/test_jsonrpc2.py b/jsonrpc/tests/test_jsonrpc2.py new file mode 100644 index 000000000..32c1639ab --- /dev/null +++ b/jsonrpc/tests/test_jsonrpc2.py @@ -0,0 +1,728 @@ +import json +import sys + +from ..exceptions import JSONRPCInvalidRequestException +from ..jsonrpc2 import ( + JSONRPC20Request, + JSONRPC20BatchRequest, + JSONRPC20Response, + JSONRPC20BatchResponse, +) + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONRPC20Request(unittest.TestCase): + + """ Test JSONRPC20Request functionality.""" + + def setUp(self): + self.request_params = { + "method": "add", + "params": [1, 2], + "_id": 1, + } + + def test_correct_init(self): + """ Test object is created.""" + JSONRPC20Request(**self.request_params) + + def test_validation_incorrect_no_parameters(self): + with self.assertRaises(ValueError): + JSONRPC20Request() + + def test_method_validation_str(self): + self.request_params.update({"method": "add"}) + JSONRPC20Request(**self.request_params) + + def test_method_validation_not_str(self): + self.request_params.update({"method": []}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + self.request_params.update({"method": {}}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + def test_method_validation_str_rpc_prefix(self): + """ Test method SHOULD NOT starts with rpc. """ + self.request_params.update({"method": "rpc."}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + self.request_params.update({"method": "rpc.test"}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + self.request_params.update({"method": "rpccorrect"}) + JSONRPC20Request(**self.request_params) + + self.request_params.update({"method": "rpc"}) + JSONRPC20Request(**self.request_params) + + def test_params_validation_list(self): + self.request_params.update({"params": []}) + JSONRPC20Request(**self.request_params) + + self.request_params.update({"params": [0]}) + JSONRPC20Request(**self.request_params) + + def test_params_validation_tuple(self): + self.request_params.update({"params": ()}) + JSONRPC20Request(**self.request_params) + + self.request_params.update({"params": tuple([0])}) + JSONRPC20Request(**self.request_params) + + def test_params_validation_dict(self): + self.request_params.update({"params": {}}) + JSONRPC20Request(**self.request_params) + + self.request_params.update({"params": {"a": 0}}) + JSONRPC20Request(**self.request_params) + + def test_params_validation_none(self): + self.request_params.update({"params": None}) + JSONRPC20Request(**self.request_params) + + def test_params_validation_incorrect(self): + self.request_params.update({"params": "str"}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + def test_request_args(self): + self.assertEqual(JSONRPC20Request("add").args, ()) + self.assertEqual(JSONRPC20Request("add", []).args, ()) + self.assertEqual(JSONRPC20Request("add", {"a": 1}).args, ()) + self.assertEqual(JSONRPC20Request("add", [1, 2]).args, (1, 2)) + + def test_request_kwargs(self): + self.assertEqual(JSONRPC20Request("add").kwargs, {}) + self.assertEqual(JSONRPC20Request("add", [1, 2]).kwargs, {}) + self.assertEqual(JSONRPC20Request("add", {}).kwargs, {}) + self.assertEqual(JSONRPC20Request("add", {"a": 1}).kwargs, {"a": 1}) + + def test_id_validation_string(self): + self.request_params.update({"_id": "id"}) + JSONRPC20Request(**self.request_params) + + def test_id_validation_int(self): + self.request_params.update({"_id": 0}) + JSONRPC20Request(**self.request_params) + + def test_id_validation_null(self): + self.request_params.update({"_id": "null"}) + JSONRPC20Request(**self.request_params) + + def test_id_validation_none(self): + self.request_params.update({"_id": None}) + JSONRPC20Request(**self.request_params) + + def test_id_validation_float(self): + self.request_params.update({"_id": 0.1}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + def test_id_validation_incorrect(self): + self.request_params.update({"_id": []}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + self.request_params.update({"_id": ()}) + with self.assertRaises(ValueError): + JSONRPC20Request(**self.request_params) + + def test_data_method_1(self): + r = JSONRPC20Request("add") + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": None, + }) + + def test_data_method_2(self): + r = JSONRPC20Request(method="add") + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": None, + }) + + def test_data_method_3(self): + r = JSONRPC20Request("add", None) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": None, + }) + + def test_data_params_1(self): + r = JSONRPC20Request("add", params=None, _id=None) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": None, + }) + + def test_data_params_2(self): + r = JSONRPC20Request("add", []) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "params": [], + "id": None, + }) + + def test_data_params_3(self): + r = JSONRPC20Request("add", ()) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "params": [], + "id": None, + }) + + def test_data_params_4(self): + r = JSONRPC20Request("add", (1, 2)) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "params": [1, 2], + "id": None, + }) + + def test_data_params_5(self): + r = JSONRPC20Request("add", {"a": 0}) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "params": {"a": 0}, + "id": None, + }) + + def test_data_id_1(self): + r = JSONRPC20Request("add", _id="null") + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": "null", + }) + + def test_data_id_1_notification(self): + r = JSONRPC20Request("add", _id="null", is_notification=True) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + }) + + def test_data_id_2(self): + r = JSONRPC20Request("add", _id=None) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": None, + }) + + def test_data_id_2_notification(self): + r = JSONRPC20Request("add", _id=None, is_notification=True) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + }) + + def test_data_id_3(self): + r = JSONRPC20Request("add", _id="id") + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": "id", + }) + + def test_data_id_3_notification(self): + r = JSONRPC20Request("add", _id="id", is_notification=True) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + }) + + def test_data_id_4(self): + r = JSONRPC20Request("add", _id=0) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + "id": 0, + }) + + def test_data_id_4_notification(self): + r = JSONRPC20Request("add", _id=0, is_notification=True) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "method": "add", + }) + + def test_is_notification(self): + r = JSONRPC20Request("add") + self.assertFalse(r.is_notification) + + r = JSONRPC20Request("add", _id=None) + self.assertFalse(r.is_notification) + + r = JSONRPC20Request("add", _id="null") + self.assertFalse(r.is_notification) + + r = JSONRPC20Request("add", _id=0) + self.assertFalse(r.is_notification) + + r = JSONRPC20Request("add", is_notification=True) + self.assertTrue(r.is_notification) + + r = JSONRPC20Request("add", is_notification=True, _id=None) + self.assertTrue(r.is_notification) + self.assertNotIn("id", r.data) + + r = JSONRPC20Request("add", is_notification=True, _id=0) + self.assertTrue(r.is_notification) + self.assertNotIn("id", r.data) + + def test_set_unset_notification_keep_id(self): + r = JSONRPC20Request("add", is_notification=True, _id=0) + self.assertTrue(r.is_notification) + self.assertFalse("id" in r.data) + + r.is_notification = False + self.assertFalse(r.is_notification) + self.assertTrue("id" in r.data) + self.assertEqual(r.data["id"], 0) + + def test_serialize_method_1(self): + r = JSONRPC20Request("add") + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": None, + }, json.loads(r.json)) + + def test_serialize_method_2(self): + r = JSONRPC20Request(method="add") + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": None, + }, json.loads(r.json)) + + def test_serialize_method_3(self): + r = JSONRPC20Request("add", None) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": None, + }, json.loads(r.json)) + + def test_serialize_params_1(self): + r = JSONRPC20Request("add", params=None, _id=None) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": None, + }, json.loads(r.json)) + + def test_serialize_params_2(self): + r = JSONRPC20Request("add", []) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "params": [], + "id": None, + }, json.loads(r.json)) + + def test_serialize_params_3(self): + r = JSONRPC20Request("add", ()) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "params": [], + "id": None, + }, json.loads(r.json)) + + def test_serialize_params_4(self): + r = JSONRPC20Request("add", (1, 2)) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "params": [1, 2], + "id": None, + }, json.loads(r.json)) + + def test_serialize_params_5(self): + r = JSONRPC20Request("add", {"a": 0}) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "params": {"a": 0}, + "id": None, + }, json.loads(r.json)) + + def test_serialize_id_1(self): + r = JSONRPC20Request("add", _id="null") + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": "null", + }, json.loads(r.json)) + + def test_serialize_id_2(self): + r = JSONRPC20Request("add", _id=None) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": None, + }, json.loads(r.json)) + + def test_serialize_id_3(self): + r = JSONRPC20Request("add", _id="id") + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": "id", + }, json.loads(r.json)) + + def test_serialize_id_4(self): + r = JSONRPC20Request("add", _id=0) + self.assertTrue({ + "jsonrpc": "2.0", + "method": "add", + "id": 0, + }, json.loads(r.json)) + + def test_from_json_request_no_id(self): + str_json = json.dumps({ + "method": "add", + "params": [1, 2], + "jsonrpc": "2.0", + }) + + request = JSONRPC20Request.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPC20Request)) + self.assertEqual(request.method, "add") + self.assertEqual(request.params, [1, 2]) + self.assertEqual(request._id, None) + self.assertTrue(request.is_notification) + + def test_from_json_request_no_params(self): + str_json = json.dumps({ + "method": "add", + "jsonrpc": "2.0", + }) + + request = JSONRPC20Request.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPC20Request)) + self.assertEqual(request.method, "add") + self.assertEqual(request.params, None) + self.assertEqual(request._id, None) + self.assertTrue(request.is_notification) + + def test_from_json_request_null_id(self): + str_json = json.dumps({ + "method": "add", + "jsonrpc": "2.0", + "id": None, + }) + + request = JSONRPC20Request.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPC20Request)) + self.assertEqual(request.method, "add") + self.assertEqual(request.params, None) + self.assertEqual(request._id, None) + self.assertFalse(request.is_notification) + + def test_from_json_request(self): + str_json = json.dumps({ + "method": "add", + "params": [0, 1], + "jsonrpc": "2.0", + "id": "id", + }) + + request = JSONRPC20Request.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPC20Request)) + self.assertEqual(request.method, "add") + self.assertEqual(request.params, [0, 1]) + self.assertEqual(request._id, "id") + self.assertFalse(request.is_notification) + + def test_from_json_invalid_request_jsonrpc(self): + str_json = json.dumps({ + "method": "add", + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC20Request.from_json(str_json) + + def test_from_json_invalid_request_method(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC20Request.from_json(str_json) + + def test_from_json_invalid_request_extra_data(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + "method": "add", + "is_notification": True, + }) + + with self.assertRaises(JSONRPCInvalidRequestException): + JSONRPC20Request.from_json(str_json) + + def test_data_setter(self): + request = JSONRPC20Request(**self.request_params) + with self.assertRaises(ValueError): + request.data = [] + + with self.assertRaises(ValueError): + request.data = "" + + with self.assertRaises(ValueError): + request.data = None + + +class TestJSONRPC20BatchRequest(unittest.TestCase): + + """ Test JSONRPC20BatchRequest functionality.""" + + def test_batch_request(self): + request = JSONRPC20BatchRequest( + JSONRPC20Request("devide", {"num": 1, "denom": 2}, _id=1), + JSONRPC20Request("devide", {"num": 3, "denom": 2}, _id=2), + ) + self.assertEqual(json.loads(request.json), [ + {"method": "devide", "params": {"num": 1, "denom": 2}, "id": 1, + "jsonrpc": "2.0"}, + {"method": "devide", "params": {"num": 3, "denom": 2}, "id": 2, + "jsonrpc": "2.0"}, + ]) + + def test_from_json_batch(self): + str_json = json.dumps([ + {"method": "add", "params": [1, 2], "jsonrpc": "2.0"}, + {"method": "mul", "params": [1, 2], "jsonrpc": "2.0"}, + ]) + + requests = JSONRPC20BatchRequest.from_json(str_json) + self.assertTrue(isinstance(requests, JSONRPC20BatchRequest)) + for r in requests: + self.assertTrue(isinstance(r, JSONRPC20Request)) + self.assertTrue(r.method in ["add", "mul"]) + self.assertEqual(r.params, [1, 2]) + self.assertEqual(r._id, None) + self.assertTrue(r.is_notification) + + def test_from_json_batch_one(self): + str_json = json.dumps([ + {"method": "add", "params": [1, 2], "jsonrpc": "2.0", "id": None}, + ]) + + requests = JSONRPC20Request.from_json(str_json) + self.assertTrue(isinstance(requests, JSONRPC20BatchRequest)) + requests = list(requests) + self.assertEqual(len(requests), 1) + r = requests[0] + self.assertTrue(isinstance(r, JSONRPC20Request)) + self.assertEqual(r.method, "add") + self.assertEqual(r.params, [1, 2]) + self.assertEqual(r._id, None) + self.assertFalse(r.is_notification) + + def test_response_iterator(self): + requests = JSONRPC20BatchRequest( + JSONRPC20Request("devide", {"num": 1, "denom": 2}, _id=1), + JSONRPC20Request("devide", {"num": 3, "denom": 2}, _id=2), + ) + for request in requests: + self.assertTrue(isinstance(request, JSONRPC20Request)) + self.assertEqual(request.method, "devide") + + +class TestJSONRPC20Response(unittest.TestCase): + + """ Test JSONRPC20Response functionality.""" + + def setUp(self): + self.response_success_params = { + "result": "", + "_id": 1, + } + self.response_error_params = { + "error": { + "code": 1, + "message": "error", + }, + "_id": 1, + } + + def test_correct_init(self): + """ Test object is created.""" + JSONRPC20Response(**self.response_success_params) + + def test_validation_incorrect_no_parameters(self): + with self.assertRaises(ValueError): + JSONRPC20Response() + + def test_validation_incorrect_result_and_error(self): + response = JSONRPC20Response(error={"code": 1, "message": ""}) + with self.assertRaises(ValueError): + response.result = "" + + def test_validation_error_correct(self): + JSONRPC20Response(**self.response_error_params) + + def test_validation_error_incorrect(self): + self.response_error_params["error"].update({"code": "str"}) + with self.assertRaises(ValueError): + JSONRPC20Response(**self.response_error_params) + + def test_validation_error_incorrect_no_code(self): + del self.response_error_params["error"]["code"] + with self.assertRaises(ValueError): + JSONRPC20Response(**self.response_error_params) + + def test_validation_error_incorrect_no_message(self): + del self.response_error_params["error"]["message"] + with self.assertRaises(ValueError): + JSONRPC20Response(**self.response_error_params) + + def test_validation_error_incorrect_message_not_str(self): + self.response_error_params["error"].update({"message": 0}) + with self.assertRaises(ValueError): + JSONRPC20Response(**self.response_error_params) + + def test_validation_id(self): + response = JSONRPC20Response(**self.response_success_params) + self.assertEqual(response._id, self.response_success_params["_id"]) + + def test_validation_id_incorrect_type(self): + response = JSONRPC20Response(**self.response_success_params) + + with self.assertRaises(ValueError): + response._id = [] + + with self.assertRaises(ValueError): + response._id = {} + + with self.assertRaises(ValueError): + response._id = 0.1 + + def test_data_result(self): + r = JSONRPC20Response(result="") + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "result": "", + "id": None, + }) + + def test_data_result_id_none(self): + r = JSONRPC20Response(result="", _id=None) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "result": "", + "id": None, + }) + + def test_data_result_id(self): + r = JSONRPC20Response(result="", _id=0) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "result": "", + "id": 0, + }) + + def test_data_error(self): + r = JSONRPC20Response(error={"code": 0, "message": ""}) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "error": { + "code": 0, + "message": "", + }, + "id": None, + }) + + def test_data_error_id_none(self): + r = JSONRPC20Response(error={"code": 0, "message": ""}, _id=None) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "error": { + "code": 0, + "message": "", + }, + "id": None, + }) + + def test_data_error_id(self): + r = JSONRPC20Response(error={"code": 0, "message": ""}, _id=0) + self.assertEqual(json.loads(r.json), r.data) + self.assertEqual(r.data, { + "jsonrpc": "2.0", + "error": { + "code": 0, + "message": "", + }, + "id": 0, + }) + + def test_data_setter(self): + response = JSONRPC20Response(**self.response_success_params) + with self.assertRaises(ValueError): + response.data = [] + + with self.assertRaises(ValueError): + response.data = "" + + with self.assertRaises(ValueError): + response.data = None + + +class TestJSONRPC20BatchResponse(unittest.TestCase): + + """ Test JSONRPC20BatchResponse functionality.""" + + def test_batch_response(self): + response = JSONRPC20BatchResponse( + JSONRPC20Response(result="result", _id=1), + JSONRPC20Response(error={"code": 0, "message": ""}, _id=2), + ) + self.assertEqual(json.loads(response.json), [ + {"result": "result", "id": 1, "jsonrpc": "2.0"}, + {"error": {"code": 0, "message": ""}, "id": 2, "jsonrpc": "2.0"}, + ]) + + def test_response_iterator(self): + responses = JSONRPC20BatchResponse( + JSONRPC20Response(result="result", _id=1), + JSONRPC20Response(result="result", _id=2), + ) + for response in responses: + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.result, "result") + + def test_batch_response_data(self): + response = JSONRPC20BatchResponse( + JSONRPC20Response(result="result", _id=1), + JSONRPC20Response(result="result", _id=2), + JSONRPC20Response(result="result"), + ) + self.assertEqual(response.data, [ + {"id": 1, "jsonrpc": "2.0", "result": "result"}, + {"id": 2, "jsonrpc": "2.0", "result": "result"}, + {"id": None, "jsonrpc": "2.0", "result": "result"}, + ]) diff --git a/jsonrpc/tests/test_jsonrpc_errors.py b/jsonrpc/tests/test_jsonrpc_errors.py new file mode 100644 index 000000000..abc559fe0 --- /dev/null +++ b/jsonrpc/tests/test_jsonrpc_errors.py @@ -0,0 +1,150 @@ +import json +import sys + +from ..exceptions import ( + JSONRPCError, + JSONRPCInternalError, + JSONRPCInvalidParams, + JSONRPCInvalidRequest, + JSONRPCMethodNotFound, + JSONRPCParseError, + JSONRPCServerError, + JSONRPCDispatchException, +) + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONRPCError(unittest.TestCase): + def setUp(self): + self.error_params = { + "code": 0, + "message": "", + } + + def test_correct_init(self): + """ Test object is created.""" + JSONRPCError(**self.error_params) + + def test_validation_incorrect_no_parameters(self): + with self.assertRaises(ValueError): + JSONRPCError() + + def test_code_validation_int(self): + self.error_params.update({"code": 32000}) + JSONRPCError(**self.error_params) + + def test_code_validation_no_code(self): + del self.error_params["code"] + with self.assertRaises(ValueError): + JSONRPCError(**self.error_params) + + def test_code_validation_str(self): + self.error_params.update({"code": "0"}) + with self.assertRaises(ValueError): + JSONRPCError(**self.error_params) + + def test_message_validation_str(self): + self.error_params.update({"message": ""}) + JSONRPCError(**self.error_params) + + def test_message_validation_none(self): + del self.error_params["message"] + with self.assertRaises(ValueError): + JSONRPCError(**self.error_params) + + def test_message_validation_int(self): + self.error_params.update({"message": 0}) + with self.assertRaises(ValueError): + JSONRPCError(**self.error_params) + + def test_data_validation_none(self): + self.error_params.update({"data": None}) + JSONRPCError(**self.error_params) + + def test_data_validation(self): + self.error_params.update({"data": {}}) + JSONRPCError(**self.error_params) + + self.error_params.update({"data": ""}) + JSONRPCError(**self.error_params) + + def test_json(self): + error = JSONRPCError(**self.error_params) + self.assertEqual( + json.loads(error.json), + self.error_params, + ) + + def test_from_json(self): + str_json = json.dumps({ + "code": 0, + "message": "", + "data": {}, + }) + + request = JSONRPCError.from_json(str_json) + self.assertTrue(isinstance(request, JSONRPCError)) + self.assertEqual(request.code, 0) + self.assertEqual(request.message, "") + self.assertEqual(request.data, {}) + + +class TestJSONRPCParseError(unittest.TestCase): + def test_code_message(self): + error = JSONRPCParseError() + self.assertEqual(error.code, -32700) + self.assertEqual(error.message, "Parse error") + self.assertEqual(error.data, None) + + +class TestJSONRPCServerError(unittest.TestCase): + def test_code_message(self): + error = JSONRPCServerError() + self.assertEqual(error.code, -32000) + self.assertEqual(error.message, "Server error") + self.assertEqual(error.data, None) + + +class TestJSONRPCInternalError(unittest.TestCase): + def test_code_message(self): + error = JSONRPCInternalError() + self.assertEqual(error.code, -32603) + self.assertEqual(error.message, "Internal error") + self.assertEqual(error.data, None) + + +class TestJSONRPCInvalidParams(unittest.TestCase): + def test_code_message(self): + error = JSONRPCInvalidParams() + self.assertEqual(error.code, -32602) + self.assertEqual(error.message, "Invalid params") + self.assertEqual(error.data, None) + + +class TestJSONRPCInvalidRequest(unittest.TestCase): + def test_code_message(self): + error = JSONRPCInvalidRequest() + self.assertEqual(error.code, -32600) + self.assertEqual(error.message, "Invalid Request") + self.assertEqual(error.data, None) + + +class TestJSONRPCMethodNotFound(unittest.TestCase): + def test_code_message(self): + error = JSONRPCMethodNotFound() + self.assertEqual(error.code, -32601) + self.assertEqual(error.message, "Method not found") + self.assertEqual(error.data, None) + + +class TestJSONRPCDispatchException(unittest.TestCase): + def test_code_message(self): + error = JSONRPCDispatchException(message="message", + code=400, data={"param": 1}) + self.assertEqual(error.error.code, 400) + self.assertEqual(error.error.message, "message") + self.assertEqual(error.error.data, {"param": 1}) diff --git a/jsonrpc/tests/test_manager.py b/jsonrpc/tests/test_manager.py new file mode 100644 index 000000000..bda108e1c --- /dev/null +++ b/jsonrpc/tests/test_manager.py @@ -0,0 +1,175 @@ +import sys + +from ..manager import JSONRPCResponseManager +from ..jsonrpc2 import ( + JSONRPC20BatchRequest, + JSONRPC20BatchResponse, + JSONRPC20Request, + JSONRPC20Response, +) +from ..jsonrpc1 import JSONRPC10Request, JSONRPC10Response +from ..exceptions import JSONRPCDispatchException + +if sys.version_info < (3, 3): + from mock import MagicMock +else: + from unittest.mock import MagicMock + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONRPCResponseManager(unittest.TestCase): + def setUp(self): + def raise_(e): + raise e + + self.long_time_method = MagicMock() + self.dispatcher = { + "add": sum, + "multiply": lambda a, b: a * b, + "list_len": len, + "101_base": lambda **kwargs: int("101", **kwargs), + "error": lambda: raise_(KeyError("error_explanation")), + "type_error": lambda: raise_(TypeError("TypeError inside method")), + "long_time_method": self.long_time_method, + "dispatch_error": lambda x: raise_( + JSONRPCDispatchException(code=4000, message="error", + data={"param": 1})), + } + + def test_dispatch_error(self): + request = JSONRPC20Request("dispatch_error", ["test"], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "error") + self.assertEqual(response.error["code"], 4000) + self.assertEqual(response.error["data"], {"param": 1}) + + def test_returned_type_response(self): + request = JSONRPC20Request("add", [[]], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + + def test_returned_type_butch_response(self): + request = JSONRPC20BatchRequest( + JSONRPC20Request("add", [[]], _id=0)) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20BatchResponse)) + + def test_returned_type_response_rpc10(self): + request = JSONRPC10Request("add", [[]], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC10Response)) + + def test_parse_error(self): + req = '{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Parse error") + self.assertEqual(response.error["code"], -32700) + + def test_invalid_request(self): + req = '{"jsonrpc": "2.0", "method": 1, "params": "bar"}' + response = JSONRPCResponseManager.handle(req, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid Request") + self.assertEqual(response.error["code"], -32600) + + def test_method_not_found(self): + request = JSONRPC20Request("does_not_exist", [[]], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Method not found") + self.assertEqual(response.error["code"], -32601) + + def test_invalid_params(self): + request = JSONRPC20Request("add", {"a": 0}, _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid params") + self.assertEqual(response.error["code"], -32602) + self.assertIn(response.error["data"]["message"], [ + 'sum() takes no keyword arguments', + "sum() got an unexpected keyword argument 'a'", + ]) + + def test_invalid_params_custom_function(self): + request = JSONRPC20Request("multiply", [0], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid params") + self.assertEqual(response.error["code"], -32602) + + request = JSONRPC20Request("multiply", [0, 1, 2], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid params") + self.assertEqual(response.error["code"], -32602) + + request = JSONRPC20Request("multiply", {"a": 1}, _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid params") + self.assertEqual(response.error["code"], -32602) + + request = JSONRPC20Request("multiply", {"a": 1, "b": 2, "c": 3}, _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid params") + self.assertEqual(response.error["code"], -32602) + + def test_server_error(self): + request = JSONRPC20Request("error", _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Server error") + self.assertEqual(response.error["code"], -32000) + self.assertEqual(response.error["data"]['type'], "KeyError") + self.assertEqual( + response.error["data"]['args'], ('error_explanation',)) + self.assertEqual( + response.error["data"]['message'], "'error_explanation'") + + def test_notification_calls_method(self): + request = JSONRPC20Request("long_time_method", is_notification=True) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertEqual(response, None) + self.long_time_method.assert_called_once_with() + + def test_notification_does_not_return_error_does_not_exist(self): + request = JSONRPC20Request("does_not_exist", is_notification=True) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertEqual(response, None) + + def test_notification_does_not_return_error_invalid_params(self): + request = JSONRPC20Request("add", {"a": 0}, is_notification=True) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertEqual(response, None) + + def test_notification_does_not_return_error(self): + request = JSONRPC20Request("error", is_notification=True) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertEqual(response, None) + + def test_type_error_inside_method(self): + request = JSONRPC20Request("type_error", _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Server error") + self.assertEqual(response.error["code"], -32000) + self.assertEqual(response.error["data"]['type'], "TypeError") + self.assertEqual( + response.error["data"]['args'], ('TypeError inside method',)) + self.assertEqual( + response.error["data"]['message'], 'TypeError inside method') + + def test_invalid_params_before_dispatcher_error(self): + request = JSONRPC20Request( + "dispatch_error", ["invalid", "params"], _id=0) + response = JSONRPCResponseManager.handle(request.json, self.dispatcher) + self.assertTrue(isinstance(response, JSONRPC20Response)) + self.assertEqual(response.error["message"], "Invalid params") + self.assertEqual(response.error["code"], -32602) diff --git a/jsonrpc/tests/test_pep3107.py b/jsonrpc/tests/test_pep3107.py new file mode 100644 index 000000000..6af14b729 --- /dev/null +++ b/jsonrpc/tests/test_pep3107.py @@ -0,0 +1,28 @@ +from ..manager import JSONRPCResponseManager + +import sys + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONRPCResponseManager(unittest.TestCase): + @unittest.skipIf(sys.version_info < (3, 5), "Test Py3.5+ functionality") + def test_typeerror_with_annotations(self): + """If a function has Python3 annotations and is called with improper + arguments, make sure the framework doesn't fail with inspect.getargspec + """ + from .py35_utils import distance + + dispatcher = { + "distance": distance, + } + + req = '{"jsonrpc": "2.0", "method": "distance", "params": [], "id": 1}' + result = JSONRPCResponseManager.handle(req, dispatcher) + + # Make sure this returns JSONRPCInvalidParams rather than raising + # UnboundLocalError + self.assertEqual(result.error['code'], -32602) diff --git a/jsonrpc/tests/test_utils.py b/jsonrpc/tests/test_utils.py new file mode 100644 index 000000000..9d0c76d81 --- /dev/null +++ b/jsonrpc/tests/test_utils.py @@ -0,0 +1,130 @@ +""" Test utility functionality.""" +from ..utils import JSONSerializable, DatetimeDecimalEncoder, is_invalid_params + +import datetime +import decimal +import json +import sys + +if sys.version_info < (3, 3): + from mock import patch +else: + from unittest.mock import patch + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class TestJSONSerializable(unittest.TestCase): + + """ Test JSONSerializable functionality.""" + + def setUp(self): + class A(JSONSerializable): + @property + def json(self): + pass + + self._class = A + + def test_abstract_class(self): + with self.assertRaises(TypeError): + JSONSerializable() + + self._class() + + def test_definse_serialize_deserialize(self): + """ Test classmethods of inherited class.""" + self.assertEqual(self._class.serialize({}), "{}") + self.assertEqual(self._class.deserialize("{}"), {}) + + def test_from_json(self): + self.assertTrue(isinstance(self._class.from_json('{}'), self._class)) + + def test_from_json_incorrect(self): + with self.assertRaises(ValueError): + self._class.from_json('[]') + + +class TestDatetimeDecimalEncoder(unittest.TestCase): + + """ Test DatetimeDecimalEncoder functionality.""" + + def test_date_encoder(self): + obj = datetime.date.today() + + with self.assertRaises(TypeError): + json.dumps(obj) + + self.assertEqual( + json.dumps(obj, cls=DatetimeDecimalEncoder), + '"{0}"'.format(obj.isoformat()), + ) + + def test_datetime_encoder(self): + obj = datetime.datetime.now() + + with self.assertRaises(TypeError): + json.dumps(obj) + + self.assertEqual( + json.dumps(obj, cls=DatetimeDecimalEncoder), + '"{0}"'.format(obj.isoformat()), + ) + + def test_decimal_encoder(self): + obj = decimal.Decimal('0.1') + + with self.assertRaises(TypeError): + json.dumps(obj) + + result = json.dumps(obj, cls=DatetimeDecimalEncoder) + self.assertTrue(isinstance(result, str)) + self.assertEqual(float(result), float(0.1)) + + def test_default(self): + encoder = DatetimeDecimalEncoder() + with patch.object(json.JSONEncoder, 'default') as json_default: + encoder.default("") + + self.assertEqual(json_default.call_count, 1) + + +class TestUtils(unittest.TestCase): + + """ Test utils functions.""" + + def test_is_invalid_params_builtin(self): + self.assertTrue(is_invalid_params(sum, 0, 0)) + # NOTE: builtin functions could not be recognized by inspect.isfunction + # It would raise TypeError if parameters are incorrect already. + # self.assertFalse(is_invalid_params(sum, [0, 0])) # <- fails + + def test_is_invalid_params_args(self): + self.assertTrue(is_invalid_params(lambda a, b: None, 0)) + self.assertTrue(is_invalid_params(lambda a, b: None, 0, 1, 2)) + + def test_is_invalid_params_kwargs(self): + self.assertTrue(is_invalid_params(lambda a: None, **{})) + self.assertTrue(is_invalid_params(lambda a: None, **{"a": 0, "b": 1})) + + def test_invalid_params_correct(self): + self.assertFalse(is_invalid_params(lambda: None)) + self.assertFalse(is_invalid_params(lambda a: None, 0)) + self.assertFalse(is_invalid_params(lambda a, b=0: None, 0)) + self.assertFalse(is_invalid_params(lambda a, b=0: None, 0, 0)) + + def test_is_invalid_params_mixed(self): + self.assertFalse(is_invalid_params(lambda a, b: None, 0, **{"b": 1})) + self.assertFalse(is_invalid_params( + lambda a, b, c=0: None, 0, **{"b": 1})) + + def test_is_invalid_params_py2(self): + with patch('jsonrpc.utils.sys') as mock_sys: + mock_sys.version_info = (2, 7) + with patch('jsonrpc.utils.is_invalid_params_py2') as mock_func: + is_invalid_params(lambda a: None, 0) + + assert mock_func.call_count == 1 diff --git a/jsonrpc/utils.py b/jsonrpc/utils.py new file mode 100644 index 000000000..cdac317b2 --- /dev/null +++ b/jsonrpc/utils.py @@ -0,0 +1,135 @@ +""" Utility functions for package.""" +from abc import ABCMeta, abstractmethod +import datetime +import decimal +import inspect +import json +import sys + +from . import six + + +class JSONSerializable(six.with_metaclass(ABCMeta, object)): + + """ Common functionality for json serializable objects.""" + + serialize = staticmethod(json.dumps) + deserialize = staticmethod(json.loads) + + @abstractmethod + def json(self): + raise NotImplementedError() + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + + if not isinstance(data, dict): + raise ValueError("data should be dict") + + return cls(**data) + + +class DatetimeDecimalEncoder(json.JSONEncoder): + + """ Encoder for datetime and decimal serialization. + + Usage: json.dumps(object, cls=DatetimeDecimalEncoder) + NOTE: _iterencode does not work + + """ + + def default(self, o): + """ Encode JSON. + + :return str: A JSON encoded string + + """ + if isinstance(o, decimal.Decimal): + return float(o) + + if isinstance(o, (datetime.datetime, datetime.date)): + return o.isoformat() + + return json.JSONEncoder.default(self, o) + + +def is_invalid_params_py2(func, *args, **kwargs): + """ Check, whether function 'func' accepts parameters 'args', 'kwargs'. + + NOTE: Method is called after funct(*args, **kwargs) generated TypeError, + it is aimed to destinguish TypeError because of invalid parameters from + TypeError from inside the function. + + .. versionadded: 1.9.0 + + """ + funcargs, varargs, varkwargs, defaults = inspect.getargspec(func) + + unexpected = set(kwargs.keys()) - set(funcargs) + if len(unexpected) > 0: + return True + + params = [funcarg for funcarg in funcargs if funcarg not in kwargs] + funcargs_required = funcargs[:-len(defaults)] \ + if defaults is not None \ + else funcargs + params_required = [ + funcarg for funcarg in funcargs_required + if funcarg not in kwargs + ] + + return not (len(params_required) <= len(args) <= len(params)) + + +def is_invalid_params_py3(func, *args, **kwargs): + """ + Use inspect.signature instead of inspect.getargspec or + inspect.getfullargspec (based on inspect.signature itself) as it provides + more information about function parameters. + + .. versionadded: 1.11.2 + + """ + signature = inspect.signature(func) + parameters = signature.parameters + + unexpected = set(kwargs.keys()) - set(parameters.keys()) + if len(unexpected) > 0: + return True + + params = [ + parameter for name, parameter in parameters.items() + if name not in kwargs + ] + params_required = [ + param for param in params + if param.default is param.empty + ] + + return not (len(params_required) <= len(args) <= len(params)) + + +def is_invalid_params(func, *args, **kwargs): + """ + Method: + Validate pre-defined criteria, if any is True - function is invalid + 0. func should be callable + 1. kwargs should not have unexpected keywords + 2. remove kwargs.keys from func.parameters + 3. number of args should be <= remaining func.parameters + 4. number of args should be >= remaining func.parameters less default + """ + # For builtin functions inspect.getargspec(funct) return error. If builtin + # function generates TypeError, it is because of wrong parameters. + if not inspect.isfunction(func): + return True + + if sys.version_info >= (3, 3): + return is_invalid_params_py3(func, *args, **kwargs) + else: + # NOTE: use Python2 method for Python 3.2 as well. Starting from Python + # 3.3 it is recommended to use inspect.signature instead. + # In Python 3.0 - 3.2 inspect.getfullargspec is preferred but these + # versions are almost not supported. Users should consider upgrading. + return is_invalid_params_py2(func, *args, **kwargs) diff --git a/websocket/__init__.py b/websocket/__init__.py new file mode 100644 index 000000000..7c3154da2 --- /dev/null +++ b/websocket/__init__.py @@ -0,0 +1,29 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +from ._abnf import * +from ._app import WebSocketApp +from ._core import * +from ._exceptions import * +from ._logging import * +from ._socket import * + +__version__ = "0.55.0" diff --git a/websocket/_abnf.py b/websocket/_abnf.py new file mode 100644 index 000000000..a0000fa1c --- /dev/null +++ b/websocket/_abnf.py @@ -0,0 +1,447 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import array +import os +import struct + +import six + +from ._exceptions import * +from ._utils import validate_utf8 +from threading import Lock + +try: + if six.PY3: + import numpy + else: + numpy = None +except ImportError: + numpy = None + +try: + # If wsaccel is available we use compiled routines to mask data. + if not numpy: + from wsaccel.xormask import XorMaskerSimple + + def _mask(_m, _d): + return XorMaskerSimple(_m).process(_d) +except ImportError: + # wsaccel is not available, we rely on python implementations. + def _mask(_m, _d): + for i in range(len(_d)): + _d[i] ^= _m[i % 4] + + if six.PY3: + return _d.tobytes() + else: + return _d.tostring() + + +__all__ = [ + 'ABNF', 'continuous_frame', 'frame_buffer', + 'STATUS_NORMAL', + 'STATUS_GOING_AWAY', + 'STATUS_PROTOCOL_ERROR', + 'STATUS_UNSUPPORTED_DATA_TYPE', + 'STATUS_STATUS_NOT_AVAILABLE', + 'STATUS_ABNORMAL_CLOSED', + 'STATUS_INVALID_PAYLOAD', + 'STATUS_POLICY_VIOLATION', + 'STATUS_MESSAGE_TOO_BIG', + 'STATUS_INVALID_EXTENSION', + 'STATUS_UNEXPECTED_CONDITION', + 'STATUS_BAD_GATEWAY', + 'STATUS_TLS_HANDSHAKE_ERROR', +] + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_BAD_GATEWAY = 1014 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +VALID_CLOSE_STATUS = ( + STATUS_NORMAL, + STATUS_GOING_AWAY, + STATUS_PROTOCOL_ERROR, + STATUS_UNSUPPORTED_DATA_TYPE, + STATUS_INVALID_PAYLOAD, + STATUS_POLICY_VIOLATION, + STATUS_MESSAGE_TOO_BIG, + STATUS_INVALID_EXTENSION, + STATUS_UNEXPECTED_CONDITION, + STATUS_BAD_GATEWAY, +) + + +class ABNF(object): + """ + ABNF frame class. + see http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threshold. + LENGTH_7 = 0x7e + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. + please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + if data is None: + data = "" + self.data = data + self.get_mask_key = os.urandom + + def validate(self, skip_utf8_validation=False): + """ + validate the ABNF frame. + skip_utf8_validation: skip utf8 validation. + """ + if self.rsv1 or self.rsv2 or self.rsv3: + raise WebSocketProtocolException("rsv is not implemented, yet") + + if self.opcode not in ABNF.OPCODES: + raise WebSocketProtocolException("Invalid opcode %r", self.opcode) + + if self.opcode == ABNF.OPCODE_PING and not self.fin: + raise WebSocketProtocolException("Invalid ping frame.") + + if self.opcode == ABNF.OPCODE_CLOSE: + l = len(self.data) + if not l: + return + if l == 1 or l >= 126: + raise WebSocketProtocolException("Invalid close frame.") + if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]): + raise WebSocketProtocolException("Invalid close frame.") + + code = 256 * \ + six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2]) + if not self._is_valid_close_status(code): + raise WebSocketProtocolException("Invalid close opcode.") + + @staticmethod + def _is_valid_close_status(code): + return code in VALID_CLOSE_STATUS or (3000 <= code < 5000) + + def __str__(self): + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode, fin=1): + """ + create frame to send text, binary and other data. + + data: data to send. This is string value(byte array). + if opcode is OPCODE_TEXT and this value is unicode, + data value is converted into unicode string, automatically. + + opcode: operation code. please see OPCODE_XXX. + + fin: fin flag. if set to 0, create continue fragmentation. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(fin, 0, 0, 0, opcode, 1, data) + + def format(self): + """ + format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 + | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 + | self.opcode) + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask << 7 | length) + frame_header = six.b(frame_header) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e) + frame_header = six.b(frame_header) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f) + frame_header = six.b(frame_header) + frame_header += struct.pack("!Q", length) + + if not self.mask: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + + if isinstance(mask_key, six.text_type): + mask_key = mask_key.encode('utf-8') + + return mask_key + s + + @staticmethod + def mask(mask_key, data): + """ + mask or unmask data. Just do xor for each byte + + mask_key: 4 byte string(byte). + + data: data to mask/unmask. + """ + if data is None: + data = "" + + if isinstance(mask_key, six.text_type): + mask_key = six.b(mask_key) + + if isinstance(data, six.text_type): + data = six.b(data) + + if numpy: + origlen = len(data) + _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0] + + # We need data to be a multiple of four... + data += bytes(" " * (4 - (len(data) % 4)), "us-ascii") + a = numpy.frombuffer(data, dtype="uint32") + masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32") + if len(data) > origlen: + return masked.tobytes()[:origlen] + return masked.tobytes() + else: + _m = array.array("B", mask_key) + _d = array.array("B", data) + return _mask(_m, _d) + + +class frame_buffer(object): + _HEADER_MASK_INDEX = 5 + _HEADER_LENGTH_INDEX = 6 + + def __init__(self, recv_fn, skip_utf8_validation): + self.recv = recv_fn + self.skip_utf8_validation = skip_utf8_validation + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self.recv_buffer = [] + self.clear() + self.lock = Lock() + + def clear(self): + self.header = None + self.length = None + self.mask = None + + def has_received_header(self): + return self.header is None + + def recv_header(self): + header = self.recv_strict(2) + b1 = header[0] + + if six.PY2: + b1 = ord(b1) + + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = header[1] + + if six.PY2: + b2 = ord(b2) + + has_mask = b2 >> 7 & 1 + length_bits = b2 & 0x7f + + self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits) + + def has_mask(self): + if not self.header: + return False + return self.header[frame_buffer._HEADER_MASK_INDEX] + + def has_received_length(self): + return self.length is None + + def recv_length(self): + bits = self.header[frame_buffer._HEADER_LENGTH_INDEX] + length_bits = bits & 0x7f + if length_bits == 0x7e: + v = self.recv_strict(2) + self.length = struct.unpack("!H", v)[0] + elif length_bits == 0x7f: + v = self.recv_strict(8) + self.length = struct.unpack("!Q", v)[0] + else: + self.length = length_bits + + def has_received_mask(self): + return self.mask is None + + def recv_mask(self): + self.mask = self.recv_strict(4) if self.has_mask() else "" + + def recv_frame(self): + + with self.lock: + # Header + if self.has_received_header(): + self.recv_header() + (fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header + + # Frame length + if self.has_received_length(): + self.recv_length() + length = self.length + + # Mask + if self.has_received_mask(): + self.recv_mask() + mask = self.mask + + # Payload + payload = self.recv_strict(length) + if has_mask: + payload = ABNF.mask(mask, payload) + + # Reset for next frame + self.clear() + + frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + frame.validate(self.skip_utf8_validation) + + return frame + + def recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self.recv_buffer) + while shortage > 0: + # Limit buffer size that we pass to socket.recv() to avoid + # fragmenting the heap -- the number of bytes recv() actually + # reads is limited by socket buffer and is relatively small, + # yet passing large numbers repeatedly causes lots of large + # buffers allocated and then shrunk, which results in + # fragmentation. + bytes_ = self.recv(min(16384, shortage)) + self.recv_buffer.append(bytes_) + shortage -= len(bytes_) + + unified = six.b("").join(self.recv_buffer) + + if shortage == 0: + self.recv_buffer = [] + return unified + else: + self.recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + +class continuous_frame(object): + + def __init__(self, fire_cont_frame, skip_utf8_validation): + self.fire_cont_frame = fire_cont_frame + self.skip_utf8_validation = skip_utf8_validation + self.cont_data = None + self.recving_frames = None + + def validate(self, frame): + if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT: + raise WebSocketProtocolException("Illegal frame") + if self.recving_frames and \ + frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + raise WebSocketProtocolException("Illegal frame") + + def add(self, frame): + if self.cont_data: + self.cont_data[1] += frame.data + else: + if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + self.recving_frames = frame.opcode + self.cont_data = [frame.opcode, frame.data] + + if frame.fin: + self.recving_frames = None + + def is_fire(self, frame): + return frame.fin or self.fire_cont_frame + + def extract(self, frame): + data = self.cont_data + self.cont_data = None + frame.data = data[1] + if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data): + raise WebSocketPayloadException( + "cannot decode: " + repr(frame.data)) + + return [data[0], frame] diff --git a/websocket/_app.py b/websocket/_app.py new file mode 100644 index 000000000..81aa1fcd9 --- /dev/null +++ b/websocket/_app.py @@ -0,0 +1,351 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + +""" +WebSocketApp provides higher level APIs. +""" +import inspect +import select +import sys +import threading +import time +import traceback + +import six + +from ._abnf import ABNF +from ._core import WebSocket, getdefaulttimeout +from ._exceptions import * +from . import _logging + + +__all__ = ["WebSocketApp"] + +class Dispatcher: + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, read_callback, check_callback): + while self.app.sock.connected: + r, w, e = select.select( + (self.app.sock.sock, ), (), (), self.ping_timeout) + if r: + if not read_callback(): + break + check_callback() + +class SSLDispacther: + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, read_callback, check_callback): + while self.app.sock.connected: + r = self.select() + if r: + if not read_callback(): + break + check_callback() + + def select(self): + sock = self.app.sock.sock + if sock.pending(): + return [sock,] + + r, w, e = select.select((sock, ), (), (), self.ping_timeout) + return r + +class WebSocketApp(object): + """ + Higher level of APIs are provided. + The interface is like JavaScript WebSocket object. + """ + + def __init__(self, url, header=None, + on_open=None, on_message=None, on_error=None, + on_close=None, on_ping=None, on_pong=None, + on_cont_message=None, + keep_running=True, get_mask_key=None, cookie=None, + subprotocols=None, + on_data=None): + """ + url: websocket url. + header: custom header for websocket handshake. + on_open: callable object which is called at opening websocket. + this function has one argument. The argument is this class object. + on_message: callable object which is called when received data. + on_message has 2 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + on_error: callable object which is called when we get error. + on_error has 2 arguments. + The 1st argument is this class object. + The 2nd argument is exception object. + on_close: callable object which is called when closed the connection. + this function has one argument. The argument is this class object. + on_cont_message: callback object which is called when receive continued + frame data. + on_cont_message has 3 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is continue flag. if 0, the data continue + to next frame data + on_data: callback object which is called when a message received. + This is called before on_message or on_cont_message, + and then on_message or on_cont_message is called. + on_data has 4 argument. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came. + The 4th argument is continue flag. if 0, the data continue + keep_running: this parameter is obsolete and ignored. + get_mask_key: a callable to produce new mask keys, + see the WebSocket.set_mask_key's docstring for more information + subprotocols: array of available sub protocols. default is None. + """ + self.url = url + self.header = header if header is not None else [] + self.cookie = cookie + + self.on_open = on_open + self.on_message = on_message + self.on_data = on_data + self.on_error = on_error + self.on_close = on_close + self.on_ping = on_ping + self.on_pong = on_pong + self.on_cont_message = on_cont_message + self.keep_running = False + self.get_mask_key = get_mask_key + self.sock = None + self.last_ping_tm = 0 + self.last_pong_tm = 0 + self.subprotocols = subprotocols + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message. + data: message to send. If you set opcode to OPCODE_TEXT, + data must be utf-8 string or unicode. + opcode: operation code of data. default is OPCODE_TEXT. + """ + + if not self.sock or self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + def close(self, **kwargs): + """ + close websocket connection. + """ + self.keep_running = False + if self.sock: + self.sock.close(**kwargs) + self.sock = None + + def _send_ping(self, interval, event): + while not event.wait(interval): + self.last_ping_tm = time.time() + if self.sock: + try: + self.sock.ping() + except Exception as ex: + _logging.warning("send_ping routine terminated: {}".format(ex)) + break + + def run_forever(self, sockopt=None, sslopt=None, + ping_interval=0, ping_timeout=None, + http_proxy_host=None, http_proxy_port=None, + http_no_proxy=None, http_proxy_auth=None, + skip_utf8_validation=False, + host=None, origin=None, dispatcher=None, + suppress_origin = False, proxy_type=None): + """ + run event loop for WebSocket framework. + This loop is infinite loop and is alive during websocket is available. + sockopt: values for socket.setsockopt. + sockopt must be tuple + and each element is argument of sock.setsockopt. + sslopt: ssl socket optional dict. + ping_interval: automatically send "ping" command + every specified period(second) + if set to 0, not send automatically. + ping_timeout: timeout(second) if the pong message is not received. + http_proxy_host: http proxy host name. + http_proxy_port: http proxy port. If not set, set to 80. + http_no_proxy: host names, which doesn't use proxy. + skip_utf8_validation: skip utf8 validation. + host: update host header. + origin: update origin header. + dispatcher: customize reading data from socket. + suppress_origin: suppress outputting origin header. + + Returns + ------- + False if caught KeyboardInterrupt + True if other exception was raised during a loop + """ + + if ping_timeout is not None and ping_timeout <= 0: + ping_timeout = None + if ping_timeout and ping_interval and ping_interval <= ping_timeout: + raise WebSocketException("Ensure ping_interval > ping_timeout") + if not sockopt: + sockopt = [] + if not sslopt: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + self.keep_running = True + self.last_ping_tm = 0 + self.last_pong_tm = 0 + + def teardown(close_frame=None): + """ + Tears down the connection. + If close_frame is set, we will invoke the on_close handler with the + statusCode and reason from there. + """ + if thread and thread.isAlive(): + event.set() + thread.join() + self.keep_running = False + if self.sock: + self.sock.close() + close_args = self._get_close_args( + close_frame.data if close_frame else None) + self._callback(self.on_close, *close_args) + self.sock = None + + try: + self.sock = WebSocket( + self.get_mask_key, sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=self.on_cont_message is not None, + skip_utf8_validation=skip_utf8_validation, + enable_multithread=True if ping_interval else False) + self.sock.settimeout(getdefaulttimeout()) + self.sock.connect( + self.url, header=self.header, cookie=self.cookie, + http_proxy_host=http_proxy_host, + http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy, + http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols, + host=host, origin=origin, suppress_origin=suppress_origin, + proxy_type=proxy_type) + if not dispatcher: + dispatcher = self.create_dispatcher(ping_timeout) + + self._callback(self.on_open) + + if ping_interval: + event = threading.Event() + thread = threading.Thread( + target=self._send_ping, args=(ping_interval, event)) + thread.setDaemon(True) + thread.start() + + def read(): + if not self.keep_running: + return teardown() + + op_code, frame = self.sock.recv_data_frame(True) + if op_code == ABNF.OPCODE_CLOSE: + return teardown(frame) + elif op_code == ABNF.OPCODE_PING: + self._callback(self.on_ping, frame.data) + elif op_code == ABNF.OPCODE_PONG: + self.last_pong_tm = time.time() + self._callback(self.on_pong, frame.data) + elif op_code == ABNF.OPCODE_CONT and self.on_cont_message: + self._callback(self.on_data, frame.data, + frame.opcode, frame.fin) + self._callback(self.on_cont_message, + frame.data, frame.fin) + else: + data = frame.data + if six.PY3 and op_code == ABNF.OPCODE_TEXT: + data = data.decode("utf-8") + self._callback(self.on_data, data, frame.opcode, True) + self._callback(self.on_message, data) + + return True + + def check(): + if (ping_timeout): + has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout + has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0 + has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout + + if (self.last_ping_tm + and has_timeout_expired + and (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)): + raise WebSocketTimeoutException("ping/pong timed out") + return True + + dispatcher.read(self.sock.sock, read, check) + except (Exception, KeyboardInterrupt, SystemExit) as e: + self._callback(self.on_error, e) + if isinstance(e, SystemExit): + # propagate SystemExit further + raise + teardown() + return not isinstance(e, KeyboardInterrupt) + + def create_dispatcher(self, ping_timeout): + timeout = ping_timeout or 10 + if self.sock.is_ssl(): + return SSLDispacther(self, timeout) + + return Dispatcher(self, timeout) + + def _get_close_args(self, data): + """ this functions extracts the code, reason from the close body + if they exists, and if the self.on_close except three arguments """ + # if the on_close callback is "old", just return empty list + if sys.version_info < (3, 0): + if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3: + return [] + else: + if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3: + return [] + + if data and len(data) >= 2: + code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2]) + reason = data[2:].decode('utf-8') + return [code, reason] + + return [None, None] + + def _callback(self, callback, *args): + if callback: + try: + if inspect.ismethod(callback): + callback(*args) + else: + callback(self, *args) + + except Exception as e: + _logging.error("error from callback {}: {}".format(callback, e)) + if _logging.isEnabledForDebug(): + _, _, tb = sys.exc_info() + traceback.print_tb(tb) diff --git a/websocket/_cookiejar.py b/websocket/_cookiejar.py new file mode 100644 index 000000000..3efeb0fd2 --- /dev/null +++ b/websocket/_cookiejar.py @@ -0,0 +1,52 @@ +try: + import Cookie +except: + import http.cookies as Cookie + + +class SimpleCookieJar(object): + def __init__(self): + self.jar = dict() + + def add(self, set_cookie): + if set_cookie: + try: + simpleCookie = Cookie.SimpleCookie(set_cookie) + except: + simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie() + cookie.update(simpleCookie) + self.jar[domain.lower()] = cookie + + def set(self, set_cookie): + if set_cookie: + try: + simpleCookie = Cookie.SimpleCookie(set_cookie) + except: + simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + self.jar[domain.lower()] = simpleCookie + + def get(self, host): + if not host: + return "" + + cookies = [] + for domain, simpleCookie in self.jar.items(): + host = host.lower() + if host.endswith(domain) or host == domain[1:]: + cookies.append(self.jar.get(domain)) + + return "; ".join(filter(None, ["%s=%s" % (k, v.value) for cookie in filter(None, sorted(cookies)) for k, v in + sorted(cookie.items())])) diff --git a/websocket/_core.py b/websocket/_core.py new file mode 100644 index 000000000..0f914c21e --- /dev/null +++ b/websocket/_core.py @@ -0,0 +1,515 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +from __future__ import print_function + +import socket +import struct +import threading +import time + +import six + +# websocket modules +from ._abnf import * +from ._exceptions import * +from ._handshake import * +from ._http import * +from ._logging import * +from ._socket import * +from ._ssl_compat import * +from ._utils import * + +__all__ = ['WebSocket', 'create_connection'] + +""" +websocket python client. +========================= + +This version support only hybi-13. +Please see http://tools.ietf.org/html/rfc6455 for protocol. +""" + + +class WebSocket(object): + """ + Low level WebSocket interface. + This class is based on + The WebSocket protocol draft-hixie-thewebsocketprotocol-76 + http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + We can connect to the websocket server and send/receive data. + The following example is an echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + get_mask_key: a callable to produce new mask keys, see the set_mask_key + function's docstring for more details + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setsockopt. + sslopt: dict object for ssl socket option. + fire_cont_frame: fire recv event for each cont frame. default is False + enable_multithread: if set to True, lock send method. + skip_utf8_validation: skip utf8 validation. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None, + fire_cont_frame=False, enable_multithread=False, + skip_utf8_validation=False, **_): + """ + Initialize WebSocket object. + """ + self.sock_opt = sock_opt(sockopt, sslopt) + self.handshake_response = None + self.sock = None + + self.connected = False + self.get_mask_key = get_mask_key + # These buffer over the build-up of a single frame. + self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation) + self.cont_frame = continuous_frame( + fire_cont_frame, skip_utf8_validation) + + if enable_multithread: + self.lock = threading.Lock() + self.readlock = threading.Lock() + else: + self.lock = NoLock() + self.readlock = NoLock() + + def __iter__(self): + """ + Allow iteration over websocket, implying sequential `recv` executions. + """ + while True: + yield self.recv() + + def __next__(self): + return self.recv() + + def next(self): + return self.__next__() + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + set function to create musk key. You can customize mask key generator. + Mainly, this is for testing purpose. + + func: callable object. the func takes 1 argument as integer. + The argument means length of mask key. + This func must return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout(second). + """ + return self.sock_opt.timeout + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + timeout: timeout time(second). + """ + self.sock_opt.timeout = timeout + if self.sock: + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def getsubprotocol(self): + """ + get subprotocol + """ + if self.handshake_response: + return self.handshake_response.subprotocol + else: + return None + + subprotocol = property(getsubprotocol) + + def getstatus(self): + """ + get handshake status + """ + if self.handshake_response: + return self.handshake_response.status + else: + return None + + status = property(getstatus) + + def getheaders(self): + """ + get handshake response header + """ + if self.handshake_response: + return self.handshake_response.headers + else: + return None + + def is_ssl(self): + return isinstance(self.sock, ssl.SSLSocket) + + headers = property(getheaders) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. + ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + options: "header" -> custom http header list or dict. + "cookie" -> cookie value. + "origin" -> custom origin url. + "suppress_origin" -> suppress outputting origin header. + "host" -> custom host header string. + "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. If not set, set to 80. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "redirect_limit" -> number of redirects to follow. + "subprotocols" - array of available sub protocols. + default is None. + "socket" - pre-initialized stream socket. + + """ + # FIXME: "subprotocols" are getting lost, not passed down + # FIXME: "header", "cookie", "origin" and "host" too + self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout) + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + + try: + self.handshake_response = handshake(self.sock, *addrs, **options) + for attempt in range(options.pop('redirect_limit', 3)): + if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES: + url = self.handshake_response.headers['location'] + self.sock.close() + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + self.handshake_response = handshake(self.sock, *addrs, **options) + self.connected = True + except: + if self.sock: + self.sock.close() + self.sock = None + raise + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + payload: Payload must be utf-8 string or unicode, + if the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array) + + opcode: operation code to send. Please see OPCODE_XXX. + """ + + frame = ABNF.create_frame(payload, opcode) + return self.send_frame(frame) + + def send_frame(self, frame): + """ + Send the data frame. + + frame: frame data created by ABNF.create_frame + + >>> ws = create_connection("ws://echo.websocket.org/") + >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1) + >>> ws.send_frame(frame) + + """ + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + trace("send: " + repr(data)) + + with self.lock: + while data: + l = self._send(data) + data = data[l:] + + return length + + def send_binary(self, payload): + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + send ping data. + + payload: data payload to send server. + """ + if isinstance(payload, six.text_type): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload): + """ + send pong data. + + payload: data payload to send server. + """ + if isinstance(payload, six.text_type): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + return value: string(byte array) value. + """ + with self.readlock: + opcode, data = self.recv_data() + if six.PY3 and opcode == ABNF.OPCODE_TEXT: + return data.decode("utf-8") + elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY: + return data + else: + return '' + + def recv_data(self, control_frame=False): + """ + Receive data with operation code. + + control_frame: a boolean flag indicating whether to return control frame + data, defaults to False + + return value: tuple of operation code and string(byte array) value. + """ + opcode, frame = self.recv_data_frame(control_frame) + return opcode, frame.data + + def recv_data_frame(self, control_frame=False): + """ + Receive data with operation code. + + control_frame: a boolean flag indicating whether to return control frame + data, defaults to False + + return value: tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketProtocolException( + "Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + self.cont_frame.validate(frame) + self.cont_frame.add(frame) + + if self.cont_frame.is_fire(frame): + return self.cont_frame.extract(frame) + + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PING: + if len(frame.data) < 126: + self.pong(frame.data) + else: + raise WebSocketProtocolException( + "Ping message is too long") + if control_frame: + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PONG: + if control_frame: + return frame.opcode, frame + + def recv_frame(self): + """ + receive data as frame from server. + + return value: ABNF frame object. + """ + return self.frame_buffer.recv_frame() + + def send_close(self, status=STATUS_NORMAL, reason=six.b("")): + """ + send close data to the server. + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string or bytes. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.connected = False + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3): + """ + Close Websocket object + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + + timeout: timeout until receive a close frame. + If None, it will wait forever until receive a close frame. + """ + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.connected = False + self.send(struct.pack('!H', status) + + reason, ABNF.OPCODE_CLOSE) + sock_timeout = self.sock.gettimeout() + self.sock.settimeout(timeout) + start_time = time.time() + while timeout is None or time.time() - start_time < timeout: + try: + frame = self.recv_frame() + if frame.opcode != ABNF.OPCODE_CLOSE: + continue + if isEnabledForError(): + recv_status = struct.unpack("!H", frame.data[0:2])[0] + if recv_status != STATUS_NORMAL: + error("close status: " + repr(recv_status)) + break + except: + break + self.sock.settimeout(sock_timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + self.shutdown() + + def abort(self): + """ + Low-level asynchronous abort, wakes up other threads that are waiting in recv_* + """ + if self.connected: + self.sock.shutdown(socket.SHUT_RDWR) + + def shutdown(self): + """close socket, immediately.""" + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + + def _send(self, data): + return send(self.sock, data) + + def _recv(self, bufsize): + try: + return recv(self.sock, bufsize) + except WebSocketConnectionClosedException: + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + raise + + +def create_connection(url, timeout=None, class_=WebSocket, **options): + """ + connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, + the global default timeout setting returned by getdefauttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + class_: class to instantiate when creating the connection. It has to implement + settimeout and connect. It's __init__ should be compatible with + WebSocket.__init__, i.e. accept all of it's kwargs. + options: "header" -> custom http header list or dict. + "cookie" -> cookie value. + "origin" -> custom origin url. + "suppress_origin" -> suppress outputting origin header. + "host" -> custom host header string. + "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. If not set, set to 80. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "enable_multithread" -> enable lock for multithread. + "redirect_limit" -> number of redirects to follow. + "sockopt" -> socket options + "sslopt" -> ssl option + "subprotocols" - array of available sub protocols. + default is None. + "skip_utf8_validation" - skip utf8 validation. + "socket" - pre-initialized stream socket. + """ + sockopt = options.pop("sockopt", []) + sslopt = options.pop("sslopt", {}) + fire_cont_frame = options.pop("fire_cont_frame", False) + enable_multithread = options.pop("enable_multithread", False) + skip_utf8_validation = options.pop("skip_utf8_validation", False) + websock = class_(sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=fire_cont_frame, + enable_multithread=enable_multithread, + skip_utf8_validation=skip_utf8_validation, **options) + websock.settimeout(timeout if timeout is not None else getdefaulttimeout()) + websock.connect(url, **options) + return websock diff --git a/websocket/_exceptions.py b/websocket/_exceptions.py new file mode 100644 index 000000000..b7a61d3f6 --- /dev/null +++ b/websocket/_exceptions.py @@ -0,0 +1,87 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + + +""" +define websocket exceptions +""" + + +class WebSocketException(Exception): + """ + websocket exception class. + """ + pass + + +class WebSocketProtocolException(WebSocketException): + """ + If the websocket protocol is invalid, this exception will be raised. + """ + pass + + +class WebSocketPayloadException(WebSocketException): + """ + If the websocket payload is invalid, this exception will be raised. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + + +class WebSocketProxyException(WebSocketException): + """ + WebSocketProxyException will be raised when proxy error occurred. + """ + pass + + +class WebSocketBadStatusException(WebSocketException): + """ + WebSocketBadStatusException will be raised when we get bad handshake status code. + """ + + def __init__(self, message, status_code, status_message=None, resp_headers=None): + msg = message % (status_code, status_message) + super(WebSocketBadStatusException, self).__init__(msg) + self.status_code = status_code + self.resp_headers = resp_headers + +class WebSocketAddressException(WebSocketException): + """ + If the websocket address info cannot be found, this exception will be raised. + """ + pass diff --git a/websocket/_handshake.py b/websocket/_handshake.py new file mode 100644 index 000000000..809a8c989 --- /dev/null +++ b/websocket/_handshake.py @@ -0,0 +1,205 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import hashlib +import hmac +import os + +import six + +from ._cookiejar import SimpleCookieJar +from ._exceptions import * +from ._http import * +from ._logging import * +from ._socket import * + +if six.PY3: + from base64 import encodebytes as base64encode +else: + from base64 import encodestring as base64encode + +if six.PY3: + if six.PY34: + from http import client as HTTPStatus + else: + from http import HTTPStatus +else: + import httplib as HTTPStatus + +__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"] + +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + def compare_digest(s1, s2): + return s1 == s2 + +# websocket supported version. +VERSION = 13 + +SUPPORTED_REDIRECT_STATUSES = [HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER] + +CookieJar = SimpleCookieJar() + + +class handshake_response(object): + + def __init__(self, status, headers, subprotocol): + self.status = status + self.headers = headers + self.subprotocol = subprotocol + CookieJar.add(headers.get("set-cookie")) + + +def handshake(sock, hostname, port, resource, **options): + headers, key = _get_handshake_headers(resource, hostname, port, options) + + header_str = "\r\n".join(headers) + send(sock, header_str) + dump("request header", header_str) + + status, resp = _get_resp_headers(sock) + if status in SUPPORTED_REDIRECT_STATUSES: + return handshake_response(status, resp, None) + success, subproto = _validate(resp, key, options.get("subprotocols")) + if not success: + raise WebSocketException("Invalid WebSocket Header") + + return handshake_response(status, resp, subproto) + +def _pack_hostname(hostname): + # IPv6 address + if ':' in hostname: + return '[' + hostname + ']' + + return hostname + +def _get_handshake_headers(resource, host, port, options): + headers = [ + "GET %s HTTP/1.1" % resource, + "Upgrade: websocket", + "Connection: Upgrade" + ] + if port == 80 or port == 443: + hostport = _pack_hostname(host) + else: + hostport = "%s:%d" % (_pack_hostname(host), port) + + if "host" in options and options["host"] is not None: + headers.append("Host: %s" % options["host"]) + else: + headers.append("Host: %s" % hostport) + + if "suppress_origin" not in options or not options["suppress_origin"]: + if "origin" in options and options["origin"] is not None: + headers.append("Origin: %s" % options["origin"]) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + + # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified + if not 'header' in options or 'Sec-WebSocket-Key' not in options['header']: + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + else: + key = options['header']['Sec-WebSocket-Key'] + + if not 'header' in options or 'Sec-WebSocket-Version' not in options['header']: + headers.append("Sec-WebSocket-Version: %s" % VERSION) + + subprotocols = options.get("subprotocols") + if subprotocols: + headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols)) + + if "header" in options: + header = options["header"] + if isinstance(header, dict): + header = [ + ": ".join([k, v]) + for k, v in header.items() + if v is not None + ] + headers.extend(header) + + server_cookie = CookieJar.get(host) + client_cookie = options.get("cookie", None) + + cookie = "; ".join(filter(None, [server_cookie, client_cookie])) + + if cookie: + headers.append("Cookie: %s" % cookie) + + headers.append("") + headers.append("") + + return headers, key + + +def _get_resp_headers(sock, success_statuses=(101, 301, 302, 303)): + status, resp_headers, status_message = read_headers(sock) + if status not in success_statuses: + raise WebSocketBadStatusException("Handshake status %d %s", status, status_message, resp_headers) + return status, resp_headers + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", +} + + +def _validate(headers, key, subprotocols): + subproto = None + for k, v in _HEADERS_TO_CHECK.items(): + r = headers.get(k, None) + if not r: + return False, None + r = r.lower() + if v != r: + return False, None + + if subprotocols: + subproto = headers.get("sec-websocket-protocol", None).lower() + if not subproto or subproto not in [s.lower() for s in subprotocols]: + error("Invalid subprotocol: " + str(subprotocols)) + return False, None + + result = headers.get("sec-websocket-accept", None) + if not result: + return False, None + result = result.lower() + + if isinstance(result, six.text_type): + result = result.encode('utf-8') + + value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8') + hashed = base64encode(hashlib.sha1(value).digest()).strip().lower() + success = compare_digest(hashed, result) + + if success: + return True, subproto + else: + return False, None + + +def _create_sec_websocket_key(): + randomness = os.urandom(16) + return base64encode(randomness).decode('utf-8').strip() diff --git a/websocket/_http.py b/websocket/_http.py new file mode 100644 index 000000000..818068746 --- /dev/null +++ b/websocket/_http.py @@ -0,0 +1,328 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import errno +import os +import socket +import sys + +import six + +from ._exceptions import * +from ._logging import * +from ._socket import* +from ._ssl_compat import * +from ._url import * + +if six.PY3: + from base64 import encodebytes as base64encode +else: + from base64 import encodestring as base64encode + +__all__ = ["proxy_info", "connect", "read_headers"] + +try: + import socks + ProxyConnectionError = socks.ProxyConnectionError + HAS_PYSOCKS = True +except: + class ProxyConnectionError(BaseException): + pass + HAS_PYSOCKS = False + +class proxy_info(object): + + def __init__(self, **options): + self.type = options.get("proxy_type") or "http" + if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']): + raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'") + self.host = options.get("http_proxy_host", None) + if self.host: + self.port = options.get("http_proxy_port", 0) + self.auth = options.get("http_proxy_auth", None) + self.no_proxy = options.get("http_no_proxy", None) + else: + self.port = 0 + self.auth = None + self.no_proxy = None + +def _open_proxied_socket(url, options, proxy): + hostname, port, resource, is_secure = parse_url(url) + + if not HAS_PYSOCKS: + raise WebSocketException("PySocks module not found.") + + ptype = socks.SOCKS5 + rdns = False + if proxy.type == "socks4": + ptype = socks.SOCKS4 + if proxy.type == "http": + ptype = socks.HTTP + if proxy.type[-1] == "h": + rdns = True + + sock = socks.create_connection( + (hostname, port), + proxy_type = ptype, + proxy_addr = proxy.host, + proxy_port = proxy.port, + proxy_rdns = rdns, + proxy_username = proxy.auth[0] if proxy.auth else None, + proxy_password = proxy.auth[1] if proxy.auth else None, + timeout = options.timeout, + socket_options = DEFAULT_SOCKET_OPTION + options.sockopt + ) + + if is_secure: + if HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + else: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + + +def connect(url, options, proxy, socket): + if proxy.host and not socket and not (proxy.type == 'http'): + return _open_proxied_socket(url, options, proxy) + + hostname, port, resource, is_secure = parse_url(url) + + if socket: + return socket, (hostname, port, resource) + + addrinfo_list, need_tunnel, auth = _get_addrinfo_list( + hostname, port, is_secure, proxy) + if not addrinfo_list: + raise WebSocketException( + "Host not found.: " + hostname + ":" + str(port)) + + sock = None + try: + sock = _open_socket(addrinfo_list, options.sockopt, options.timeout) + if need_tunnel: + sock = _tunnel(sock, hostname, port, auth) + + if is_secure: + if HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + else: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + except: + if sock: + sock.close() + raise + + +def _get_addrinfo_list(hostname, port, is_secure, proxy): + phost, pport, pauth = get_proxy_info( + hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy) + try: + if not phost: + addrinfo_list = [ai for ai in socket.getaddrinfo( + hostname, port, 0, 0, socket.SOL_TCP) + if (ai[0] == socket.AF_INET6 and socket.has_ipv6) or ai[0] != socket.AF_INET6 + ] + return addrinfo_list, False, None + else: + pport = pport and pport or 80 + # when running on windows 10, the getaddrinfo used above + # returns a socktype 0. This generates an error exception: + #_on_error: exception Socket type must be stream or datagram, not 0 + # Force the socket type to SOCK_STREAM + addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP) + return addrinfo_list, True, pauth + except socket.gaierror as e: + raise WebSocketAddressException(e) + + +def _open_socket(addrinfo_list, sockopt, timeout): + err = None + for addrinfo in addrinfo_list: + family, socktype, proto = addrinfo[:3] + sock = socket.socket(family, socktype, proto) + sock.settimeout(timeout) + for opts in DEFAULT_SOCKET_OPTION: + sock.setsockopt(*opts) + for opts in sockopt: + sock.setsockopt(*opts) + + address = addrinfo[4] + err = None + while not err: + try: + sock.connect(address) + except ProxyConnectionError as error: + err = WebSocketProxyException(str(error)) + err.remote_ip = str(address[0]) + continue + except socket.error as error: + error.remote_ip = str(address[0]) + try: + eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED) + except: + eConnRefused = (errno.ECONNREFUSED, ) + if error.errno == errno.EINTR: + continue + elif error.errno in eConnRefused: + err = error + continue + else: + raise error + else: + break + else: + continue + break + else: + if err: + raise err + + return sock + + +def _can_use_sni(): + return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2) + + +def _wrap_sni_socket(sock, sslopt, hostname, check_hostname): + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) + + if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: + cafile = sslopt.get('ca_certs', None) + capath = sslopt.get('ca_cert_path', None) + if cafile or capath: + context.load_verify_locations(cafile=cafile, capath=capath) + elif hasattr(context, 'load_default_certs'): + context.load_default_certs(ssl.Purpose.SERVER_AUTH) + if sslopt.get('certfile', None): + context.load_cert_chain( + sslopt['certfile'], + sslopt.get('keyfile', None), + sslopt.get('password', None), + ) + # see + # https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153 + context.verify_mode = sslopt['cert_reqs'] + if HAVE_CONTEXT_CHECK_HOSTNAME: + context.check_hostname = check_hostname + if 'ciphers' in sslopt: + context.set_ciphers(sslopt['ciphers']) + if 'cert_chain' in sslopt: + certfile, keyfile, password = sslopt['cert_chain'] + context.load_cert_chain(certfile, keyfile, password) + if 'ecdh_curve' in sslopt: + context.set_ecdh_curve(sslopt['ecdh_curve']) + + return context.wrap_socket( + sock, + do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), + suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), + server_hostname=hostname, + ) + + +def _ssl_socket(sock, user_sslopt, hostname): + sslopt = dict(cert_reqs=ssl.CERT_REQUIRED) + sslopt.update(user_sslopt) + + certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE') + if certPath and os.path.isfile(certPath) \ + and user_sslopt.get('ca_certs', None) is None \ + and user_sslopt.get('ca_cert', None) is None: + sslopt['ca_certs'] = certPath + elif certPath and os.path.isdir(certPath) \ + and user_sslopt.get('ca_cert_path', None) is None: + sslopt['ca_cert_path'] = certPath + + check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop( + 'check_hostname', True) + + if _can_use_sni(): + sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) + else: + sslopt.pop('check_hostname', True) + sock = ssl.wrap_socket(sock, **sslopt) + + if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname: + match_hostname(sock.getpeercert(), hostname) + + return sock + + +def _tunnel(sock, host, port, auth): + debug("Connecting proxy...") + connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port) + # TODO: support digest auth. + if auth and auth[0]: + auth_str = auth[0] + if auth[1]: + auth_str += ":" + auth[1] + encoded_str = base64encode(auth_str.encode()).strip().decode() + connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str + connect_header += "\r\n" + dump("request header", connect_header) + + send(sock, connect_header) + + try: + status, resp_headers, status_message = read_headers(sock) + except Exception as e: + raise WebSocketProxyException(str(e)) + + if status != 200: + raise WebSocketProxyException( + "failed CONNECT via proxy status: %r" % status) + + return sock + + +def read_headers(sock): + status = None + status_message = None + headers = {} + trace("--- response header ---") + + while True: + line = recv_line(sock) + line = line.decode('utf-8').strip() + if not line: + break + trace(line) + if not status: + + status_info = line.split(" ", 2) + status = int(status_info[1]) + if len(status_info) > 2: + status_message = status_info[2] + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip() + else: + raise WebSocketException("Invalid header") + + trace("-----------------------") + + return status, headers, status_message diff --git a/websocket/_logging.py b/websocket/_logging.py new file mode 100644 index 000000000..70a6271d9 --- /dev/null +++ b/websocket/_logging.py @@ -0,0 +1,82 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import logging + +_logger = logging.getLogger('websocket') +try: + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +_logger.addHandler(NullHandler()) + +_traceEnabled = False + +__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace", + "isEnabledForError", "isEnabledForDebug"] + + +def enableTrace(traceable, handler = logging.StreamHandler()): + """ + turn on/off the traceability. + + traceable: boolean value. if set True, traceability is enabled. + """ + global _traceEnabled + _traceEnabled = traceable + if traceable: + _logger.addHandler(handler) + _logger.setLevel(logging.DEBUG) + + +def dump(title, message): + if _traceEnabled: + _logger.debug("--- " + title + " ---") + _logger.debug(message) + _logger.debug("-----------------------") + + +def error(msg): + _logger.error(msg) + + +def warning(msg): + _logger.warning(msg) + + +def debug(msg): + _logger.debug(msg) + + +def trace(msg): + if _traceEnabled: + _logger.debug(msg) + + +def isEnabledForError(): + return _logger.isEnabledFor(logging.ERROR) + + +def isEnabledForDebug(): + return _logger.isEnabledFor(logging.DEBUG) diff --git a/websocket/_socket.py b/websocket/_socket.py new file mode 100644 index 000000000..d811c21d9 --- /dev/null +++ b/websocket/_socket.py @@ -0,0 +1,160 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import errno +import select +import socket + +import six +import sys + +from ._exceptions import * +from ._ssl_compat import * +from ._utils import * + +DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)] +if hasattr(socket, "SO_KEEPALIVE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)) +if hasattr(socket, "TCP_KEEPIDLE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30)) +if hasattr(socket, "TCP_KEEPINTVL"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)) +if hasattr(socket, "TCP_KEEPCNT"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3)) + +_default_timeout = None + +__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout", + "recv", "recv_line", "send"] + + +class sock_opt(object): + + def __init__(self, sockopt, sslopt): + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.sockopt = sockopt + self.sslopt = sslopt + self.timeout = None + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + timeout: default socket timeout time. This value is second. + """ + global _default_timeout + _default_timeout = timeout + + +def getdefaulttimeout(): + """ + Return the global timeout setting(second) to connect. + """ + return _default_timeout + + +def recv(sock, bufsize): + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + def _recv(): + try: + return sock.recv(bufsize) + except SSLWantReadError: + pass + except socket.error as exc: + error_code = extract_error_code(exc) + if error_code is None: + raise + if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: + raise + + r, w, e = select.select((sock, ), (), (), sock.gettimeout()) + if r: + return sock.recv(bufsize) + + try: + bytes_ = _recv() + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except SSLError as e: + message = extract_err_message(e) + if isinstance(message, str) and 'timed out' in message: + raise WebSocketTimeoutException(message) + else: + raise + + if not bytes_: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + return bytes_ + + +def recv_line(sock): + line = [] + while True: + c = recv(sock, 1) + line.append(c) + if c == six.b("\n"): + break + return six.b("").join(line) + + +def send(sock, data): + if isinstance(data, six.text_type): + data = data.encode('utf-8') + + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + def _send(): + try: + return sock.send(data) + except SSLWantWriteError: + pass + except socket.error as exc: + error_code = extract_error_code(exc) + if error_code is None: + raise + if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: + raise + + r, w, e = select.select((), (sock, ), (), sock.gettimeout()) + if w: + return sock.send(data) + + try: + return _send() + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except Exception as e: + message = extract_err_message(e) + if isinstance(message, str) and "timed out" in message: + raise WebSocketTimeoutException(message) + else: + raise diff --git a/websocket/_ssl_compat.py b/websocket/_ssl_compat.py new file mode 100644 index 000000000..5b3c413c8 --- /dev/null +++ b/websocket/_ssl_compat.py @@ -0,0 +1,52 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +__all__ = ["HAVE_SSL", "ssl", "SSLError", "SSLWantReadError", "SSLWantWriteError"] + +try: + import ssl + from ssl import SSLError + from ssl import SSLWantReadError + from ssl import SSLWantWriteError + if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'): + HAVE_CONTEXT_CHECK_HOSTNAME = True + else: + HAVE_CONTEXT_CHECK_HOSTNAME = False + if hasattr(ssl, "match_hostname"): + from ssl import match_hostname + else: + from backports.ssl_match_hostname import match_hostname + __all__.append("match_hostname") + __all__.append("HAVE_CONTEXT_CHECK_HOSTNAME") + + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + + class SSLWantReadError(Exception): + pass + + class SSLWantWriteError(Exception): + pass + + HAVE_SSL = False diff --git a/websocket/_url.py b/websocket/_url.py new file mode 100644 index 000000000..ae46d6c40 --- /dev/null +++ b/websocket/_url.py @@ -0,0 +1,163 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + +import os +import socket +import struct + +from six.moves.urllib.parse import urlparse + + +__all__ = ["parse_url", "get_proxy_info"] + + +def parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + url: url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="ws") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return hostname, port, resource, is_secure + + +DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"] + + +def _is_ip_address(addr): + try: + socket.inet_aton(addr) + except socket.error: + return False + else: + return True + + +def _is_subnet_address(hostname): + try: + addr, netmask = hostname.split("/") + return _is_ip_address(addr) and 0 <= int(netmask) < 32 + except ValueError: + return False + + +def _is_address_in_network(ip, net): + ipaddr = struct.unpack('I', socket.inet_aton(ip))[0] + netaddr, bits = net.split('/') + netmask = struct.unpack('I', socket.inet_aton(netaddr))[0] & ((2 << int(bits) - 1) - 1) + return ipaddr & netmask == netmask + + +def _is_no_proxy_host(hostname, no_proxy): + if not no_proxy: + v = os.environ.get("no_proxy", "").replace(" ", "") + no_proxy = v.split(",") + if not no_proxy: + no_proxy = DEFAULT_NO_PROXY_HOST + + if hostname in no_proxy: + return True + elif _is_ip_address(hostname): + return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)]) + + return False + + +def get_proxy_info( + hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, + no_proxy=None, proxy_type='http'): + """ + try to retrieve proxy host and port from environment + if not provided in options. + result is (proxy_host, proxy_port, proxy_auth). + proxy_auth is tuple of username and password + of proxy authentication information. + + hostname: websocket server name. + + is_secure: is the connection secure? (wss) + looks for "https_proxy" in env + before falling back to "http_proxy" + + options: "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "proxy_type" - if set to "socks5" PySocks wrapper + will be used in place of a http proxy. + default is "http" + """ + if _is_no_proxy_host(hostname, no_proxy): + return None, 0, None + + if proxy_host: + port = proxy_port + auth = proxy_auth + return proxy_host, port, auth + + env_keys = ["http_proxy"] + if is_secure: + env_keys.insert(0, "https_proxy") + + for key in env_keys: + value = os.environ.get(key, None) + if value: + proxy = urlparse(value) + auth = (proxy.username, proxy.password) if proxy.username else None + return proxy.hostname, proxy.port, auth + + return None, 0, None diff --git a/websocket/_utils.py b/websocket/_utils.py new file mode 100644 index 000000000..8eddabf9c --- /dev/null +++ b/websocket/_utils.py @@ -0,0 +1,110 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import six + +__all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"] + + +class NoLock(object): + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + +try: + # If wsaccel is available we use compiled routines to validate UTF-8 + # strings. + from wsaccel.utf8validator import Utf8Validator + + def _validate_utf8(utfbytes): + return Utf8Validator().validate(utfbytes)[0] + +except ImportError: + # UTF-8 validator + # python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + + _UTF8_ACCEPT = 0 + _UTF8_REJECT = 12 + + _UTF8D = [ + # The first part of the table maps bytes to character classes that + # to reduce the size of the transition table and create bitmasks. + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, + + # The second part is a transition table that maps a combination + # of a state of the automaton and a character class to a state. + 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, + 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, + 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, + 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, + 12,36,12,12,12,12,12,12,12,12,12,12, ] + + def _decode(state, codep, ch): + tp = _UTF8D[ch] + + codep = (ch & 0x3f) | (codep << 6) if ( + state != _UTF8_ACCEPT) else (0xff >> tp) & ch + state = _UTF8D[256 + state + tp] + + return state, codep + + def _validate_utf8(utfbytes): + state = _UTF8_ACCEPT + codep = 0 + for i in utfbytes: + if six.PY2: + i = ord(i) + state, codep = _decode(state, codep, i) + if state == _UTF8_REJECT: + return False + + return True + + +def validate_utf8(utfbytes): + """ + validate utf8 byte string. + utfbytes: utf byte string to check. + return value: if valid utf8 string, return true. Otherwise, return false. + """ + return _validate_utf8(utfbytes) + + +def extract_err_message(exception): + if exception.args: + return exception.args[0] + else: + return None + + +def extract_error_code(exception): + if exception.args and len(exception.args) > 1: + return exception.args[0] if isinstance(exception.args[0], int) else None diff --git a/websocket/tests/__init__.py b/websocket/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/websocket/tests/data/header01.txt b/websocket/tests/data/header01.txt new file mode 100644 index 000000000..3142b43b3 --- /dev/null +++ b/websocket/tests/data/header01.txt @@ -0,0 +1,6 @@ +HTTP/1.1 101 WebSocket Protocol Handshake +Connection: Upgrade +Upgrade: WebSocket +Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0= +some_header: something + diff --git a/websocket/tests/data/header02.txt b/websocket/tests/data/header02.txt new file mode 100644 index 000000000..a9dd2ce3e --- /dev/null +++ b/websocket/tests/data/header02.txt @@ -0,0 +1,6 @@ +HTTP/1.1 101 WebSocket Protocol Handshake +Connection: Upgrade +Upgrade WebSocket +Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0= +some_header: something + diff --git a/websocket/tests/test_cookiejar.py b/websocket/tests/test_cookiejar.py new file mode 100644 index 000000000..c40a00bd2 --- /dev/null +++ b/websocket/tests/test_cookiejar.py @@ -0,0 +1,98 @@ +import unittest + +from websocket._cookiejar import SimpleCookieJar + +try: + import Cookie +except: + import http.cookies as Cookie + + +class CookieJarTest(unittest.TestCase): + def testAdd(self): + cookie_jar = SimpleCookieJar() + cookie_jar.add("") + self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b") + self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; domain=.abc") + self.assertTrue(".abc" in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; domain=abc") + self.assertTrue(".abc" in cookie_jar.jar) + self.assertTrue("abc" not in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + cookie_jar.add("e=f; domain=abc") + self.assertEquals(cookie_jar.get("abc"), "a=b; c=d; e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + cookie_jar.add("e=f; domain=.abc") + self.assertEquals(cookie_jar.get("abc"), "a=b; c=d; e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + cookie_jar.add("e=f; domain=xyz") + self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") + self.assertEquals(cookie_jar.get("xyz"), "e=f") + self.assertEquals(cookie_jar.get("something"), "") + + def testSet(self): + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b") + self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; domain=.abc") + self.assertTrue(".abc" in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; domain=abc") + self.assertTrue(".abc" in cookie_jar.jar) + self.assertTrue("abc" not in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + cookie_jar.set("e=f; domain=abc") + self.assertEquals(cookie_jar.get("abc"), "e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + cookie_jar.set("e=f; domain=.abc") + self.assertEquals(cookie_jar.get("abc"), "e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + cookie_jar.set("e=f; domain=xyz") + self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") + self.assertEquals(cookie_jar.get("xyz"), "e=f") + self.assertEquals(cookie_jar.get("something"), "") + + def testGet(self): + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc.com") + self.assertEquals(cookie_jar.get("abc.com"), "a=b; c=d") + self.assertEquals(cookie_jar.get("x.abc.com"), "a=b; c=d") + self.assertEquals(cookie_jar.get("abc.com.es"), "") + self.assertEquals(cookie_jar.get("xabc.com"), "") + + cookie_jar.set("a=b; c=d; domain=.abc.com") + self.assertEquals(cookie_jar.get("abc.com"), "a=b; c=d") + self.assertEquals(cookie_jar.get("x.abc.com"), "a=b; c=d") + self.assertEquals(cookie_jar.get("abc.com.es"), "") + self.assertEquals(cookie_jar.get("xabc.com"), "") diff --git a/websocket/tests/test_websocket.py b/websocket/tests/test_websocket.py new file mode 100644 index 000000000..f49a89398 --- /dev/null +++ b/websocket/tests/test_websocket.py @@ -0,0 +1,662 @@ +# -*- coding: utf-8 -*- +# + +import sys +sys.path[0:0] = [""] + +import os +import os.path +import socket + +import six + +# websocket-client +import websocket as ws +from websocket._handshake import _create_sec_websocket_key, \ + _validate as _validate_header +from websocket._http import read_headers +from websocket._url import get_proxy_info, parse_url +from websocket._utils import validate_utf8 + +if six.PY3: + from base64 import decodebytes as base64decode +else: + from base64 import decodestring as base64decode + +if sys.version_info[0] == 2 and sys.version_info[1] < 7: + import unittest2 as unittest +else: + import unittest + +try: + from ssl import SSLError +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + +# Skip test to access the internet. +TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' + +# Skip Secure WebSocket test. +TEST_SECURE_WS = True +TRACEABLE = True + + +def create_mask_key(_): + return "abcd" + + +class SockMock(object): + def __init__(self): + self.data = [] + self.sent = [] + + def add_packet(self, data): + self.data.append(data) + + def recv(self, bufsize): + if self.data: + e = self.data.pop(0) + if isinstance(e, Exception): + raise e + if len(e) > bufsize: + self.data.insert(0, e[bufsize:]) + return e[:bufsize] + + def send(self, data): + self.sent.append(data) + return len(data) + + def close(self): + pass + + +class HeaderSockMock(SockMock): + + def __init__(self, fname): + SockMock.__init__(self) + path = os.path.join(os.path.dirname(__file__), fname) + with open(path, "rb") as f: + self.add_packet(f.read()) + + +class WebSocketTest(unittest.TestCase): + def setUp(self): + ws.enableTrace(TRACEABLE) + + def tearDown(self): + pass + + def testDefaultTimeout(self): + self.assertEqual(ws.getdefaulttimeout(), None) + ws.setdefaulttimeout(10) + self.assertEqual(ws.getdefaulttimeout(), 10) + ws.setdefaulttimeout(None) + + def testParseUrl(self): + p = parse_url("ws://www.example.com/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com/r/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("wss://www.example.com:8080/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + p = parse_url("wss://www.example.com:8080/r?key=value") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r?key=value") + self.assertEqual(p[3], True) + + self.assertRaises(ValueError, parse_url, "http://www.example.com/r") + + if sys.version_info[0] == 2 and sys.version_info[1] < 7: + return + + p = parse_url("ws://[2a03:4000:123:83::3]/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://[2a03:4000:123:83::3]:8080/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("wss://[2a03:4000:123:83::3]/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 443) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + p = parse_url("wss://[2a03:4000:123:83::3]:8080/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + def testWSKey(self): + key = _create_sec_websocket_key() + self.assertTrue(key != 24) + self.assertTrue(six.u("¥n") not in key) + + def testWsUtils(self): + key = "c6b8hTg4EeGb2gQMztV1/g==" + required_header = { + "upgrade": "websocket", + "connection": "upgrade", + "sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0=", + } + self.assertEqual(_validate_header(required_header, key, None), (True, None)) + + header = required_header.copy() + header["upgrade"] = "http" + self.assertEqual(_validate_header(header, key, None), (False, None)) + del header["upgrade"] + self.assertEqual(_validate_header(header, key, None), (False, None)) + + header = required_header.copy() + header["connection"] = "something" + self.assertEqual(_validate_header(header, key, None), (False, None)) + del header["connection"] + self.assertEqual(_validate_header(header, key, None), (False, None)) + + header = required_header.copy() + header["sec-websocket-accept"] = "something" + self.assertEqual(_validate_header(header, key, None), (False, None)) + del header["sec-websocket-accept"] + self.assertEqual(_validate_header(header, key, None), (False, None)) + + header = required_header.copy() + header["sec-websocket-protocol"] = "sub1" + self.assertEqual(_validate_header(header, key, ["sub1", "sub2"]), (True, "sub1")) + self.assertEqual(_validate_header(header, key, ["sub2", "sub3"]), (False, None)) + + header = required_header.copy() + header["sec-websocket-protocol"] = "sUb1" + self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (True, "sub1")) + + + def testReadHeader(self): + status, header, status_message = read_headers(HeaderSockMock("data/header01.txt")) + self.assertEqual(status, 101) + self.assertEqual(header["connection"], "Upgrade") + + HeaderSockMock("data/header02.txt") + self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt")) + + def testSend(self): + # TODO: add longer frame data + sock = ws.WebSocket() + sock.set_mask_key(create_mask_key) + s = sock.sock = HeaderSockMock("data/header01.txt") + sock.send("Hello") + self.assertEqual(s.sent[0], six.b("\x81\x85abcd)\x07\x0f\x08\x0e")) + + sock.send("こんにちは") + self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")) + + sock.send(u"こんにちは") + self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")) + + sock.send("x" * 127) + + def testRecv(self): + # TODO: add longer frame data + sock = ws.WebSocket() + s = sock.sock = SockMock() + something = six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc") + s.add_packet(something) + data = sock.recv() + self.assertEqual(data, "こんにちは") + + s.add_packet(six.b("\x81\x85abcd)\x07\x0f\x08\x0e")) + data = sock.recv() + self.assertEqual(data, "Hello") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testIter(self): + count = 2 + for _ in ws.create_connection('ws://stream.meetup.com/2/rsvps'): + count -= 1 + if count == 0: + break + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testNext(self): + sock = ws.create_connection('ws://stream.meetup.com/2/rsvps') + self.assertEqual(str, type(next(sock))) + + def testInternalRecvStrict(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + s.add_packet(six.b("foo")) + s.add_packet(socket.timeout()) + s.add_packet(six.b("bar")) + # s.add_packet(SSLError("The read operation timed out")) + s.add_packet(six.b("baz")) + with self.assertRaises(ws.WebSocketTimeoutException): + sock.frame_buffer.recv_strict(9) + # if six.PY2: + # with self.assertRaises(ws.WebSocketTimeoutException): + # data = sock._recv_strict(9) + # else: + # with self.assertRaises(SSLError): + # data = sock._recv_strict(9) + data = sock.frame_buffer.recv_strict(9) + self.assertEqual(data, six.b("foobarbaz")) + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.frame_buffer.recv_strict(1) + + def testRecvTimeout(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + s.add_packet(six.b("\x81")) + s.add_packet(socket.timeout()) + s.add_packet(six.b("\x8dabcd\x29\x07\x0f\x08\x0e")) + s.add_packet(socket.timeout()) + s.add_packet(six.b("\x4e\x43\x33\x0e\x10\x0f\x00\x40")) + with self.assertRaises(ws.WebSocketTimeoutException): + sock.recv() + with self.assertRaises(ws.WebSocketTimeoutException): + sock.recv() + data = sock.recv() + self.assertEqual(data, "Hello, World!") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testRecvWithSimpleFragmentation(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Brevity is " + s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + # OPCODE=CONT, FIN=1, MSG="the soul of wit" + s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17")) + data = sock.recv() + self.assertEqual(data, "Brevity is the soul of wit") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testRecvWithFireEventOfFragmentation(self): + sock = ws.WebSocket(fire_cont_frame=True) + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Brevity is " + s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + # OPCODE=CONT, FIN=0, MSG="Brevity is " + s.add_packet(six.b("\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + # OPCODE=CONT, FIN=1, MSG="the soul of wit" + s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17")) + + _, data = sock.recv_data() + self.assertEqual(data, six.b("Brevity is ")) + _, data = sock.recv_data() + self.assertEqual(data, six.b("Brevity is ")) + _, data = sock.recv_data() + self.assertEqual(data, six.b("the soul of wit")) + + # OPCODE=CONT, FIN=0, MSG="Brevity is " + s.add_packet(six.b("\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + + with self.assertRaises(ws.WebSocketException): + sock.recv_data() + + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testClose(self): + sock = ws.WebSocket() + sock.sock = SockMock() + sock.connected = True + sock.close() + self.assertEqual(sock.connected, False) + + sock = ws.WebSocket() + s = sock.sock = SockMock() + sock.connected = True + s.add_packet(six.b('\x88\x80\x17\x98p\x84')) + sock.recv() + self.assertEqual(sock.connected, False) + + def testRecvContFragmentation(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + # OPCODE=CONT, FIN=1, MSG="the soul of wit" + s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17")) + self.assertRaises(ws.WebSocketException, sock.recv) + + def testRecvWithProlongedFragmentation(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Once more unto the breach, " + s.add_packet(six.b("\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15" + "\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC")) + # OPCODE=CONT, FIN=0, MSG="dear friends, " + s.add_packet(six.b("\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07" + "\x17MB")) + # OPCODE=CONT, FIN=1, MSG="once more" + s.add_packet(six.b("\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04")) + data = sock.recv() + self.assertEqual( + data, + "Once more unto the breach, dear friends, once more") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testRecvWithFragmentationAndControlFrame(self): + sock = ws.WebSocket() + sock.set_mask_key(create_mask_key) + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Too much " + s.add_packet(six.b("\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA")) + # OPCODE=PING, FIN=1, MSG="Please PONG this" + s.add_packet(six.b("\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17")) + # OPCODE=CONT, FIN=1, MSG="of a good thing" + s.add_packet(six.b("\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c" + "\x08\x0c\x04")) + data = sock.recv() + self.assertEqual(data, "Too much of a good thing") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + self.assertEqual( + s.sent[0], + six.b("\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17")) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testWebSocket(self): + s = ws.create_connection("ws://echo.websocket.org/") + self.assertNotEqual(s, None) + s.send("Hello, World") + result = s.recv() + self.assertEqual(result, "Hello, World") + + s.send(u"こにゃにゃちは、世界") + result = s.recv() + self.assertEqual(result, "こにゃにゃちは、世界") + s.close() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testPingPong(self): + s = ws.create_connection("ws://echo.websocket.org/") + self.assertNotEqual(s, None) + s.ping("Hello") + s.pong("Hi") + s.close() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + @unittest.skipUnless(TEST_SECURE_WS, "wss://echo.websocket.org doesn't work well.") + def testSecureWebSocket(self): + if 1: + import ssl + s = ws.create_connection("wss://echo.websocket.org/") + self.assertNotEqual(s, None) + self.assertTrue(isinstance(s.sock, ssl.SSLSocket)) + s.send("Hello, World") + result = s.recv() + self.assertEqual(result, "Hello, World") + s.send(u"こにゃにゃちは、世界") + result = s.recv() + self.assertEqual(result, "こにゃにゃちは、世界") + s.close() + #except: + # pass + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testWebSocketWihtCustomHeader(self): + s = ws.create_connection("ws://echo.websocket.org/", + headers={"User-Agent": "PythonWebsocketClient"}) + self.assertNotEqual(s, None) + s.send("Hello, World") + result = s.recv() + self.assertEqual(result, "Hello, World") + s.close() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testAfterClose(self): + s = ws.create_connection("ws://echo.websocket.org/") + self.assertNotEqual(s, None) + s.close() + self.assertRaises(ws.WebSocketConnectionClosedException, s.send, "Hello") + self.assertRaises(ws.WebSocketConnectionClosedException, s.recv) + + def testNonce(self): + """ WebSocket key should be a random 16-byte nonce. + """ + key = _create_sec_websocket_key() + nonce = base64decode(key.encode("utf-8")) + self.assertEqual(16, len(nonce)) + + +class WebSocketAppTest(unittest.TestCase): + + class NotSetYet(object): + """ A marker class for signalling that a value hasn't been set yet. + """ + + def setUp(self): + ws.enableTrace(TRACEABLE) + + WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() + WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() + WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() + + def tearDown(self): + WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() + WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() + WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testKeepRunning(self): + """ A WebSocketApp should keep running as long as its self.keep_running + is not False (in the boolean context). + """ + + def on_open(self, *args, **kwargs): + """ Set the keep_running flag for later inspection and immediately + close the connection. + """ + WebSocketAppTest.keep_running_open = self.keep_running + + self.close() + + def on_close(self, *args, **kwargs): + """ Set the keep_running flag for the test to use. + """ + WebSocketAppTest.keep_running_close = self.keep_running + + app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, on_close=on_close) + app.run_forever() + + # if numpy is installed, this assertion fail + # self.assertFalse(isinstance(WebSocketAppTest.keep_running_open, + # WebSocketAppTest.NotSetYet)) + + # self.assertFalse(isinstance(WebSocketAppTest.keep_running_close, + # WebSocketAppTest.NotSetYet)) + + # self.assertEqual(True, WebSocketAppTest.keep_running_open) + # self.assertEqual(False, WebSocketAppTest.keep_running_close) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSockMaskKey(self): + """ A WebSocketApp should forward the received mask_key function down + to the actual socket. + """ + + def my_mask_key_func(): + pass + + def on_open(self, *args, **kwargs): + """ Set the value so the test can use it later on and immediately + close the connection. + """ + WebSocketAppTest.get_mask_key_id = id(self.get_mask_key) + self.close() + + app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, get_mask_key=my_mask_key_func) + app.run_forever() + + # if numpu is installed, this assertion fail + # Note: We can't use 'is' for comparing the functions directly, need to use 'id'. + # self.assertEqual(WebSocketAppTest.get_mask_key_id, id(my_mask_key_func)) + + +class SockOptTest(unittest.TestCase): + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSockOpt(self): + sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),) + s = ws.create_connection("ws://echo.websocket.org", sockopt=sockopt) + self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0) + s.close() + + +class UtilsTest(unittest.TestCase): + def testUtf8Validator(self): + state = validate_utf8(six.b('\xf0\x90\x80\x80')) + self.assertEqual(state, True) + state = validate_utf8(six.b('\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited')) + self.assertEqual(state, False) + state = validate_utf8(six.b('')) + self.assertEqual(state, True) + + +class ProxyInfoTest(unittest.TestCase): + def setUp(self): + self.http_proxy = os.environ.get("http_proxy", None) + self.https_proxy = os.environ.get("https_proxy", None) + if "http_proxy" in os.environ: + del os.environ["http_proxy"] + if "https_proxy" in os.environ: + del os.environ["https_proxy"] + + def tearDown(self): + if self.http_proxy: + os.environ["http_proxy"] = self.http_proxy + elif "http_proxy" in os.environ: + del os.environ["http_proxy"] + + if self.https_proxy: + os.environ["https_proxy"] = self.https_proxy + elif "https_proxy" in os.environ: + del os.environ["https_proxy"] + + def testProxyFromArgs(self): + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None)) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None)) + + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")), + ("localhost", 0, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")), + ("localhost", 0, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["example.com"], proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")), + (None, 0, None)) + + def testProxyFromEnv(self): + os.environ["http_proxy"] = "http://localhost/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None)) + + + os.environ["http_proxy"] = "http://a:b@localhost/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + os.environ["no_proxy"] = "example1.com,example2.com" + self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org" + self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None)) + + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "127.0.0.0/8, 192.168.0.0/16" + self.assertEqual(get_proxy_info("127.0.0.1", False), (None, 0, None)) + self.assertEqual(get_proxy_info("192.168.1.1", False), (None, 0, None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/websocket_client-0.55.0-py2.7.egg-info/PKG-INFO b/websocket_client-0.55.0-py2.7.egg-info/PKG-INFO new file mode 100644 index 000000000..d9ae55980 --- /dev/null +++ b/websocket_client-0.55.0-py2.7.egg-info/PKG-INFO @@ -0,0 +1,305 @@ +Metadata-Version: 1.2 +Name: websocket-client +Version: 0.55.0 +Summary: WebSocket client for Python. hybi13 is supported. +Home-page: https://github.com/websocket-client/websocket-client.git +Author: liris +Author-email: liris.pp@gmail.com +License: BSD +Description: ================= + websocket-client + ================= + + websocket-client module is WebSocket client for python. This provide the low level APIs for WebSocket. All APIs are the synchronous functions. + + websocket-client supports only hybi-13. + + + License + ======= + + - BSD + + Installation + ============ + + This module is tested on Python 2.7 and Python 3.4+. + + Type "python setup.py install" or "pip install websocket-client" to install. + + .. CAUTION:: + + from v0.16.0, we can install by "pip install websocket-client" for Python 3. + + This module depends on + + - six + - backports.ssl_match_hostname for Python 2.x + + Performance + ----------- + + The "send" and "validate_utf8" methods are too slow on pure python. If you want to get better performace, please install both numpy and wsaccel. + + + How about Python 3 + ================== + + Now, we support Python 3 on single source code from version 0.14.0. Thanks, @battlemidget and @ralphbean. + + HTTP Proxy + ========== + + Support websocket access via http proxy. + The proxy server must allow "CONNECT" method to websocket port. + Default squid setting is "ALLOWED TO CONNECT ONLY HTTPS PORT". + + Current implementation of websocket-client is using "CONNECT" method via proxy. + + + example + + .. code:: python + + import websocket + ws = websocket.WebSocket() + ws.connect("ws://example.com/websocket", http_proxy_host="proxy_host_name", http_proxy_port=3128) + + + + Examples + ======== + + Long-lived connection + --------------------- + This example is similar to how WebSocket code looks in browsers using JavaScript. + + .. code:: python + + import websocket + try: + import thread + except ImportError: + import _thread as thread + import time + + def on_message(ws, message): + print(message) + + def on_error(ws, error): + print(error) + + def on_close(ws): + print("### closed ###") + + def on_open(ws): + def run(*args): + for i in range(3): + time.sleep(1) + ws.send("Hello %d" % i) + time.sleep(1) + ws.close() + print("thread terminating...") + thread.start_new_thread(run, ()) + + + if __name__ == "__main__": + websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://echo.websocket.org/", + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + ws.run_forever() + + + + Short-lived one-off send-receive + -------------------------------- + This is if you want to communicate a short message and disconnect immediately when done. + + .. code:: python + + from websocket import create_connection + ws = create_connection("ws://echo.websocket.org/") + print("Sending 'Hello, World'...") + ws.send("Hello, World") + print("Sent") + print("Receiving...") + result = ws.recv() + print("Received '%s'" % result) + ws.close() + + + If you want to customize socket options, set sockopt. + + sockopt example + + .. code:: python + + from websocket import create_connection + ws = create_connection("ws://echo.websocket.org/", + sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY),)) + + + More advanced: Custom class + --------------------------- + You can also write your own class for the connection, if you want to handle the nitty-gritty details yourself. + + .. code:: python + + import socket + from websocket import create_connection, WebSocket + class MyWebSocket(WebSocket): + def recv_frame(self): + frame = super().recv_frame() + print('yay! I got this frame: ', frame) + return frame + + ws = create_connection("ws://echo.websocket.org/", + sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),), class_=MyWebSocket) + + + FAQ + === + + How to disable ssl cert verification? + ------------------------------------- + + Please set sslopt to {"cert_reqs": ssl.CERT_NONE}. + + WebSocketApp sample + + .. code:: python + + ws = websocket.WebSocketApp("wss://echo.websocket.org") + ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) + + + create_connection sample + + .. code:: python + + ws = websocket.create_connection("wss://echo.websocket.org", + sslopt={"cert_reqs": ssl.CERT_NONE}) + + + WebSocket sample + + .. code:: python + + ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE}) + ws.connect("wss://echo.websocket.org") + + + How to disable hostname verification? + ------------------------------------- + + Please set sslopt to {"check_hostname": False}. + (since v0.18.0) + + WebSocketApp sample + + .. code:: python + + ws = websocket.WebSocketApp("wss://echo.websocket.org") + ws.run_forever(sslopt={"check_hostname": False}) + + + create_connection sample + + .. code:: python + + ws = websocket.create_connection("wss://echo.websocket.org", + sslopt={"check_hostname": False}) + + + WebSocket sample + + .. code:: python + + ws = websocket.WebSocket(sslopt={"check_hostname": False}) + ws.connect("wss://echo.websocket.org") + + + How to enable `SNI `_? + --------------------------------------------------------------------------- + + SNI support is available for Python 2.7.9+ and 3.2+. It will be enabled automatically whenever possible. + + + Sub Protocols. + -------------- + + The server needs to support sub protocols, please set the subprotocol like this. + + + Subprotocol sample + + .. code:: python + + ws = websocket.create_connection("ws://example.com/websocket", subprotocols=["binary", "base64"]) + + + + wsdump.py + ========= + + wsdump.py is simple WebSocket test(debug) tool. + + sample for echo.websocket.org:: + + $ wsdump.py ws://echo.websocket.org/ + Press Ctrl+C to quit + > Hello, WebSocket + < Hello, WebSocket + > How are you? + < How are you? + + + Usage + ----- + + usage:: + + wsdump.py [-h] [-v [VERBOSE]] ws_url + + + WebSocket Simple Dump Tool + + positional arguments: + ws_url websocket url. ex. ws://echo.websocket.org/ + + + optional arguments: + -h, --help show this help message and exit + WebSocketApp + -v VERBOSE, --verbose VERBOSE set verbose mode. If set to 1, show opcode. If set to 2, enable to trace websocket module + + + example:: + + $ wsdump.py ws://echo.websocket.org/ + $ wsdump.py ws://echo.websocket.org/ -v + $ wsdump.py ws://echo.websocket.org/ -vv + +Keywords: websockets +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Intended Audience :: Developers +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff --git a/websocket_client-0.55.0-py2.7.egg-info/SOURCES.txt b/websocket_client-0.55.0-py2.7.egg-info/SOURCES.txt new file mode 100644 index 000000000..d003db826 --- /dev/null +++ b/websocket_client-0.55.0-py2.7.egg-info/SOURCES.txt @@ -0,0 +1,32 @@ +ChangeLog +LICENSE +MANIFEST.in +README.rst +setup.cfg +setup.py +bin/wsdump.py +examples/echo_client.py +examples/echoapp_client.py +websocket/__init__.py +websocket/_abnf.py +websocket/_app.py +websocket/_cookiejar.py +websocket/_core.py +websocket/_exceptions.py +websocket/_handshake.py +websocket/_http.py +websocket/_logging.py +websocket/_socket.py +websocket/_ssl_compat.py +websocket/_url.py +websocket/_utils.py +websocket/tests/__init__.py +websocket/tests/test_cookiejar.py +websocket/tests/test_websocket.py +websocket/tests/data/header01.txt +websocket/tests/data/header02.txt +websocket_client.egg-info/PKG-INFO +websocket_client.egg-info/SOURCES.txt +websocket_client.egg-info/dependency_links.txt +websocket_client.egg-info/requires.txt +websocket_client.egg-info/top_level.txt \ No newline at end of file diff --git a/websocket_client-0.55.0-py2.7.egg-info/dependency_links.txt b/websocket_client-0.55.0-py2.7.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/websocket_client-0.55.0-py2.7.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/websocket_client-0.55.0-py2.7.egg-info/installed-files.txt b/websocket_client-0.55.0-py2.7.egg-info/installed-files.txt new file mode 100644 index 000000000..2069b5c8f --- /dev/null +++ b/websocket_client-0.55.0-py2.7.egg-info/installed-files.txt @@ -0,0 +1,40 @@ +../../../../bin/wsdump.py +../websocket/__init__.py +../websocket/__init__.pyc +../websocket/_abnf.py +../websocket/_abnf.pyc +../websocket/_app.py +../websocket/_app.pyc +../websocket/_cookiejar.py +../websocket/_cookiejar.pyc +../websocket/_core.py +../websocket/_core.pyc +../websocket/_exceptions.py +../websocket/_exceptions.pyc +../websocket/_handshake.py +../websocket/_handshake.pyc +../websocket/_http.py +../websocket/_http.pyc +../websocket/_logging.py +../websocket/_logging.pyc +../websocket/_socket.py +../websocket/_socket.pyc +../websocket/_ssl_compat.py +../websocket/_ssl_compat.pyc +../websocket/_url.py +../websocket/_url.pyc +../websocket/_utils.py +../websocket/_utils.pyc +../websocket/tests/__init__.py +../websocket/tests/__init__.pyc +../websocket/tests/data/header01.txt +../websocket/tests/data/header02.txt +../websocket/tests/test_cookiejar.py +../websocket/tests/test_cookiejar.pyc +../websocket/tests/test_websocket.py +../websocket/tests/test_websocket.pyc +PKG-INFO +SOURCES.txt +dependency_links.txt +requires.txt +top_level.txt diff --git a/websocket_client-0.55.0-py2.7.egg-info/requires.txt b/websocket_client-0.55.0-py2.7.egg-info/requires.txt new file mode 100644 index 000000000..e87d97531 --- /dev/null +++ b/websocket_client-0.55.0-py2.7.egg-info/requires.txt @@ -0,0 +1,2 @@ +six +backports.ssl_match_hostname diff --git a/websocket_client-0.55.0-py2.7.egg-info/top_level.txt b/websocket_client-0.55.0-py2.7.egg-info/top_level.txt new file mode 100644 index 000000000..ca4cb0cf8 --- /dev/null +++ b/websocket_client-0.55.0-py2.7.egg-info/top_level.txt @@ -0,0 +1 @@ +websocket