import struct import socket from os import urandom from string import printable OPCODE_CONTINUATION = 0x0 OPCODE_TEXT = 0x1 OPCODE_BINARY = 0x2 OPCODE_CLOSE = 0x8 OPCODE_PING = 0x9 OPCODE_PONG = 0xA CLOSE_NORMAL = 1000 CLOSE_GOING_AWAY = 1001 CLOSE_PROTOCOL_ERROR = 1002 CLOSE_NOACCEPT_DTYPE = 1003 CLOSE_INVALID_DATA = 1007 CLOSE_POLICY = 1008 CLOSE_MESSAGE_TOOBIG = 1009 CLOSE_MISSING_EXTENSIONS = 1010 CLOSE_UNABLE = 1011 line_printable = [c for c in printable if c not in '\r\n\x0b\x0c'] def printstr(s): return ''.join(c if c in line_printable else '.' for c in str(s)) class Frame(object): """ A Frame instance represents a web socket data frame as defined in RFC 6455. To encoding a frame for sending it over a socket, use Frame.pack(). To receive and decode a frame from a socket, use receive_frame(). """ def __init__(self, opcode, payload, masking_key='', mask=False, final=True, rsv1=False, rsv2=False, rsv3=False): """ Create a new frame. `opcode` is one of the constants as defined above. `payload` is a string of bytes containing the data sendt in the frame. `masking_key` is an optional custom key to use for masking, or `mask` can be used instead to let this constructor generate a random masking key. `final` is a boolean indicating whether this frame is the last in a chain of fragments. `rsv1`, `rsv2` and `rsv3` are booleans indicating bit values for RSV1, RVS2 and RSV3, which are only non-zero if defined so by extensions. """ if mask: masking_key = urandom(4) if len(masking_key) not in (0, 4): raise ValueError('invalid masking key "%s"' % masking_key) self.final = final self.rsv1 = rsv1 self.rsv2 = rsv2 self.rsv3 = rsv3 self.opcode = opcode self.masking_key = masking_key self.payload = payload def pack(self): """ Pack the frame into a string according to the following scheme: +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ """ header = struct.pack('!B', (self.final << 7) | (self.rsv1 << 6) | (self.rsv2 << 5) | (self.rsv3 << 4) | (self.opcode & 0xf)) mask = bool(self.masking_key) << 7 payload_len = len(self.payload) if payload_len <= 125: header += struct.pack('!B', mask | payload_len) elif payload_len < (1 << 16): header += struct.pack('!BH', mask | 126, payload_len) elif payload_len < (1 << 63): header += struct.pack('!BQ', mask | 127, payload_len) else: # FIXME: RFC 6455 defines an action for this... raise Exception('the payload length is too damn high!') if mask: return header + self.masking_key + self.mask_payload() return header + self.payload def mask_payload(self): return mask(self.masking_key, self.payload) def fragment(self, fragment_size, mask=False): """ Fragment the frame into a chain of fragment frames: - An initial frame with non-zero opcode - Zero or more frames with opcode = 0 and final = False - A final frame with opcode = 0 and final = True The first and last frame may be the same frame, having a non-zero opcode and final = True. Thus, this function returns a list containing at least a single frame. `fragment_size` indicates the maximum payload size of each fragment. The payload of the original frame is split into one or more parts, and each part is converted to a Frame instance. `mask` is a boolean (default False) indicating whether the payloads should be masked. If True, each frame is assigned a randomly generated masking key. """ frames = [] for start in xrange(0, len(self.payload), fragment_size): payload = self.payload[start:start + fragment_size] frames.append(Frame(OPCODE_CONTINUATION, payload, mask=mask, final=False)) frames[0].opcode = self.opcode frames[-1].final = True return frames def is_fragmented(self): return not self.final or self.opcode == OPCODE_CONTINUATION def __str__(self): s = '<%s opcode=0x%X len=%d' \ % (self.__class__.__name__, self.opcode, len(self.payload)) if self.masking_key: s += ' masking_key=%4s' % printstr(self.masking_key) max_pl_disp = 30 pl = printstr(self.payload)[:max_pl_disp] if len(self.payload) > max_pl_disp: pl += '...' s += ' payload=%s' % pl if self.rsv1: s += ' rsv1' if self.rsv2: s += ' rsv2' if self.rsv3: s += ' rsv3' return s + '>' class ControlFrame(Frame): """ A control frame is a frame with an opcode OPCODE_CLOSE, OPCODE_PING or OPCODE_PONG. These frames must be handled as defined by RFC 6455, and """ def fragment(self, fragment_size, mask=False): """ Control frames must not be fragmented. """ raise TypeError('control frames must not be fragmented') def pack(self): """ Same as Frame.pack(), but asserts that the payload size does not exceed 125 bytes. """ if len(self.payload) > 125: raise ValueError('control frames must not be larger than 125 ' 'bytes') return Frame.pack(self) def unpack_close(self): """ Unpack a close message into a status code and a reason. If no payload is given, the code is None and the reason is an empty string. """ if self.payload: code = struct.unpack('!H', str(self.payload[:2]))[0] reason = str(self.payload[2:]) else: code = None reason = '' return code, reason def decode_frame(reader): b1, b2 = struct.unpack('!BB', reader.readn(2)) final = bool(b1 & 0x80) rsv1 = bool(b1 & 0x40) rsv2 = bool(b1 & 0x20) rsv3 = bool(b1 & 0x10) opcode = b1 & 0x0F masked = bool(b2 & 0x80) payload_len = b2 & 0x7F if payload_len == 126: payload_len = struct.unpack('!H', reader.readn(2)) elif payload_len == 127: payload_len = struct.unpack('!Q', reader.readn(8)) if masked: masking_key = reader.readn(4) payload = mask(masking_key, reader.readn(payload_len)) else: masking_key = '' payload = reader.readn(payload_len) # Control frames have most significant bit 1 cls = ControlFrame if opcode & 0x8 else Frame return cls(opcode, payload, masking_key=masking_key, final=final, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3) def receive_frame(sock): return decode_frame(SocketReader(sock)) def read_frame(data): reader = BufferReader(data) frame = decode_frame(reader) return frame, reader.offset def pop_frame(data): frame, size = read_frame(data) return frame, data[size:] class BufferReader(object): def __init__(self, data): self.data = data self.offset = 0 def readn(self, n): assert len(self.data) - self.offset >= n self.offset += n return self.data[self.offset - n:self.offset] class SocketReader(object): def __init__(self, sock): self.sock = sock def readn(self, n): """ Keep receiving data until exactly `n` bytes have been read. """ data = '' while len(data) < n: received = self.sock.recv(n - len(data)) if not len(received): raise socket.error('no data read from socket') data += received return data def contains_frame(data): """ Read the frame length from the start of `data` and check if the data is long enough to contain the entire frame. """ if len(data) < 2: return False b2 = struct.unpack('!B', data[1])[0] payload_len = b2 & 0x7F payload_start = 2 if payload_len == 126: if len(data) > 4: payload_len = struct.unpack('!H', data[2:4]) payload_start = 4 elif payload_len == 127: if len(data) > 12: payload_len = struct.unpack('!Q', data[4:12]) payload_start = 12 return len(data) >= payload_len + payload_start def mask(key, original): """ Mask an octet string using the given masking key. The following masking algorithm is used, as defined in RFC 6455: for each octet: j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j """ if len(key) != 4: raise ValueError('invalid masking key "%s"' % key) key = map(ord, key) masked = bytearray(original) for i in xrange(len(masked)): masked[i] ^= key[i % 4] return masked def create_close_frame(code, reason): payload = '' if code is None else struct.pack('!H', code) + reason return ControlFrame(OPCODE_CLOSE, payload)