import re import errno import socket import struct import collections from base64 import b64decode, b64encode from hashlib import sha1 WS_KEY = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' def pack_message(message): """Pack the message inside ``00`` and ``FF`` As per the dataframing section (5.3) for the websocket spec """ if isinstance(message, unicode): message = message.encode('utf-8') elif not isinstance(message, str): message = str(message) packed = "\x00%s\xFF" % message return packed def encode_hybi(buf, opcode, base64=False): """ Encode a HyBi style WebSocket frame. Optional opcode: 0x0 - continuation 0x1 - text frame (base64 encode buf) 0x2 - binary frame (use raw buf) 0x8 - connection close 0x9 - ping 0xA - pong """ if base64: buf = b64encode(buf) b1 = 0x80 | (opcode & 0x0f) # FIN + opcode payload_len = len(buf) if payload_len <= 125: header = struct.pack('>BB', b1, payload_len) elif payload_len > 125 and payload_len < 65536: header = struct.pack('>BBH', b1, 126, payload_len) elif payload_len >= 65536: header = struct.pack('>BBQ', b1, 127, payload_len) return header + buf, len(header), 0 def decode_hybi(buf, base64=False): """Decode HyBi style WebSocket packets.""" f = {'fin' : 0, 'opcode' : 0, 'mask' : 0, 'hlen' : 2, 'length' : 0, 'payload' : None, 'left' : 0, 'close_code' : None, 'close_reason' : None} blen = len(buf) f['left'] = blen if blen < f['hlen']: return f # Incomplete frame header b1, b2 = struct.unpack_from(">BB", buf) f['opcode'] = b1 & 0x0f f['fin'] = (b1 & 0x80) >> 7 has_mask = (b2 & 0x80) >> 7 f['length'] = b2 & 0x7f if f['length'] == 126: f['hlen'] = 4 if blen < f['hlen']: return f # Incomplete frame header (f['length'],) = struct.unpack_from('>xxH', buf) elif f['length'] == 127: f['hlen'] = 10 if blen < f['hlen']: return f # Incomplete frame header (f['length'],) = struct.unpack_from('>xxQ', buf) full_len = f['hlen'] + has_mask * 4 + f['length'] if blen < full_len: # Incomplete frame return f # Incomplete frame header # Number of bytes that are part of the next frame(s) f['left'] = blen - full_len # Process 1 frame if has_mask: # unmask payload f['mask'] = buf[f['hlen']:f['hlen']+4] b = c = '' if f['length'] >= 4: data = struct.unpack('= 2: f['close_code'] = struct.unpack_from(">H", f['payload']) if f['length'] > 3: f['close_reason'] = f['payload'][2:] return f class WebSocketWSGI(object): def __init__(self, handler): self.handler = handler def verify_client(self, ws): pass def __call__(self, environ, start_response): if not (environ.get('HTTP_CONNECTION').find('Upgrade') != -1 and environ['HTTP_UPGRADE'].lower() == 'websocket'): # need to check a few more things here for true compliance start_response('400 Bad Request', [('Connection','close')]) return [] sock = environ['gunicorn.socket'] ws = WebSocket(sock, environ) handshake_reply = ("HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n") path = environ['PATH_INFO'] key = environ.get('HTTP_SEC_WEBSOCKET_KEY') if key: ws_key = b64decode(key) if len(ws_key) != 16: start_response('400 Bad Request', [('Connection','close')]) return [] protocols = [] subprotocols = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') ws_protocols = [] if subprotocols: for s in subprotocols.split(','): s = s.strip() if s in protocols: ws_protocols.append(s) if ws_protocols: handshake_reply += 'Sec-WebSocket-Protocol: %s\r\n' % ', '.join(ws_protocols) exts = [] extensions = environ.get('HTTP_SEC_WEBSOCKET_EXTENSIONS') ws_extensions = [] if extensions: for ext in extensions.split(','): ext = ext.strip() if ext in exts: ws_extensions.append(ext) if ws_extensions: handshake_reply += 'Sec-WebSocket-Extensions: %s\r\n' % ', '.join(ws_extensions) handshake_reply += ( "Sec-WebSocket-Origin: %s\r\n" "Sec-WebSocket-Location: ws://%s%s\r\n" "Sec-WebSocket-Version: %s\r\n" "Sec-WebSocket-Accept: %s\r\n\r\n" % ( environ.get('HTTP_ORIGIN'), environ.get('HTTP_HOST'), path, ws.version, b64encode(sha1(key + WS_KEY).digest()) )) else: handshake_reply += ( "WebSocket-Origin: %s\r\n" "WebSocket-Location: ws://%s%s\r\n\r\n" % ( environ.get('HTTP_ORIGIN'), environ.get('HTTP_HOST'), path)) sock.sendall(handshake_reply) try: self.handler(ws) except socket.error as e: if e[0] != errno.EPIPE: raise # use this undocumented feature of grainbows to ensure that it # doesn't barf on the fact that we didn't call start_response return ALREADY_HANDLED class WebSocket(object): def __init__(self, sock, environ, version=76): self._socket = sock try: version = int(environ.get('HTTP_SEC_WEBSOCKET_VERSION')) except (ValueError, TypeError): version = 76 self.version = version self.closed = False self.accepted = False self._buf = b'' self._msgs = collections.deque() def _parse_messages(self): """ Parses for messages in the buffer *buf*. It is assumed that the buffer contains the start character for a message, but that it may contain only part of the rest of the message. Returns an array of messages, and the buffer remainder that didn't contain any full messages.""" msgs = [] end_idx = 0 buf = self._buf while buf: if self.version in (7, 8, 13): frame = decode_hybi(buf, base64=False) if frame['payload'] == None: break else: if frame['opcode'] == 0x8: # connection close self.closed = True break else: msgs.append(frame['payload']); if frame['left']: buf = buf[-frame['left']:] else: buf = b'' else: frame_type = ord(buf[0]) if frame_type == 0: # Normal message. end_idx = buf.find("\xFF") if end_idx == -1: #pragma NO COVER break msgs.append(buf[1:end_idx].decode('utf-8', 'replace')) buf = buf[end_idx+1:] elif frame_type == 255: # Closing handshake. assert ord(buf[1]) == 0, "Unexpected closing handshake: %r" % buf self.closed = True break else: raise ValueError("Don't understand how to parse this type of message: %r" % buf) self._buf = buf return msgs def send(self, message): """Send a message to the browser. *message* should be convertable to a string; unicode objects should be encodable as utf-8. Raises socket.error with errno of 32 (broken pipe) if the socket has already been closed by the client. """ if self.version in (7, 8, 13): packed, lenhead, lentail = encode_hybi( message, opcode=0x01, base64=False) else: packed = pack_message(message) self._socket.sendall(packed) def wait(self): """Waits for and deserializes messages. Returns a single message; the oldest not yet processed. If the client has already closed the connection, returns None. This is different from normal socket behavior because the empty string is a valid websocket message.""" while not self._msgs: # Websocket might be closed already. if self.closed: return None # no parsed messages, must mean buf needs more data delta = self._socket.recv(8096) if delta == '': return None self._buf += delta msgs = self._parse_messages() self._msgs.extend(msgs) return self._msgs.popleft() def _send_closing_frame(self, ignore_send_errors=False): """Sends the closing frame to the client, if required.""" if self.version in (7, 8, 13) and not self.closed: msg = '' #if code != None: # msg = struct.pack(">H%ds" % (len(reason)), code) buf, h, t = encode_hybi(msg, opcode=0x08, base64=False) self._socket.sendall(buf) self.closed = True elif self.version == 76 and not self.closed: try: self._socket.sendall("\xff\x00") except socket.error: # Sometimes, like when the remote side cuts off the connection, # we don't care about this. if not ignore_send_errors: #pragma NO COVER raise self.closed = True def close(self): """Forcibly close the websocket; generally it is preferable to return from the handler method.""" self._send_closing_frame() self._socket.shutdown(True) self._socket.close()