- add comment.

This commit is contained in:
liris
2012-01-16 18:13:14 +09:00
parent aedebb7ebc
commit 742a36fb4d
2 changed files with 131 additions and 26 deletions

View File

@@ -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)

View File

@@ -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):
""" """