commit
be5c2aef3a
69 changed files with 8796 additions and 0 deletions
@ -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,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,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,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