- add comment.
This commit is contained in:
@@ -4,6 +4,8 @@ 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 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
|
License
|
||||||
============
|
============
|
||||||
|
|
||||||
@@ -73,6 +75,8 @@ JavaScript websocket-like API example::
|
|||||||
ChangeLog
|
ChangeLog
|
||||||
============
|
============
|
||||||
|
|
||||||
|
- v0.5.0
|
||||||
|
- support hybi-13 protocol.
|
||||||
- v0.4.1
|
- v0.4.1
|
||||||
- fix incorrect custom header order(ISSUE#1)
|
- fix incorrect custom header order(ISSUE#1)
|
||||||
|
|
||||||
|
153
websocket.py
153
websocket.py
@@ -29,9 +29,19 @@ import sha
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket python client.
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This version support only hybi-13.
|
||||||
|
Please see http://tools.ietf.org/html/rfc6455 for protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# websocket supported version.
|
||||||
VERSION = 13
|
VERSION = 13
|
||||||
|
|
||||||
|
# closing frame status codes.
|
||||||
STATUS_NORMAL = 1000
|
STATUS_NORMAL = 1000
|
||||||
STATUS_GOING_AWAY = 1001
|
STATUS_GOING_AWAY = 1001
|
||||||
STATUS_PROTOCOL_ERROR = 1002
|
STATUS_PROTOCOL_ERROR = 1002
|
||||||
@@ -49,9 +59,9 @@ STATUS_TLS_HANDSHAKE_ERROR = 1015
|
|||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
class WebSocketException(Exception):
|
class WebSocketException(Exception):
|
||||||
pass
|
"""
|
||||||
|
websocket exeception class.
|
||||||
class ConnectionClosedException(WebSocketException):
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
default_timeout = None
|
default_timeout = None
|
||||||
@@ -60,6 +70,8 @@ traceEnabled = False
|
|||||||
def enableTrace(tracable):
|
def enableTrace(tracable):
|
||||||
"""
|
"""
|
||||||
turn on/off the tracability.
|
turn on/off the tracability.
|
||||||
|
|
||||||
|
tracable: boolean value. if set True, tracability is enabled.
|
||||||
"""
|
"""
|
||||||
global traceEnabled
|
global traceEnabled
|
||||||
traceEnabled = tracable
|
traceEnabled = tracable
|
||||||
@@ -71,13 +83,15 @@ def enableTrace(tracable):
|
|||||||
def setdefaulttimeout(timeout):
|
def setdefaulttimeout(timeout):
|
||||||
"""
|
"""
|
||||||
Set the global timeout setting to connect.
|
Set the global timeout setting to connect.
|
||||||
|
|
||||||
|
timeout: default socket timeout time. This value is second.
|
||||||
"""
|
"""
|
||||||
global default_timeout
|
global default_timeout
|
||||||
default_timeout = timeout
|
default_timeout = timeout
|
||||||
|
|
||||||
def getdefaulttimeout():
|
def getdefaulttimeout():
|
||||||
"""
|
"""
|
||||||
Return the global timeout setting to connect.
|
Return the global timeout setting(second) to connect.
|
||||||
"""
|
"""
|
||||||
return default_timeout
|
return default_timeout
|
||||||
|
|
||||||
@@ -85,6 +99,8 @@ def _parse_url(url):
|
|||||||
"""
|
"""
|
||||||
parse url and the result is tuple of
|
parse url and the result is tuple of
|
||||||
(hostname, port, resource path and the flag of secure mode)
|
(hostname, port, resource path and the flag of secure mode)
|
||||||
|
|
||||||
|
url: url string.
|
||||||
"""
|
"""
|
||||||
if ":" not in url:
|
if ":" not in url:
|
||||||
raise ValueError("url is invalid")
|
raise ValueError("url is invalid")
|
||||||
@@ -131,6 +147,12 @@ def create_connection(url, timeout=None, **options):
|
|||||||
|
|
||||||
>>> conn = create_connection("ws://echo.websocket.org/",
|
>>> conn = create_connection("ws://echo.websocket.org/",
|
||||||
... headers={"User-Agent": "MyProgram"})
|
... headers={"User-Agent": "MyProgram"})
|
||||||
|
|
||||||
|
timeout: socket timeout time. This value is integer.
|
||||||
|
if you set None for this value, it means "use default_timeout value"
|
||||||
|
|
||||||
|
options: current support option is only "header".
|
||||||
|
if you set header as dict value, the custom HTTP headers are added.
|
||||||
"""
|
"""
|
||||||
websock = WebSocket()
|
websock = WebSocket()
|
||||||
websock.settimeout(timeout != None and timeout or default_timeout)
|
websock.settimeout(timeout != None and timeout or default_timeout)
|
||||||
@@ -148,7 +170,7 @@ def _create_sec_websocket_key():
|
|||||||
uid = uuid.uuid1()
|
uid = uuid.uuid1()
|
||||||
return base64.encodestring(uid.bytes).strip()
|
return base64.encodestring(uid.bytes).strip()
|
||||||
|
|
||||||
HEADERS_TO_CHECK = {
|
_HEADERS_TO_CHECK = {
|
||||||
"upgrade": "websocket",
|
"upgrade": "websocket",
|
||||||
"connection": "upgrade",
|
"connection": "upgrade",
|
||||||
}
|
}
|
||||||
@@ -163,10 +185,10 @@ class _SSLSocketWrapper(object):
|
|||||||
def send(self, payload):
|
def send(self, payload):
|
||||||
return self.ssl.write(payload)
|
return self.ssl.write(payload)
|
||||||
|
|
||||||
BOOL_VALUES = (0, 1)
|
_BOOL_VALUES = (0, 1)
|
||||||
def is_bool(*values):
|
def _is_bool(*values):
|
||||||
for v in values:
|
for v in values:
|
||||||
if v not in BOOL_VALUES:
|
if v not in _BOOL_VALUES:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -177,20 +199,29 @@ class ABNF(object):
|
|||||||
see http://tools.ietf.org/html/rfc5234
|
see http://tools.ietf.org/html/rfc5234
|
||||||
and http://tools.ietf.org/html/rfc6455#section-5.2
|
and http://tools.ietf.org/html/rfc6455#section-5.2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# operation code values.
|
||||||
OPCODE_TEXT = 0x1
|
OPCODE_TEXT = 0x1
|
||||||
OPCODE_BINARY = 0x2
|
OPCODE_BINARY = 0x2
|
||||||
OPCODE_CLOSE = 0x8
|
OPCODE_CLOSE = 0x8
|
||||||
OPCODE_PING = 0x9
|
OPCODE_PING = 0x9
|
||||||
OPCODE_PONG = 0xa
|
OPCODE_PONG = 0xa
|
||||||
OPTCODES = (OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
|
||||||
|
# available operation code value tuple
|
||||||
|
OPCODES = (OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
||||||
OPCODE_PING, OPCODE_PONG)
|
OPCODE_PING, OPCODE_PONG)
|
||||||
|
|
||||||
|
# data length threashold.
|
||||||
LENGTH_7 = 0x7d
|
LENGTH_7 = 0x7d
|
||||||
LENGTH_16 = 1 << 16
|
LENGTH_16 = 1 << 16
|
||||||
LENGTH_63 = 1 << 63
|
LENGTH_63 = 1 << 63
|
||||||
|
|
||||||
def __init__(self, fin = 0, rsv1 = 0, rsv2 = 0, rsv3 = 0,
|
def __init__(self, fin = 0, rsv1 = 0, rsv2 = 0, rsv3 = 0,
|
||||||
opcode = OPCODE_TEXT, mask = 1, data = ""):
|
opcode = OPCODE_TEXT, mask = 1, data = ""):
|
||||||
|
"""
|
||||||
|
Constructor for ABNF.
|
||||||
|
please check RFC for arguments.
|
||||||
|
"""
|
||||||
self.fin = fin
|
self.fin = fin
|
||||||
self.rsv1 = rsv1
|
self.rsv1 = rsv1
|
||||||
self.rsv2 = rsv2
|
self.rsv2 = rsv2
|
||||||
@@ -202,15 +233,27 @@ class ABNF(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_frame(data, opcode):
|
def create_frame(data, opcode):
|
||||||
|
"""
|
||||||
|
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 uniocde,
|
||||||
|
data value is conveted into unicode string, automatically.
|
||||||
|
|
||||||
|
opcode: operation code. please see OPCODE_XXX.
|
||||||
|
"""
|
||||||
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
|
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
|
||||||
data = data.encode("utf-8")
|
data = data.encode("utf-8")
|
||||||
# mask must be set if send data from client
|
# mask must be set if send data from client
|
||||||
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
||||||
|
|
||||||
def format(self):
|
def format(self):
|
||||||
if not is_bool(self.fin, self.rsv1, self.rsv2, self.rsv3):
|
"""
|
||||||
|
format this object to string(byte array) to send data to server.
|
||||||
|
"""
|
||||||
|
if not _is_bool(self.fin, self.rsv1, self.rsv2, self.rsv3):
|
||||||
raise ValueError("not 0 or 1")
|
raise ValueError("not 0 or 1")
|
||||||
if self.opcode not in ABNF.OPTCODES:
|
if self.opcode not in ABNF.OPCODES:
|
||||||
raise ValueError("Invalid OPCODE")
|
raise ValueError("Invalid OPCODE")
|
||||||
length = len(self.data)
|
length = len(self.data)
|
||||||
if length >= ABNF.LENGTH_63:
|
if length >= ABNF.LENGTH_63:
|
||||||
@@ -240,6 +283,13 @@ class ABNF(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mask(mask_key, data):
|
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.
|
||||||
|
"""
|
||||||
_m = map(ord, mask_key)
|
_m = map(ord, mask_key)
|
||||||
_d = map(ord, data)
|
_d = map(ord, data)
|
||||||
for i in range(len(_d)):
|
for i in range(len(_d)):
|
||||||
@@ -247,10 +297,6 @@ class ABNF(object):
|
|||||||
s = map(chr, _d)
|
s = map(chr, _d)
|
||||||
return "".join(s)
|
return "".join(s)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(object):
|
class WebSocket(object):
|
||||||
"""
|
"""
|
||||||
Low level WebSocket interface.
|
Low level WebSocket interface.
|
||||||
@@ -278,17 +324,28 @@ class WebSocket(object):
|
|||||||
self.get_mask_key = None
|
self.get_mask_key = None
|
||||||
|
|
||||||
def set_mask_key(self, func):
|
def set_mask_key(self, func):
|
||||||
|
"""
|
||||||
|
set function to create musk key. You can custumize mask key generator.
|
||||||
|
Mainly, this is for testing purpose.
|
||||||
|
|
||||||
|
func: callable object. the fuct must 1 argument as integer.
|
||||||
|
The argument means length of mask key.
|
||||||
|
This func must be return string(byte array),
|
||||||
|
which length is argument specified.
|
||||||
|
"""
|
||||||
self.get_mask_key = func
|
self.get_mask_key = func
|
||||||
|
|
||||||
def settimeout(self, timeout):
|
def settimeout(self, timeout):
|
||||||
"""
|
"""
|
||||||
Set the timeout to the websocket.
|
Set the timeout to the websocket.
|
||||||
|
|
||||||
|
timeout: timeout time(second).
|
||||||
"""
|
"""
|
||||||
self.sock.settimeout(timeout)
|
self.sock.settimeout(timeout)
|
||||||
|
|
||||||
def gettimeout(self):
|
def gettimeout(self):
|
||||||
"""
|
"""
|
||||||
Get the websocket timeout.
|
Get the websocket timeout(second).
|
||||||
"""
|
"""
|
||||||
return self.sock.gettimeout()
|
return self.sock.gettimeout()
|
||||||
|
|
||||||
@@ -301,6 +358,15 @@ class WebSocket(object):
|
|||||||
>>> ws = WebSocket()
|
>>> ws = WebSocket()
|
||||||
>>> ws.connect("ws://echo.websocket.org/",
|
>>> ws.connect("ws://echo.websocket.org/",
|
||||||
... headers={"User-Agent": "MyProgram"})
|
... headers={"User-Agent": "MyProgram"})
|
||||||
|
|
||||||
|
timeout: socket timeout time. This value is integer.
|
||||||
|
if you set None for this value,
|
||||||
|
it means "use default_timeout value"
|
||||||
|
|
||||||
|
options: current support option is only "header".
|
||||||
|
if you set header as dict value,
|
||||||
|
the custom HTTP headers are added.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
hostname, port, resource, is_secure = _parse_url(url)
|
hostname, port, resource, is_secure = _parse_url(url)
|
||||||
# TODO: we need to support proxy
|
# TODO: we need to support proxy
|
||||||
@@ -352,7 +418,7 @@ class WebSocket(object):
|
|||||||
self.connected = True
|
self.connected = True
|
||||||
|
|
||||||
def _validate_header(self, headers, key):
|
def _validate_header(self, headers, key):
|
||||||
for k, v in HEADERS_TO_CHECK.iteritems():
|
for k, v in _HEADERS_TO_CHECK.iteritems():
|
||||||
r = headers.get(k, None)
|
r = headers.get(k, None)
|
||||||
if not r:
|
if not r:
|
||||||
return False
|
return False
|
||||||
@@ -398,9 +464,15 @@ class WebSocket(object):
|
|||||||
|
|
||||||
return status, headers
|
return status, headers
|
||||||
|
|
||||||
def send(self, payload, opcode = ABNF.OPCODE_TEXT, binary = False):
|
def send(self, payload, opcode = ABNF.OPCODE_TEXT):
|
||||||
"""
|
"""
|
||||||
Send the data as string. payload must be utf-8 string or unicoce.
|
Send the data as string.
|
||||||
|
|
||||||
|
payload: Payload must be utf-8 string or unicoce,
|
||||||
|
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)
|
frame = ABNF.create_frame(payload, opcode)
|
||||||
if self.get_mask_key:
|
if self.get_mask_key:
|
||||||
@@ -410,32 +482,54 @@ class WebSocket(object):
|
|||||||
if traceEnabled:
|
if traceEnabled:
|
||||||
logger.debug("send: " + repr(data))
|
logger.debug("send: " + repr(data))
|
||||||
|
|
||||||
def ping(self, payload):
|
def ping(self, payload = ""):
|
||||||
|
"""
|
||||||
|
send ping data.
|
||||||
|
|
||||||
|
payload: data payload to send server.
|
||||||
|
"""
|
||||||
self.send(payload, ABNF.OPCODE_PING)
|
self.send(payload, ABNF.OPCODE_PING)
|
||||||
|
|
||||||
def pong(self, payload):
|
def pong(self, payload):
|
||||||
|
"""
|
||||||
|
send pong data.
|
||||||
|
|
||||||
|
payload: data payload to send server.
|
||||||
|
"""
|
||||||
self.send(payload, ABNF.OPCODE_PONG)
|
self.send(payload, ABNF.OPCODE_PONG)
|
||||||
|
|
||||||
def recv(self):
|
def recv(self):
|
||||||
"""
|
"""
|
||||||
Receive utf-8 string data from the server.
|
Receive string data(byte array) from the server.
|
||||||
|
|
||||||
|
return value: string(byte array) value.
|
||||||
"""
|
"""
|
||||||
opcode, data = self.recv_data()
|
opcode, data = self.recv_data()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def recv_data(self):
|
def recv_data(self):
|
||||||
|
"""
|
||||||
|
Recieve data with operation code.
|
||||||
|
|
||||||
|
return value: tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
frame = self.recv_frame()
|
frame = self.recv_frame()
|
||||||
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
return (frame.opcode, frame.data)
|
return (frame.opcode, frame.data)
|
||||||
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
||||||
self.send_close()
|
self.send_close()
|
||||||
return None
|
return (frame.opcode, None)
|
||||||
elif frame.opcode == ABNF.OPCODE_PING:
|
elif frame.opcode == ABNF.OPCODE_PING:
|
||||||
self.pong("Hi!")
|
self.pong("Hi!")
|
||||||
|
|
||||||
|
|
||||||
def recv_frame(self):
|
def recv_frame(self):
|
||||||
|
"""
|
||||||
|
recieve data as frame from server.
|
||||||
|
|
||||||
|
return value: ABNF frame object.
|
||||||
|
"""
|
||||||
header_bytes = self._recv(2)
|
header_bytes = self._recv(2)
|
||||||
if not header_bytes:
|
if not header_bytes:
|
||||||
return None
|
return None
|
||||||
@@ -467,6 +561,13 @@ class WebSocket(object):
|
|||||||
return frame
|
return frame
|
||||||
|
|
||||||
def send_close(self, status = STATUS_NORMAL, reason = ""):
|
def send_close(self, status = STATUS_NORMAL, reason = ""):
|
||||||
|
"""
|
||||||
|
send close data to the server.
|
||||||
|
|
||||||
|
status: status code to send. see STATUS_XXX.
|
||||||
|
|
||||||
|
reason: the reason to close. This must be string.
|
||||||
|
"""
|
||||||
if status < 0 or status > ABNF.LENGTH_16:
|
if status < 0 or status > ABNF.LENGTH_16:
|
||||||
raise ValueError("code is invalid range")
|
raise ValueError("code is invalid range")
|
||||||
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
@@ -476,6 +577,10 @@ class WebSocket(object):
|
|||||||
def close(self, status = STATUS_NORMAL, reason = ""):
|
def close(self, status = STATUS_NORMAL, reason = ""):
|
||||||
"""
|
"""
|
||||||
Close Websocket object
|
Close Websocket object
|
||||||
|
|
||||||
|
status: status code to send. see STATUS_XXX.
|
||||||
|
|
||||||
|
reason: the reason to close. This must be string.
|
||||||
"""
|
"""
|
||||||
if self.connected:
|
if self.connected:
|
||||||
if status < 0 or status > ABNF.LENGTH_16:
|
if status < 0 or status > ABNF.LENGTH_16:
|
||||||
@@ -524,10 +629,6 @@ class WebSocket(object):
|
|||||||
if c == "\n":
|
if c == "\n":
|
||||||
break
|
break
|
||||||
return "".join(line)
|
return "".join(line)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketApp(object):
|
class WebSocketApp(object):
|
||||||
"""
|
"""
|
||||||
|
Reference in New Issue
Block a user