Squashed 'pyextra/' changes from 0d19c13e..42428013

42428013 websocket patch from commaai/websocket-client.git
0fda5bb7 add jsonrpc
8139b06b add websocket_client

git-subtree-dir: pyextra
git-subtree-split: 4242801316e12c55e5b7c626331fbefad2e15e0c
pull/582/head
Vehicle Researcher 2019-03-26 01:03:51 -07:00
parent 9a79df8a8a
commit 342bb13bff
69 changed files with 8796 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
backports

View File

@ -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__)

View File

@ -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")

201
bin/wsdump.py 100755
View File

@ -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)

View File

@ -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 <http://www.jsonrpc.org/specification>`_ and `JSON-RPC1.0 <http://json-rpc.org/wiki/specification>`_ 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 <https://github.com/pavlov99/json-rpc/tree/master/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 <https://circleci.com/>`_ 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 <http://werkzeug.pocoo.org/>`_)
.. 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 {<method_name>: 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 <http://www.python-requests.org/en/latest/>`_)
.. 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 <http://en.wikipedia.org/wiki/JSON-RPC#Implementations>`_ 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

View File

@ -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

View File

@ -0,0 +1 @@

View File

@ -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

View File

@ -0,0 +1 @@
jsonrpc

View File

@ -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

View File

View File

@ -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 = "<h1>JSON-RPC map</h1><pre>{0}</pre>".format("\n\n".join([
"{0}: {1}".format(fname, f.__doc__)
for fname, f in self.dispatcher.items()
]))
return HttpResponse(result)
api = JSONRPCAPI()

View File

@ -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 = "<h1>JSON-RPC map</h1><pre>{0}</pre>".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()

87
jsonrpc/base.py 100644
View File

@ -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)

View File

@ -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")
<function __main__.<lambda>>
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

View File

@ -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)

28
jsonrpc/jsonrpc.py 100644
View File

@ -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)

151
jsonrpc/jsonrpc1.py 100644
View File

@ -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

267
jsonrpc/jsonrpc2.py 100644
View File

@ -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)

136
jsonrpc/manager.py 100644
View File

@ -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<function_name:function>.
"""
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

584
jsonrpc/six.py 100644
View File

@ -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 <benjamin@python.org>"
__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

View File

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)),
]

View File

@ -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))

View File

@ -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

View File

@ -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}'
))

View File

@ -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))

View File

@ -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
))

View File

@ -0,0 +1 @@
""" Tets base JSON-RPC structures."""

View File

@ -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"])

View File

@ -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"},
])

View File

@ -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})

View File

@ -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)

View File

@ -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)

View File

@ -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

135
jsonrpc/utils.py 100644
View File

@ -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)

View File

@ -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"

447
websocket/_abnf.py 100644
View File

@ -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]

351
websocket/_app.py 100644
View File

@ -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)

View File

@ -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())]))

515
websocket/_core.py 100644
View File

@ -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

View File

@ -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

View File

@ -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()

328
websocket/_http.py 100644
View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

163
websocket/_url.py 100644
View File

@ -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

110
websocket/_utils.py 100644
View File

@ -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

View File

View File

@ -0,0 +1,6 @@
HTTP/1.1 101 WebSocket Protocol Handshake
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
some_header: something

View File

@ -0,0 +1,6 @@
HTTP/1.1 101 WebSocket Protocol Handshake
Connection: Upgrade
Upgrade WebSocket
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
some_header: something

View File

@ -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"), "")

View File

@ -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()

View File

@ -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 <http://en.wikipedia.org/wiki/Server_Name_Indication>`_?
---------------------------------------------------------------------------
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.*

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
six
backports.ssl_match_hostname

View File

@ -0,0 +1 @@
websocket