Hello community, here is the log from the commit of package python3-ws4py for openSUSE:Factory checked in at 2016-06-25 02:23:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python3-ws4py (Old) and /work/SRC/openSUSE:Factory/.python3-ws4py.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python3-ws4py" Changes: -------- --- /work/SRC/openSUSE:Factory/python3-ws4py/python3-ws4py.changes 2016-05-25 21:26:53.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.python3-ws4py.new/python3-ws4py.changes 2016-06-25 02:23:58.000000000 +0200 @@ -1,0 +2,9 @@ +Thu Jun 23 15:43:25 UTC 2016 - arun@gmx.de + +- specfile: + * include python3-setuptools + +- update to version 0.3.5: + (no changelog available) + +------------------------------------------------------------------- @@ -6 +14,0 @@ - Old: ---- ws4py-0.3.4.tar.gz New: ---- ws4py-0.3.5.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python3-ws4py.spec ++++++ --- /var/tmp/diff_new_pack.6DkLys/_old 2016-06-25 02:23:59.000000000 +0200 +++ /var/tmp/diff_new_pack.6DkLys/_new 2016-06-25 02:23:59.000000000 +0200 @@ -17,7 +17,7 @@ Name: python3-ws4py -Version: 0.3.4 +Version: 0.3.5 Release: 0 Summary: WebSocket for Python License: BSD-2-Clause @@ -26,6 +26,7 @@ Source0: https://files.pythonhosted.org/packages/source/w/ws4py/ws4py-%{version}.tar.gz BuildArch: noarch BuildRequires: python3-devel +BuildRequires: python3-setuptools BuildRoot: %{_tmppath}/%{name}-%{version}-build %description ++++++ ws4py-0.3.4.tar.gz -> ws4py-0.3.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/PKG-INFO new/ws4py-0.3.5/PKG-INFO --- old/ws4py-0.3.4/PKG-INFO 2014-03-30 12:28:57.000000000 +0200 +++ new/ws4py-0.3.5/PKG-INFO 2016-06-05 21:28:30.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: ws4py -Version: 0.3.4 +Version: 0.3.5 Summary: WebSocket client and server library for Python 2 and 3 as well as PyPy Home-page: https://github.com/Lawouach/WebSocket-for-Python Author: Sylvain Hellegouarch @@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Communications diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/setup.py new/ws4py-0.3.5/setup.py --- old/ws4py-0.3.4/setup.py 2014-03-30 12:23:02.000000000 +0200 +++ new/ws4py-0.3.5/setup.py 2016-06-05 21:07:56.000000000 +0200 @@ -29,7 +29,7 @@ return amended_modules setup(name = "ws4py", - version = '0.3.4', + version = '0.3.5', description = "WebSocket client and server library for Python 2 and 3 as well as PyPy", maintainer = "Sylvain Hellegouarch", maintainer_email = "sh@defuze.org", @@ -51,6 +51,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_cherrypy.py new/ws4py-0.3.5/test/test_cherrypy.py --- old/ws4py-0.3.4/test/test_cherrypy.py 2014-01-25 22:30:19.000000000 +0100 +++ new/ws4py-0.3.5/test/test_cherrypy.py 2016-06-05 17:59:03.000000000 +0200 @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import os +import socket import time import unittest @@ -65,7 +66,7 @@ manager = cherrypy.engine.websocket.manager self.assertEqual(len(manager), 0) - s = MagicMock() + s = MagicMock(spec=socket.socket) s.recv.return_value = Frame(opcode=OPCODE_TEXT, body=b'hello', fin=1, masking_key=os.urandom(4)).build() h = EchoWebSocket(s, [], []) @@ -87,7 +88,7 @@ # the poller runs a thread, give it time to get there time.sleep(1) - + # TODO: Implement a fake poller so that works... self.assertEqual(len(manager), 0) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_client.py new/ws4py-0.3.5/test/test_client.py --- old/ws4py-0.3.4/test/test_client.py 2014-01-25 22:30:07.000000000 +0100 +++ new/ws4py-0.3.5/test/test_client.py 2016-06-05 17:59:03.000000000 +0200 @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +from base64 import b64encode +from hashlib import sha1 +import os +import socket import time import unittest @@ -6,8 +10,13 @@ from ws4py.manager import WebSocketManager from ws4py.websocket import WebSocket +from ws4py import WS_KEY +from ws4py.exc import HandshakeError +from ws4py.framing import Frame, OPCODE_TEXT, OPCODE_CLOSE +from ws4py.messaging import CloseControlMessage from ws4py.client import WebSocketBaseClient - +from ws4py.client.threadedclient import WebSocketClient + class BasicClientTest(unittest.TestCase): def test_invalid_hostname_in_url(self): self.assertRaises(ValueError, WebSocketBaseClient, url="qsdfqsd65qsd354") @@ -15,11 +24,244 @@ def test_invalid_scheme_in_url(self): self.assertRaises(ValueError, WebSocketBaseClient, url="ftp://localhost") + def test_invalid_hostname_in_url(self): + self.assertRaises(ValueError, WebSocketBaseClient, url="ftp://?/") + + def test_parse_unix_schemes(self): + c = WebSocketBaseClient(url="ws+unix:///my.socket") + self.assertEqual(c.scheme, "ws+unix") + self.assertEqual(c.host, "localhost") + self.assertIsNone(c.port) + self.assertEqual(c.unix_socket_path, "/my.socket") + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, "/my.socket") + + c = WebSocketBaseClient(url="wss+unix:///my.socket") + self.assertEqual(c.scheme, "wss+unix") + self.assertEqual(c.host, "localhost") + self.assertIsNone(c.port) + self.assertEqual(c.unix_socket_path, "/my.socket") + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, "/my.socket") + + def test_parse_ws_scheme(self): + c = WebSocketBaseClient(url="ws://127.0.0.1/") + self.assertEqual(c.scheme, "ws") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 80) + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, ("127.0.0.1", 80)) + + def test_parse_ws_scheme_when_missing_resource(self): + c = WebSocketBaseClient(url="ws://127.0.0.1") + self.assertEqual(c.scheme, "ws") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 80) + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, ("127.0.0.1", 80)) + + def test_parse_ws_scheme_with_port(self): + c = WebSocketBaseClient(url="ws://127.0.0.1:9090") + self.assertEqual(c.scheme, "ws") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 9090) + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, ("127.0.0.1", 9090)) + + def test_parse_ws_scheme_with_query_string(self): + c = WebSocketBaseClient(url="ws://127.0.0.1/?token=value") + self.assertEqual(c.scheme, "ws") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 80) + self.assertEqual(c.resource, "/?token=value") + self.assertEqual(c.bind_addr, ("127.0.0.1", 80)) + + def test_parse_wss_scheme(self): + c = WebSocketBaseClient(url="wss://127.0.0.1/") + self.assertEqual(c.scheme, "wss") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 443) + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + + def test_parse_wss_scheme_when_missing_resource(self): + c = WebSocketBaseClient(url="wss://127.0.0.1") + self.assertEqual(c.scheme, "wss") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 443) + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + + def test_parse_wss_scheme_with_port(self): + c = WebSocketBaseClient(url="wss://127.0.0.1:9090") + self.assertEqual(c.scheme, "wss") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 9090) + self.assertEqual(c.resource, "/") + self.assertEqual(c.bind_addr, ("127.0.0.1", 9090)) + + def test_parse_wss_scheme_with_query_string(self): + c = WebSocketBaseClient(url="wss://127.0.0.1/?token=value") + self.assertEqual(c.scheme, "wss") + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 443) + self.assertEqual(c.resource, "/?token=value") + self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + + @patch('ws4py.client.socket') + def test_connect_and_close(self, sock): + + s = MagicMock() + sock.socket.return_value = s + sock.getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", + ("127.0.0.1", 80, 0, 0))] + + c = WebSocketBaseClient(url="ws://127.0.0.1/?token=value") + + s.recv.return_value = b"\r\n".join([ + b"HTTP/1.1 101 Switching Protocols", + b"Connection: Upgrade", + b"Sec-Websocket-Version: 13", + b"Content-Type: text/plain;charset=utf-8", + b"Sec-Websocket-Accept: " + b64encode(sha1(c.key + WS_KEY).digest()), + b"Upgrade: websocket", + b"Date: Sun, 26 Jul 2015 12:32:55 GMT", + b"Server: ws4py/test", + b"\r\n" + ]) + + c.connect() + s.connect.assert_called_once_with(("127.0.0.1", 80)) + + s.reset_mock() + c.close(code=1006, reason="boom") + args = s.sendall.call_args_list[0] + f = Frame() + f.parser.send(args[0][0]) + f.parser.close() + self.assertIn(b'boom', f.unmask(f.body)) + + @patch('ws4py.client.socket') + def test_empty_response(self, sock): + + s = MagicMock() + sock.socket.return_value = s + sock.getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", + ("127.0.0.1", 80, 0, 0))] + + c = WebSocketBaseClient(url="ws://127.0.0.1/?token=value") + + s.recv.return_value = b"" + self.assertRaises(HandshakeError, c.connect) + s.shutdown.assert_called_once_with(socket.SHUT_RDWR) + s.close.assert_called_once_with() + + @patch('ws4py.client.socket') + def test_invdalid_response_code(self, sock): + + s = MagicMock() + sock.socket.return_value = s + sock.getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", + ("127.0.0.1", 80, 0, 0))] + + c = WebSocketBaseClient(url="ws://127.0.0.1/?token=value") + + s.recv.return_value = b"\r\n".join([ + b"HTTP/1.1 200 Switching Protocols", + b"Connection: Upgrade", + b"Sec-Websocket-Version: 13", + b"Content-Type: text/plain;charset=utf-8", + b"Sec-Websocket-Accept: " + b64encode(sha1(c.key + WS_KEY).digest()), + b"Upgrade: websocket", + b"Date: Sun, 26 Jul 2015 12:32:55 GMT", + b"Server: ws4py/test", + b"\r\n" + ]) + + self.assertRaises(HandshakeError, c.connect) + s.shutdown.assert_called_once_with(socket.SHUT_RDWR) + s.close.assert_called_once_with() + + @patch('ws4py.client.socket') + def test_invalid_response_headers(self, sock): + + for key_header, invalid_value in ((b'upgrade', b'boom'), + (b'connection', b'bim')): + s = MagicMock() + sock.socket.return_value = s + sock.getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", + ("127.0.0.1", 80, 0, 0))] + c = WebSocketBaseClient(url="ws://127.0.0.1/?token=value") + + status_line = b"HTTP/1.1 101 Switching Protocols" + headers = { + b"connection": b"Upgrade", + b"Sec-Websocket-Version": b"13", + b"Content-Type": b"text/plain;charset=utf-8", + b"Sec-Websocket-Accept": b64encode(sha1(c.key + WS_KEY).digest()), + b"upgrade": b"websocket", + b"Date": b"Sun, 26 Jul 2015 12:32:55 GMT", + b"Server": b"ws4py/test" + } + + headers[key_header] = invalid_value + + request = [status_line] + [k + b" : " + v for (k, v) in headers.items()] + [b'\r\n'] + s.recv.return_value = b"\r\n".join(request) + + self.assertRaises(HandshakeError, c.connect) + s.shutdown.assert_called_once_with(socket.SHUT_RDWR) + s.close.assert_called_once_with() + sock.reset_mock() + +class ThreadedClientTest(unittest.TestCase): + @patch('ws4py.client.socket') + def test_thread_is_started_once_connected(self, sock): + s = MagicMock(spec=socket.socket) + sock.socket.return_value = s + sock.getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", + ("127.0.0.1", 80, 0, 0))] + + c = WebSocketClient(url="ws://127.0.0.1/") + + def exchange1(*args, **kwargs): + yield b"\r\n".join([ + b"HTTP/1.1 101 Switching Protocols", + b"Connection: Upgrade", + b"Sec-Websocket-Version: 13", + b"Content-Type: text/plain;charset=utf-8", + b"Sec-Websocket-Accept: " + b64encode(sha1(c.key + WS_KEY).digest()), + b"Upgrade: websocket", + b"Date: Sun, 26 Jul 2015 12:32:55 GMT", + b"Server: ws4py/test", + b"\r\n" + ]) + + for i in range(100): + time.sleep(0.1) + yield Frame(opcode=OPCODE_TEXT, body=b'hello', + fin=1).build() + + s.recv.side_effect = exchange1() + self.assertFalse(c._th.is_alive()) + + c.connect() + time.sleep(0.5) + self.assertTrue(c._th.is_alive()) + + def exchange2(*args, **kwargs): + yield Frame(opcode=OPCODE_CLOSE, body=b'', + fin=1).build() + s.recv.side_effect = exchange2() + time.sleep(0.5) + self.assertFalse(c._th.is_alive()) + if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() - for testcase in [BasicClientTest]: + for testcase in [BasicClientTest, + ThreadedClientTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_frame.py new/ws4py-0.3.5/test/test_frame.py --- old/ws4py-0.3.4/test/test_frame.py 2014-03-29 13:54:10.000000000 +0100 +++ new/ws4py-0.3.5/test/test_frame.py 2016-06-05 17:59:03.000000000 +0200 @@ -3,6 +3,7 @@ import unittest import types import random +from struct import pack, unpack from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ @@ -238,6 +239,31 @@ f.parser.send(bytes[12:]) self.assertEqual(f.unmask(f.body), body) + def test_frame_too_large_with_7_bit_length(self): + header = pack('!B', ((1 << 7) + | (0 << 6) + | (0 << 5) + | (0 << 4) + | OPCODE_TEXT)) + header += pack('!B', 127) + pack('!Q', 1 << 63) + b = bytes(header + b'') + f = Frame() + self.assertRaises(FrameTooLargeException, f.parser.send, b) + + def test_not_sensitive_to_overflow(self): + header = pack('!B', ((1 << 7) + | (0 << 6) + | (0 << 5) + | (0 << 4) + | OPCODE_TEXT)) + header += pack('!B', 126) + pack('!H', 256) + b = bytes(header + b'*' * 512) + f = Frame() + f.parser.send(b) + # even though we tried to inject 512 bytes, we + # still only read 256 + self.assertEqual(len(f.body), 256) + def test_frame_sized_126(self): body = b'*'*256 bytes = Frame(opcode=OPCODE_TEXT, body=body, fin=1).build() @@ -271,7 +297,7 @@ # parse the rest of our data f.parser.send(bytes[10:]) self.assertEqual(f.body, body) - + if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_logger.py new/ws4py-0.3.5/test/test_logger.py --- old/ws4py-0.3.4/test/test_logger.py 1970-01-01 01:00:00.000000000 +0100 +++ new/ws4py-0.3.5/test/test_logger.py 2016-06-05 17:59:03.000000000 +0200 @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +import logging +import logging.handlers as handlers +import os, os.path +import unittest +import sys + +from ws4py import configure_logger + +def clean_logger(): + logger = logging.getLogger('ws4py') + for handler in list(logger.handlers): + try: + handler.close() + except KeyError: + pass + logger.removeHandler(handler) + +class WSTestLogger(unittest.TestCase): + def tearDown(self): + clean_logger() + + def test_named_logger(self): + logger = configure_logger(stdout=False, filepath='./my.log') + + logger = logging.getLogger('ws4py') + self.assertEqual(logger.getEffectiveLevel(), logging.INFO) + + def test_level(self): + logger = configure_logger(stdout=True, filepath='./my.log', + level=logging.DEBUG) + + self.assertEqual(logger.getEffectiveLevel(), logging.DEBUG) + for handler in logger.handlers: + self.assertEqual(handler.level, logging.DEBUG) + + def test_file_logger(self): + filepath = os.path.abspath('./my.log') + logger = configure_logger(stdout=False, filepath=filepath) + for handler in logger.handlers: + if isinstance(handler, handlers.RotatingFileHandler): + self.assertEqual(handler.baseFilename, filepath) + self.assertEqual(handler.stream.name, filepath) + break + else: + self.fail("File logger not configured") + + def test_stdout_logger(self): + logger = configure_logger() + for handler in logger.handlers: + if isinstance(handler, logging.StreamHandler) and not\ + isinstance(handler, handlers.RotatingFileHandler): + self.assertTrue(handler.stream is sys.stdout) + break + else: + self.fail("Stream logger not configured") + +if __name__ == '__main__': + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for testcase in [WSTestLogger]: + tests = loader.loadTestsFromTestCase(testcase) + suite.addTests(tests) + unittest.TextTestRunner(verbosity=2).run(suite) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_manager.py new/ws4py-0.3.5/test/test_manager.py --- old/ws4py-0.3.4/test/test_manager.py 2014-01-25 22:30:07.000000000 +0100 +++ new/ws4py-0.3.5/test/test_manager.py 2016-06-05 17:59:03.000000000 +0200 @@ -1,10 +1,17 @@ # -*- coding: utf-8 -*- import time +import itertools import unittest +try: + from itertools import izip_longest as zip_longest +except ImportError: + from itertools import zip_longest + from mock import MagicMock, call, patch -from ws4py.manager import WebSocketManager +from ws4py.manager import WebSocketManager, SelectPoller,\ + EPollPoller from ws4py.websocket import WebSocket class WSManagerTest(unittest.TestCase): @@ -16,10 +23,10 @@ ws.sock.fileno.return_value = 1 m.add(ws) - m.poller.register.assert_call_once_with(ws) + m.poller.register.assert_called_once_with(1) m.remove(ws) - m.poller.unregister.assert_call_once_with(ws) + m.poller.unregister.assert_called_once_with(1) @patch('ws4py.manager.SelectPoller') def test_cannot_add_websocket_more_than_once(self, MockSelectPoller): @@ -49,7 +56,7 @@ self.assertEqual(len(m), 1) m.remove(ws) self.assertEqual(len(m), 0) - m.poller.unregister.assert_call_once_with(ws) + m.poller.unregister.assert_called_once_with(1) m.poller.reset_mock() m.remove(ws) @@ -97,8 +104,9 @@ m.add(ws) m.start() + time.sleep(0.2) - ws.terminate.assert_call_once_with() + ws.terminate.assert_called_once_with() m.stop() @@ -109,7 +117,7 @@ ws = MagicMock() m.add(ws) m.close_all() - ws.terminate.assert_call_once_with(1001, 'Server is shutting down') + ws.close.assert_called_once_with(code=1001, reason='Server is shutting down') @patch('ws4py.manager.SelectPoller') def test_broadcast(self, MockSelectPoller): @@ -120,7 +128,7 @@ m.add(ws) m.broadcast(b'hello there') - ws.send.assert_call_once_with(b'hello there') + ws.send.assert_called_once_with(b'hello there', False) @patch('ws4py.manager.SelectPoller') def test_broadcast_failure_must_not_break_caller(self, MockSelectPoller): @@ -135,11 +143,144 @@ m.broadcast(b'hello there') except: self.fail("Broadcasting shouldn't have failed") - + +class WSSelectPollerTest(unittest.TestCase): + @patch('ws4py.manager.select') + def test_release_poller(self, select): + poller = SelectPoller() + select.select.return_value = (poller._fds, None, None) + + poller.register(0) + poller.register(1) + fd = poller.poll() + self.assertEqual(fd, [0, 1]) + + poller.unregister(0) + fd = poller.poll() + self.assertEqual(fd, [1]) + + poller.release() + fd = poller.poll() + self.assertEqual(fd, []) + + def test_timeout_when_no_registered_fds(self): + poller = SelectPoller(timeout=0.5) + a = time.time() + fd = poller.poll() + b = time.time() + self.assertEqual(fd, []) + + d = b - a + if not (0.48 < d < 0.52): + self.fail("Did not wait for the appropriate amount of time: %f" % d) + + @patch('ws4py.manager.select') + def test_register_twice_does_not_duplicate_fd(self, select): + poller = SelectPoller() + select.select.return_value = (poller._fds, None, None) + poller.register(0) + poller.register(0) + fd = poller.poll() + self.assertEqual(fd, [0]) + + def test_unregister_twice_has_no_side_effect(self): + poller = SelectPoller() + poller.register(0) + poller.unregister(0) + try: + poller.unregister(0) + except Exception as ex: + self.fail("Shouldn't have failed: %s" % ex) + + +class FakeEpoll(object): + def __init__(self): + self.fds = [] + + def close(self): + self.fds = [] + + def register(self, fd, mask): + if fd in self.fds: + raise IOError("Already registered") + + self.fds.append(fd) + + def unregister(self, fd): + # epoll's documentation doesn't say anything + # about removing fds that are not registered + # yet/any longer + if fd in self.fds: + self.fds.remove(fd) + + def poll(self, timeout): + # this isn't really what's happening + # inside but we're not testing + # epoll afterall + if not self.fds: + time.sleep(timeout) + return [] + + # fake EPOLLIN + return zip_longest(self.fds, '', fillvalue=1) + +class WSEPollPollerTest(unittest.TestCase): + @patch('ws4py.manager.select') + def test_release_poller(self, select): + select.epoll.return_value = FakeEpoll() + poller = EPollPoller() + + poller.register(0) + poller.register(1) + fd = poller.poll() + self.assertEqual(list(fd), [0, 1]) + + poller.unregister(0) + fd = poller.poll() + self.assertEqual(list(fd), [1]) + + poller.release() + fd = poller.poll() + self.assertEqual(list(fd), []) + + @patch('ws4py.manager.select') + def test_timeout_when_no_registered_fds(self, select): + select.epoll.return_value = FakeEpoll() + poller = EPollPoller(timeout=0.5) + + a = time.time() + fd = list(poller.poll()) + b = time.time() + self.assertEqual(fd, []) + + d = b - a + if not (0.48 < d < 0.52): + self.fail("Did not wait for the appropriate amount of time: %f" % d) + + @patch('ws4py.manager.select') + def test_register_twice_does_not_duplicate_fd(self, select): + select.epoll.return_value = FakeEpoll() + poller = EPollPoller() + poller.register(0) + poller.register(0) # IOError is swallowed + fd = poller.poll() + self.assertEqual(list(fd), [0]) + + @patch('ws4py.manager.select') + def test_unregister_twice_has_no_side_effect(self, select): + select.epoll.return_value = FakeEpoll() + poller = EPollPoller() + poller.register(0) + poller.unregister(0) + try: + poller.unregister(0) + except Exception as ex: + self.fail("Shouldn't have failed: %s" % ex) + if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() - for testcase in [WSManagerTest]: + for testcase in [WSManagerTest, WSSelectPollerTest]: tests = loader.loadTestsFromTestCase(testcase) suite.addTests(tests) unittest.TextTestRunner(verbosity=2).run(suite) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_utils.py new/ws4py-0.3.5/test/test_utils.py --- old/ws4py-0.3.4/test/test_utils.py 1970-01-01 01:00:00.000000000 +0100 +++ new/ws4py-0.3.5/test/test_utils.py 2016-06-05 17:59:03.000000000 +0200 @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +import unittest + +from ws4py import format_addresses +from ws4py.websocket import WebSocket +from mock import MagicMock + +class WSUtilities(unittest.TestCase): + def test_format_address(self): + m = MagicMock() + m.getsockname.return_value = ('127.0.0.1', 52300, None, None) + m.getpeername.return_value = ('127.0.0.1', 4800, None, None) + ws = WebSocket(sock=m) + + log = format_addresses(ws) + self.assertEqual(log, "[Local => 127.0.0.1:52300 | Remote => 127.0.0.1:4800]") + +if __name__ == '__main__': + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for testcase in [WSUtilities]: + tests = loader.loadTestsFromTestCase(testcase) + suite.addTests(tests) + unittest.TextTestRunner(verbosity=2).run(suite) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/test/test_websocket.py new/ws4py-0.3.5/test/test_websocket.py --- old/ws4py-0.3.4/test/test_websocket.py 2014-01-25 22:30:07.000000000 +0100 +++ new/ws4py-0.3.5/test/test_websocket.py 2016-06-05 17:59:03.000000000 +0200 @@ -103,7 +103,7 @@ self.assertFalse(ws.once()) def test_no_bytes_were_read(self): - m = MagicMock() + m = MagicMock(spec=socket.socket) m.recv.return_value = b'' ws = WebSocket(sock=m) self.assertFalse(ws.once()) @@ -169,6 +169,14 @@ ws.process(b'unused for this test') c.assert_called_once_with(1000, b'test closing') + def test_sending_ping(self): + tm = PingControlMessage("hello").single(mask=False) + + m = MagicMock() + ws = WebSocket(sock=m) + ws.ping("hello") + m.sendall.assert_called_once_with(tm) + if __name__ == '__main__': suite = unittest.TestSuite() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/__init__.py new/ws4py-0.3.5/ws4py/__init__.py --- old/ws4py-0.3.4/ws4py/__init__.py 2014-03-29 16:16:55.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/__init__.py 2016-06-05 17:59:03.000000000 +0200 @@ -27,9 +27,10 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import logging +import logging.handlers as handlers __author__ = "Sylvain Hellegouarch" -__version__ = "0.3.4" +__version__ = "0.3.5" __all__ = ['WS_KEY', 'WS_VERSION', 'configure_logger', 'format_addresses'] WS_KEY = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/async_websocket.py new/ws4py-0.3.5/ws4py/async_websocket.py --- old/ws4py-0.3.4/ws4py/async_websocket.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/async_websocket.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -__doc__ = """ -WebSocket implementation that relies on two new Python -features: - -* asyncio to provide the high-level interface above transports -* yield from to delegate to the reading stream whenever more - bytes are required - -You can use these implementations in that context -and benefit from those features whilst using ws4py. - -Strictly speaking this module probably doesn't have to -be called async_websocket but it feels this will be its typical -usage and is probably more readable than -delegated_generator_websocket_on_top_of_asyncio.py -""" -import asyncio -import types - -from ws4py.websocket import WebSocket as _WebSocket -from ws4py.messaging import Message - -__all__ = ['WebSocket', 'EchoWebSocket'] - -class WebSocket(_WebSocket): - def __init__(self, proto): - """ - A :pep:`3156` ready websocket handler that works - well in a coroutine-aware loop such as the one provided - by the asyncio module. - - The provided `proto` instance is a - :class:`asyncio.Protocol` subclass instance that will - be used internally to read and write from the - underlying transport. - - Because the base :class:`ws4py.websocket.WebSocket` - class is still coupled a bit to the socket interface, - we have to override a little more than necessary - to play nice with the :pep:`3156` interface. Hopefully, - some day this will be cleaned out. - """ - _WebSocket.__init__(self, None) - self.started = False - self.proto = proto - - @property - def local_address(self): - """ - Local endpoint address as a tuple - """ - if not self._local_address: - self._local_address = self.proto.reader.transport.get_extra_info('sockname') - if len(self._local_address) == 4: - self._local_address = self._local_address[:2] - return self._local_address - - @property - def peer_address(self): - """ - Peer endpoint address as a tuple - """ - if not self._peer_address: - self._peer_address = self.proto.reader.transport.get_extra_info('peername') - if len(self._peer_address) == 4: - self._peer_address = self._peer_address[:2] - return self._peer_address - - def once(self): - """ - The base class directly is used in conjunction with - the :class:`ws4py.manager.WebSocketManager` which is - not actually used with the asyncio implementation - of ws4py. So let's make it clear it shan't be used. - """ - raise NotImplemented() - - def close_connection(self): - """ - Close the underlying transport - """ - @asyncio.coroutine - def closeit(): - yield from self.proto.writer.drain() - self.proto.writer.close() - asyncio.async(closeit()) - - def _write(self, data): - """ - Write to the underlying transport - """ - @asyncio.coroutine - def sendit(data): - self.proto.writer.write(data) - yield from self.proto.writer.drain() - asyncio.async(sendit(data)) - - @asyncio.coroutine - def run(self): - """ - Coroutine that runs until the websocket - exchange is terminated. It also calls the - `opened()` method to indicate the exchange - has started. - """ - self.started = True - try: - self.opened() - reader = self.proto.reader - while True: - data = yield from reader.read(self.reading_buffer_size) - if not self.process(data): - return False - finally: - self.terminate() - - return True - -class EchoWebSocket(WebSocket): - def received_message(self, message): - """ - Automatically sends back the provided ``message`` to - its originating endpoint. - """ - self.send(message.data, message.is_binary) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/client/__init__.py new/ws4py-0.3.5/ws4py/client/__init__.py --- old/ws4py-0.3.4/ws4py/client/__init__.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/client/__init__.py 2016-06-05 17:59:03.000000000 +0200 @@ -205,7 +205,8 @@ if self.scheme == "wss": # default port is now 443; upgrade self.sender to send ssl self.sock = ssl.wrap_socket(self.sock, **self.ssl_options) - + self._is_secure = True + self.sock.connect(self.bind_addr) self._write(self.handshake_request) @@ -245,11 +246,10 @@ handshake. """ headers = [ - ('Host', self.host), + ('Host', '%s:%s' % (self.host, self.port)), ('Connection', 'Upgrade'), ('Upgrade', 'websocket'), ('Sec-WebSocket-Key', self.key.decode('utf-8')), - ('Origin', self.url), ('Sec-WebSocket-Version', str(max(WS_VERSION))) ] @@ -259,6 +259,19 @@ if self.extra_headers: headers.extend(self.extra_headers) + if not any(x for x in headers if x[0].lower() == 'origin'): + + scheme, url = self.url.split(":", 1) + parsed = urlsplit(url, scheme="http") + if parsed.hostname: + self.host = parsed.hostname + else: + self.host = 'localhost' + origin = scheme + '://' + parsed.hostname + if parsed.port: + origin = origin + ':' + str(parsed.port) + headers.append(('Origin', origin)) + return headers @property @@ -298,21 +311,24 @@ header = header.strip().lower() value = value.strip().lower() - if header == 'upgrade' and value != 'websocket': + if header == b'upgrade' and value != b'websocket': raise HandshakeError("Invalid Upgrade header: %s" % value) - elif header == 'connection' and value != 'upgrade': + elif header == b'connection' and value != b'upgrade': raise HandshakeError("Invalid Connection header: %s" % value) - elif header == 'sec-websocket-accept': - match = b64encode(sha1(self.key.encode('utf-8') + WS_KEY).digest()) + elif header == b'sec-websocket-accept': + match = b64encode(sha1(self.key + WS_KEY).digest()) if value != match.lower(): raise HandshakeError("Invalid challenge response: %s" % value) - elif header == 'sec-websocket-protocol': + elif header == b'sec-websocket-protocol': protocols = ','.join(value) - elif header == 'sec-websocket-extensions': + elif header == b'sec-websocket-extensions': extensions = ','.join(value) return protocols, extensions + + def handshake_ok(self): + self.opened() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/client/tornadoclient.py new/ws4py-0.3.5/ws4py/client/tornadoclient.py --- old/ws4py-0.3.4/ws4py/client/tornadoclient.py 2014-03-29 13:21:08.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/client/tornadoclient.py 2016-06-05 17:59:03.000000000 +0200 @@ -33,10 +33,10 @@ """ WebSocketBaseClient.__init__(self, url, protocols, extensions, ssl_options=ssl_options, headers=headers) - self.ssl_options["do_handshake_on_connect"] = False if self.scheme == "wss": - self.sock = ssl.wrap_socket(self.sock, **self.ssl_options) - self.io = iostream.SSLIOStream(self.sock, io_loop) + self.sock = ssl.wrap_socket(self.sock, do_handshake_on_connect=False, **self.ssl_options) + self._is_secure = True + self.io = iostream.SSLIOStream(self.sock, io_loop, ssl_options=self.ssl_options) else: self.io = iostream.IOStream(self.sock, io_loop) self.io_loop = io_loop diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/server/cherrypyserver.py new/ws4py-0.3.5/ws4py/server/cherrypyserver.py --- old/ws4py-0.3.4/ws4py/server/cherrypyserver.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/server/cherrypyserver.py 2016-06-05 17:59:03.000000000 +0200 @@ -69,7 +69,7 @@ import cherrypy from cherrypy import Tool from cherrypy.process import plugins -from cherrypy.wsgiserver import HTTPConnection, HTTPRequest +from cherrypy.wsgiserver import HTTPConnection, HTTPRequest, KnownLengthRFile from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError @@ -197,7 +197,11 @@ response.headers['Sec-WebSocket-Extensions'] = ','.join(ws_extensions) addr = (request.remote.ip, request.remote.port) - ws_conn = get_connection(request.rfile.rfile) + rfile = request.rfile.rfile + if isinstance(rfile, KnownLengthRFile): + rfile = rfile.rfile + + ws_conn = get_connection(rfile) request.ws_handler = handler_cls(ws_conn, ws_protocols, ws_extensions, request.wsgi_environ.copy(), heartbeat_freq=heartbeat_freq) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/server/geventserver.py new/ws4py-0.3.5/ws4py/server/geventserver.py --- old/ws4py-0.3.4/ws4py/server/geventserver.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/server/geventserver.py 2016-06-05 17:59:03.000000000 +0200 @@ -16,7 +16,6 @@ """ import logging -import sys import gevent from gevent.pywsgi import WSGIHandler, WSGIServer as _WSGIServer @@ -25,11 +24,13 @@ from ws4py import format_addresses from ws4py.server.wsgiutils import WebSocketWSGIApplication + logger = logging.getLogger('ws4py') __all__ = ['WebSocketWSGIHandler', 'WSGIServer', 'GEventWebSocketPool'] + class WebSocketWSGIHandler(WSGIHandler): """ A WSGI handler that will perform the :rfc:`6455` @@ -43,24 +44,25 @@ def run_application(self): upgrade_header = self.environ.get('HTTP_UPGRADE', '').lower() if upgrade_header: - try: - # Build and start the HTTP response - self.environ['ws4py.socket'] = self.socket or self.environ['wsgi.input'].rfile._sock - self.result = self.application(self.environ, self.start_response) or [] - self.process_result() - except: - raise - else: - del self.environ['ws4py.socket'] - self.socket = None - self.rfile.close() - - ws = self.environ.pop('ws4py.websocket') - if ws: - self.server.pool.track(ws) + # Build and start the HTTP response + self.environ['ws4py.socket'] = self.socket or self.environ['wsgi.input'].rfile._sock + self.result = self.application(self.environ, self.start_response) or [] + self.process_result() + del self.environ['ws4py.socket'] + self.socket = None + self.rfile.close() + + ws = self.environ.pop('ws4py.websocket', None) + if ws: + ws_greenlet = self.server.pool.track(ws) + # issue #170 + # in gevent 1.1 socket will be closed once application returns + # so let's wait for websocket handler to finish + ws_greenlet.join() else: gevent.pywsgi.WSGIHandler.run_application(self) + class GEventWebSocketPool(Pool): """ Simple pool of bound websockets. @@ -85,7 +87,8 @@ pass finally: self.discard(greenlet) - + + class WSGIServer(_WSGIServer): handler_class = WebSocketWSGIHandler @@ -106,8 +109,8 @@ self.pool.clear() _WSGIServer.stop(self, *args, **kwargs) + if __name__ == '__main__': - import os from ws4py import configure_logger configure_logger() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/server/tulipserver.py new/ws4py-0.3.5/ws4py/server/tulipserver.py --- old/ws4py-0.3.4/ws4py/server/tulipserver.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/server/tulipserver.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,220 +0,0 @@ -# -*- coding: utf-8 -*- -import base64 -from hashlib import sha1 -from email.parser import BytesHeaderParser -import io - -import asyncio - -from ws4py import WS_KEY, WS_VERSION -from ws4py.exc import HandshakeError -from ws4py.websocket import WebSocket - -LF = b'\n' -CRLF = b'\r\n' -SPACE = b' ' -EMPTY = b'' - -__all__ = ['WebSocketProtocol'] - -class WebSocketProtocol(asyncio.StreamReaderProtocol): - def __init__(self, handler_cls): - asyncio.StreamReaderProtocol.__init__(self, asyncio.StreamReader(), - self._pseudo_connected) - self.ws = handler_cls(self) - - def _pseudo_connected(self, reader, writer): - pass - - def connection_made(self, transport): - """ - A peer is now connected and we receive an instance - of the underlying :class:`asyncio.Transport`. - - We :class:`asyncio.StreamReader` is created - and the transport is associated before the - initial HTTP handshake is undertaken. - """ - #self.transport = transport - #self.stream = asyncio.StreamReader() - #self.stream.set_transport(transport) - asyncio.StreamReaderProtocol.connection_made(self, transport) - # Let make it concurrent for others to tag along - f = asyncio.async(self.handle_initial_handshake()) - f.add_done_callback(self.terminated) - - @property - def writer(self): - return self._stream_writer - - @property - def reader(self): - return self._stream_reader - - def terminated(self, f): - if f.done() and not f.cancelled(): - ex = f.exception() - if ex: - response = [b'HTTP/1.0 400 Bad Request'] - response.append(b'Content-Length: 0') - response.append(b'Connection: close') - response.append(b'') - response.append(b'') - self.writer.write(CRLF.join(response)) - self.ws.close_connection() - - def close(self): - """ - Initiate the websocket closing handshake - which will eventuall lead to the underlying - transport. - """ - self.ws.close() - - def timeout(self): - self.ws.close_connection() - if self.ws.started: - self.ws.closed(1002, "Peer connection timed-out") - - def connection_lost(self, exc): - """ - The peer connection is now, the closing - handshake won't work so let's not even try. - However let's make the websocket handler - be aware of it by calling its `closed` - method. - """ - if exc is not None: - self.ws.close_connection() - if self.ws.started: - self.ws.closed(1002, "Peer connection was lost") - - @asyncio.coroutine - def handle_initial_handshake(self): - """ - Performs the HTTP handshake described in :rfc:`6455`. Note that - this implementation is really basic and it is strongly advised - against using it in production. It would probably break for - most clients. If you want a better support for HTTP, please - use a more reliable HTTP server implemented using asyncio. - """ - request_line = yield from self.next_line() - method, uri, req_protocol = request_line.strip().split(SPACE, 2) - - # GET required - if method.upper() != b'GET': - raise HandshakeError('HTTP method must be a GET') - - headers = yield from self.read_headers() - if req_protocol == b'HTTP/1.1' and 'Host' not in headers: - raise ValueError("Missing host header") - - for key, expected_value in [('Upgrade', 'websocket'), - ('Connection', 'upgrade')]: - actual_value = headers.get(key, '').lower() - if not actual_value: - raise HandshakeError('Header %s is not defined' % str(key)) - if expected_value not in actual_value: - raise HandshakeError('Illegal value for header %s: %s' % - (key, actual_value)) - - response_headers = {} - - ws_version = WS_VERSION - version = headers.get('Sec-WebSocket-Version') - supported_versions = ', '.join([str(v) for v in ws_version]) - version_is_valid = False - if version: - try: version = int(version) - except: pass - else: version_is_valid = version in ws_version - - if not version_is_valid: - response_headers['Sec-WebSocket-Version'] = supported_versions - raise HandshakeError('Unhandled or missing WebSocket version') - - key = headers.get('Sec-WebSocket-Key') - if key: - ws_key = base64.b64decode(key.encode('utf-8')) - if len(ws_key) != 16: - raise HandshakeError("WebSocket key's length is invalid") - - protocols = [] - ws_protocols = [] - subprotocols = headers.get('Sec-WebSocket-Protocol') - if subprotocols: - for s in subprotocols.split(','): - s = s.strip() - if s in protocols: - ws_protocols.append(s) - - exts = [] - ws_extensions = [] - extensions = headers.get('Sec-WebSocket-Extensions') - if extensions: - for ext in extensions.split(','): - ext = ext.strip() - if ext in exts: - ws_extensions.append(ext) - - response = [req_protocol + b' 101 Switching Protocols'] - response.append(b'Upgrade: websocket') - response.append(b'Content-Type: text/plain') - response.append(b'Content-Length: 0') - response.append(b'Connection: Upgrade') - response.append(b'Sec-WebSocket-Version:' + bytes(str(version), 'utf-8')) - response.append(b'Sec-WebSocket-Accept:' + base64.b64encode(sha1(key.encode('utf-8') + WS_KEY).digest())) - if ws_protocols: - response.append(b'Sec-WebSocket-Protocol:' + b', '.join(ws_protocols)) - if ws_extensions: - response.append(b'Sec-WebSocket-Extensions:' + b','.join(ws_extensions)) - response.append(b'') - response.append(b'') - self.writer.write(CRLF.join(response)) - yield from self.handle_websocket() - - @asyncio.coroutine - def handle_websocket(self): - """ - Starts the websocket process until the - exchange is completed and terminated. - """ - yield from self.ws.run() - - @asyncio.coroutine - def read_headers(self): - """ - Read all HTTP headers from the HTTP request - and returns a dictionary of them. - """ - headers = b'' - while True: - line = yield from self.next_line() - headers += line - if line == CRLF: - break - return BytesHeaderParser().parsebytes(headers) - - @asyncio.coroutine - def next_line(self): - """ - Reads data until \r\n is met and then return all read - bytes. - """ - line = yield from self.reader.readline() - if not line.endswith(CRLF): - raise ValueError("Missing mandatory trailing CRLF") - return line - -if __name__ == '__main__': - from ws4py.async_websocket import EchoWebSocket - - loop = asyncio.get_event_loop() - - def start_server(): - proto_factory = lambda: WebSocketProtocol(EchoWebSocket) - return loop.create_server(proto_factory, '', 9007) - - s = loop.run_until_complete(start_server()) - print('serving on', s.sockets[0].getsockname()) - loop.run_forever() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/server/wsgirefserver.py new/ws4py-0.3.5/ws4py/server/wsgirefserver.py --- old/ws4py-0.3.4/ws4py/server/wsgirefserver.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/server/wsgirefserver.py 2016-06-05 17:59:03.000000000 +0200 @@ -52,6 +52,7 @@ """ SimpleHandler.setup_environ(self) self.environ['ws4py.socket'] = get_connection(self.environ['wsgi.input']) + self.http_version = self.environ['SERVER_PROTOCOL'].rsplit('/')[-1] def finish_response(self): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py/websocket.py new/ws4py-0.3.5/ws4py/websocket.py --- old/ws4py-0.3.4/ws4py/websocket.py 2014-01-25 22:30:21.000000000 +0100 +++ new/ws4py-0.3.5/ws4py/websocket.py 2016-06-05 17:59:03.000000000 +0200 @@ -1,14 +1,22 @@ # -*- coding: utf-8 -*- import logging import socket +import ssl import time import threading import types +try: + from OpenSSL.SSL import Error as pyOpenSSLError +except ImportError: + class pyOpenSSLError(Exception): + pass + from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError, StreamClosed from ws4py.streaming import Stream -from ws4py.messaging import Message, PongControlMessage +from ws4py.messaging import Message, PingControlMessage,\ + PongControlMessage from ws4py.compat import basestring, unicode DEFAULT_READING_SIZE = 2 @@ -98,7 +106,12 @@ """ Underlying connection. """ - + + self._is_secure = hasattr(sock, '_ssl') or hasattr(sock, '_sslobj') + """ + Tell us if the socket is secure or not. + """ + self.client_terminated = False """ Indicates if the client has been marked as terminated. @@ -210,6 +223,13 @@ finally: self.sock = None + def ping(self, message): + """ + Send a ping message to the remote peer. + The given `message` must be a unicode string. + """ + self.send(PingControlMessage(message)) + def ponged(self, pong): """ Pong message, as a :class:`messaging.PongControlMessage` instance, @@ -229,6 +249,22 @@ """ pass + def unhandled_error(self, error): + """ + Called whenever a socket, or an OS, error is trapped + by ws4py but not managed by it. The given error is + an instance of `socket.error` or `OSError`. + + Note however that application exceptions will not go + through this handler. Instead, do make sure you + protect your code appropriately in `received_message` + or `send`. + + The default behaviour of this handler is to log + the error with a message. + """ + logger.exception("Failed to receive data") + def _write(self, b): """ Trying to prevent a write operation @@ -277,6 +313,50 @@ else: raise ValueError("Unsupported type '%s' passed to send()" % type(payload)) + def _get_from_pending(self): + """ + The SSL socket object provides the same interface + as the socket interface but behaves differently. + + When data is sent over a SSL connection + more data may be read than was requested from by + the ws4py websocket object. + + In that case, the data may have been indeed read + from the underlying real socket, but not read by the + application which will expect another trigger from the + manager's polling mechanism as if more data was still on the + wire. This will happen only when new data is + sent by the other peer which means there will be + some delay before the initial read data is handled + by the application. + + Due to this, we have to rely on a non-public method + to query the internal SSL socket buffer if it has indeed + more data pending in its buffer. + + Now, some people in the Python community + `discourage https://bugs.python.org/issue21430`_ + this usage of the ``pending()`` method because it's not + the right way of dealing with such use case. They advise + `this approach https://docs.python.org/dev/library/ssl.html#notes-on-non-blocking-sockets`_ + instead. Unfortunately, this applies only if the + application can directly control the poller which is not + the case with the WebSocket abstraction here. + + We therefore rely on this `technic http://stackoverflow.com/questions/3187565/select-and-ssl-in-python`_ + which seems to be valid anyway. + + This is a bit of a shame because we have to process + more data than what wanted initially. + """ + data = b"" + pending = self.sock.pending() + while pending: + data += self.sock.recv(pending) + pending = self.sock.pending() + return data + def once(self): """ Performs the operation of reading from the underlying @@ -298,8 +378,11 @@ try: b = self.sock.recv(self.reading_buffer_size) - except socket.error: - logger.exception("Failed to receive data") + # This will only make sense with secure sockets. + if self._is_secure: + b += self._get_from_pending() + except (socket.error, OSError, pyOpenSSLError) as e: + self.unhandled_error(e) return False else: if not self.process(b): @@ -323,7 +406,7 @@ self.client_terminated = self.server_terminated = True try: - if not s.closing: + if s.closing is None: self.closed(1006, "Going away") else: self.closed(s.closing.code, s.closing.reason) @@ -362,7 +445,6 @@ self.close(s.closing.code, s.closing.reason) else: self.client_terminated = True - s = None return False if s.errors: @@ -370,7 +452,6 @@ logger.debug("Error message received (%d) '%s'" % (error.code, error.reason)) self.close(error.code, error.reason) s.errors = [] - s = None return False if s.has_message: @@ -378,7 +459,6 @@ if s.message is not None: s.message.data = None s.message = None - s = None return True if s.pings: @@ -391,7 +471,6 @@ self.ponged(pong) s.pongs = [] - s = None return True def run(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py.egg-info/PKG-INFO new/ws4py-0.3.5/ws4py.egg-info/PKG-INFO --- old/ws4py-0.3.4/ws4py.egg-info/PKG-INFO 2014-03-30 12:28:57.000000000 +0200 +++ new/ws4py-0.3.5/ws4py.egg-info/PKG-INFO 2016-06-05 21:28:29.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: ws4py -Version: 0.3.4 +Version: 0.3.5 Summary: WebSocket client and server library for Python 2 and 3 as well as PyPy Home-page: https://github.com/Lawouach/WebSocket-for-Python Author: Sylvain Hellegouarch @@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Communications diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ws4py-0.3.4/ws4py.egg-info/SOURCES.txt new/ws4py-0.3.5/ws4py.egg-info/SOURCES.txt --- old/ws4py-0.3.4/ws4py.egg-info/SOURCES.txt 2014-03-30 12:28:57.000000000 +0200 +++ new/ws4py-0.3.5/ws4py.egg-info/SOURCES.txt 2016-06-05 21:28:30.000000000 +0200 @@ -2,12 +2,13 @@ test/test_cherrypy.py test/test_client.py test/test_frame.py +test/test_logger.py test/test_manager.py test/test_messaging.py test/test_stream.py +test/test_utils.py test/test_websocket.py ws4py/__init__.py -ws4py/async_websocket.py ws4py/compat.py ws4py/exc.py ws4py/framing.py @@ -27,6 +28,5 @@ ws4py/server/__init__.py ws4py/server/cherrypyserver.py ws4py/server/geventserver.py -ws4py/server/tulipserver.py ws4py/server/wsgirefserver.py ws4py/server/wsgiutils.py \ No newline at end of file