Skip to content

Commit f8984a8

Browse files
committed
Basic websocket support
1 parent c31f3df commit f8984a8

File tree

6 files changed

+138
-3
lines changed

6 files changed

+138
-3
lines changed

pook/interceptors/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from .websocket import WebSocketInterceptor
23
from .urllib3 import Urllib3Interceptor
34
from .http import HTTPClientInterceptor
45
from .base import BaseInterceptor
@@ -7,6 +8,7 @@
78
__all__ = (
89
'interceptors', 'add', 'get',
910
'BaseInterceptor',
11+
'WebSocketInterceptor',
1012
'Urllib3Interceptor',
1113
'HTTPClientInterceptor',
1214
'AIOHTTPInterceptor',
@@ -15,7 +17,8 @@
1517
# Store built-in interceptors in pook.
1618
interceptors = [
1719
Urllib3Interceptor,
18-
HTTPClientInterceptor
20+
HTTPClientInterceptor,
21+
WebSocketInterceptor
1922
]
2023

2124
# Import aiohttp in modern Python runtimes

pook/interceptors/websocket.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from ..request import Request
2+
from .base import BaseInterceptor
3+
4+
# Support Python 2/3
5+
try:
6+
import mock
7+
except Exception:
8+
from unittest import mock
9+
10+
11+
class MockFrame(object):
12+
def __init__(self, opcode, data, fin=1):
13+
self.opcode = opcode
14+
self.data = data
15+
self.fin = fin
16+
17+
18+
class MockSocket(object):
19+
def fileno(self):
20+
return 0
21+
22+
def gettimeout(self):
23+
return 0
24+
25+
26+
class MockHandshakeResponse(object):
27+
28+
def __init__(self, status, headers, subprotocol):
29+
self.status = status
30+
self.headers = headers
31+
self.subprotocol = subprotocol
32+
33+
34+
class WebSocketInterceptor(BaseInterceptor):
35+
36+
def _handshake(self, sock, hostname, port, resource, **options):
37+
return MockHandshakeResponse(200, {}, "")
38+
39+
def _connect(self, url, options, proxy, socket):
40+
req = Request()
41+
req.headers = {} # TODO
42+
req.url = url
43+
44+
# TODO does this work multithreaded?!?
45+
self._mock = self.engine.match(req)
46+
if not self._mock:
47+
# we cannot forward, as we have mocked away the connection
48+
raise ValueError(
49+
"Request to '%s' could not be matched or forwarded" % url
50+
)
51+
52+
# make the body always a list to simplify our lives
53+
body = self._mock._response._body
54+
if not isinstance(body, list):
55+
self._mock._response._body = [body]
56+
57+
sock = MockSocket()
58+
addr = ("hostname", "port", "resource")
59+
60+
return sock, addr
61+
62+
def _send(self, data):
63+
# mock as if all data has been sent
64+
return len(data)
65+
66+
def _recv_frame(self):
67+
# alias
68+
body = self._mock._response._body
69+
70+
idx = getattr(self, "_data_index", 0)
71+
if len(body) >= idx:
72+
# close frame
73+
return MockFrame(0x8, None)
74+
75+
# data frame
76+
self._data_index = idx + 1
77+
return MockFrame(0x1, body[idx].encode("utf-8"), 1)
78+
79+
def _patch(self, path, handler):
80+
try:
81+
# Create a new patcher for Urllib3 urlopen function
82+
# used as entry point for all the HTTP communications
83+
patcher = mock.patch(path, handler)
84+
# Retrieve original patched function that we might need for real
85+
# networking
86+
# request = patcher.get_original()[0]
87+
# Start patching function calls
88+
patcher.start()
89+
except Exception:
90+
# Exceptions may accur due to missing package
91+
# Ignore all the exceptions for now
92+
pass
93+
else:
94+
self.patchers.append(patcher)
95+
96+
def activate(self):
97+
"""
98+
Activates the traffic interceptor.
99+
This method must be implemented by any interceptor.
100+
"""
101+
patches = [
102+
('websocket._core.connect', self._connect),
103+
('websocket._core.handshake', self._handshake),
104+
('websocket.WebSocket._send', self._send),
105+
('websocket.WebSocket.recv_frame', self._recv_frame),
106+
]
107+
108+
[self._patch(path, handler) for path, handler in patches]
109+
110+
def disable(self):
111+
"""
112+
Disables the traffic interceptor.
113+
This method must be implemented by any interceptor.
114+
"""
115+
[patch.stop() for patch in self.patchers]

pook/matchers/url.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
else: # Python 3
1111
from urllib.parse import urlparse
1212

13-
# URI protocol test regular expression
14-
protoregex = re.compile('^http[s]?://', re.IGNORECASE)
13+
# URI protocol test regular expression (without group capture)
14+
protoregex = re.compile('^(?:ws|http)[s]?://', re.IGNORECASE)
1515

1616

1717
class URLMatcher(BaseMatcher):

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ urllib3>=1.24.2
1212
bumpversion~=0.5.3
1313
aiohttp~=1.1.5 ; python_version >= '3.4.2'
1414
mocket~=1.6.0
15+
websocket~=0.57.0
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from websocket import WebSocket
2+
import pook
3+
4+
5+
@pook.on
6+
def test_websocket():
7+
(pook.get('ws://some-non-existing.org')
8+
.reply(204)
9+
.body('test'))
10+
11+
socket = WebSocket()
12+
socket.connect('ws://some-non-existing.org/', header=['x-custom: header'])
13+
socket.send('test')

tests/unit/matchers/url_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def test_url_matcher_urlparse():
3232
('http://foo.com/foo/bar', 'http://foo.com/foo/bar', True),
3333
('http://foo.com/foo/bar/baz', 'http://foo.com/foo/bar/baz', True),
3434
('http://foo.com/foo?x=y&z=w', 'http://foo.com/foo?x=y&z=w', True),
35+
('ws://foo.com', 'ws://foo.com', True),
36+
('wss://foo.com', 'wss://foo.com', True),
3537

3638
# Invalid cases
3739
('http://foo.com', 'http://bar.com', False),
@@ -41,6 +43,7 @@ def test_url_matcher_urlparse():
4143
('http://foo.com/foo/bar', 'http://foo.com/bar/foo', False),
4244
('http://foo.com/foo/bar/baz', 'http://foo.com/baz/bar/foo', False),
4345
('http://foo.com/foo?x=y&z=w', 'http://foo.com/foo?x=x&y=y', False),
46+
('ws://foo.com', 'wss://foo.com', False),
4447
))
4548

4649

0 commit comments

Comments
 (0)