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: 4242801316e12c55e5b7c626331fbefad2e15e0cpull/582/head
parent
9a79df8a8a
commit
342bb13bff
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
backports
|
|
@ -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__)
|
|
@ -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")
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
jsonrpc
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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)),
|
||||
]
|
|
@ -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))
|
|
@ -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
|
|
@ -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}'
|
||||
))
|
|
@ -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))
|
|
@ -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
|
||||
))
|
|
@ -0,0 +1 @@
|
|||
""" Tets base JSON-RPC structures."""
|
|
@ -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"])
|
|
@ -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"},
|
||||
])
|
|
@ -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})
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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"
|
|
@ -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]
|
|
@ -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)
|
|
@ -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())]))
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||
Connection: Upgrade
|
||||
Upgrade: WebSocket
|
||||
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||
some_header: something
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||
Connection: Upgrade
|
||||
Upgrade WebSocket
|
||||
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||
some_header: something
|
||||
|
|
@ -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"), "")
|
|
@ -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()
|
|
@ -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.*
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
six
|
||||
backports.ssl_match_hostname
|
|
@ -0,0 +1 @@
|
|||
websocket
|
Loading…
Reference in New Issue