%PDF- %PDF-
Direktori : /lib/calibre/calibre/srv/ |
Current File : //lib/calibre/calibre/srv/web_socket.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> import os import socket import weakref from calibre_extensions.speedup import utf8_decode, websocket_mask as fast_mask from collections import deque from hashlib import sha1 from struct import error as struct_error, pack, unpack_from from threading import Lock from calibre import as_unicode from calibre.srv.http_response import HTTPConnection, create_http_handler from calibre.srv.loop import ( RDWR, READ, WRITE, Connection, HandleInterrupt, ServerLoop ) from calibre.srv.utils import DESIRED_SEND_BUFFER_SIZE from calibre.utils.speedups import ReadOnlyFileBuffer from polyglot import http_client from polyglot.binary import as_base64_unicode from polyglot.queue import Empty, Queue HANDSHAKE_STR = ( "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: WebSocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n\r\n" ) GUID_STR = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' CONTINUATION = 0x0 TEXT = 0x1 BINARY = 0x2 CLOSE = 0x8 PING = 0x9 PONG = 0xA CONTROL_CODES = (CLOSE, PING, PONG) ALL_CODES = CONTROL_CODES + (CONTINUATION, TEXT, BINARY) CHUNK_SIZE = 16 * 1024 SEND_CHUNK_SIZE = DESIRED_SEND_BUFFER_SIZE - 16 NORMAL_CLOSE = 1000 SHUTTING_DOWN = 1001 PROTOCOL_ERROR = 1002 UNSUPPORTED_DATA = 1003 INCONSISTENT_DATA = 1007 POLICY_VIOLATION = 1008 MESSAGE_TOO_BIG = 1009 UNEXPECTED_ERROR = 1011 RESERVED_CLOSE_CODES = (1004,1005,1006,) class ReadFrame: # {{{ def __init__(self): self.header_buf = bytearray(14) self.rbuf = bytearray(CHUNK_SIZE) self.empty = memoryview(b'') self.reset() def reset(self): self.header_view = memoryview(self.header_buf)[:6] self.state = self.read_header def __call__(self, conn): return self.state(conn) def read_header(self, conn): num_bytes = conn.recv_into(self.header_view) if num_bytes == 0: return read_bytes = 6 - len(self.header_view) + num_bytes if read_bytes > 2: b1, b2 = self.header_buf[0], self.header_buf[1] self.fin = bool(b1 & 0b10000000) if b1 & 0b01110000: conn.log.error('RSV bits set in frame from client') conn.websocket_close(PROTOCOL_ERROR, 'RSV bits set') return self.opcode = b1 & 0b1111 self.is_control = self.opcode in CONTROL_CODES if self.opcode not in ALL_CODES: conn.log.error('Unknown OPCODE from client: %r' % self.opcode) conn.websocket_close(PROTOCOL_ERROR, 'Unknown OPCODE: %r' % self.opcode) return if not self.fin and self.is_control: conn.log.error('Fragmented control frame from client') conn.websocket_close(PROTOCOL_ERROR, 'Fragmented control frame') return mask = b2 & 0b10000000 if not mask: conn.log.error('Unmasked packet from client') conn.websocket_close(PROTOCOL_ERROR, 'Unmasked packet not allowed') self.reset() return self.payload_length = l = b2 & 0b01111111 if self.is_control and l > 125: conn.log.error('Too large control frame from client') conn.websocket_close(PROTOCOL_ERROR, 'Control frame too large') self.reset() return header_len = 6 + (0 if l < 126 else 2 if l == 126 else 8) if header_len <= read_bytes: self.process_header(conn) else: self.header_view = memoryview(self.header_buf)[read_bytes:header_len] self.state = self.finish_reading_header else: self.header_view = self.header_view[num_bytes:] def finish_reading_header(self, conn): num_bytes = conn.recv_into(self.header_view) if num_bytes == 0: return if num_bytes >= len(self.header_view): self.process_header(conn) else: self.header_view = self.header_view[num_bytes:] def process_header(self, conn): if self.payload_length < 126: self.mask = memoryview(self.header_buf)[2:6] elif self.payload_length == 126: self.payload_length, = unpack_from(b'!H', self.header_buf, 2) self.mask = memoryview(self.header_buf)[4:8] else: self.payload_length, = unpack_from(b'!Q', self.header_buf, 2) self.mask = memoryview(self.header_buf)[10:14] self.frame_starting = True self.bytes_received = 0 if self.payload_length <= CHUNK_SIZE: if self.payload_length == 0: conn.ws_data_received(self.empty, self.opcode, True, True, self.fin) self.reset() else: self.rview = memoryview(self.rbuf)[:self.payload_length] self.state = self.read_packet else: self.rview = memoryview(self.rbuf) self.state = self.read_payload def read_packet(self, conn): num_bytes = conn.recv_into(self.rview) if num_bytes == 0: return if num_bytes >= len(self.rview): data = memoryview(self.rbuf)[:self.payload_length] fast_mask(data, self.mask) conn.ws_data_received(data, self.opcode, True, True, self.fin) self.reset() else: self.rview = self.rview[num_bytes:] def read_payload(self, conn): num_bytes = conn.recv_into(self.rview, min(len(self.rview), self.payload_length - self.bytes_received)) if num_bytes == 0: return data = memoryview(self.rbuf)[:num_bytes] fast_mask(data, self.mask, self.bytes_received) self.bytes_received += num_bytes frame_finished = self.bytes_received >= self.payload_length conn.ws_data_received(data, self.opcode, self.frame_starting, frame_finished, self.fin) self.frame_starting = False if frame_finished: self.reset() # }}} # Sending frames {{{ def create_frame(fin, opcode, payload, mask=None, rsv=0): if isinstance(payload, str): payload = payload.encode('utf-8') l = len(payload) header_len = 2 + (0 if l < 126 else 2 if 126 <= l <= 65535 else 8) + (0 if mask is None else 4) frame = bytearray(header_len + l) if l > 0: frame[-l:] = payload frame[0] = (opcode & 0b1111) | (0b10000000 if fin else 0) | (rsv & 0b01110000) if l < 126: frame[1] = l elif 126 <= l <= 65535: frame[2:4] = pack(b'!H', l) frame[1] = 126 else: frame[2:10] = pack(b'!Q', l) frame[1] = 127 if mask is not None: frame[1] |= 0b10000000 frame[header_len-4:header_len] = mask if l > 0: fast_mask(memoryview(frame)[-l:], mask) return memoryview(frame) class MessageWriter: def __init__(self, buf, mask=None, chunk_size=None): self.buf, self.data_type, self.mask = buf, BINARY, mask if isinstance(buf, str): self.buf, self.data_type = ReadOnlyFileBuffer(buf.encode('utf-8')), TEXT elif isinstance(buf, bytes): self.buf = ReadOnlyFileBuffer(buf) buf = self.buf self.chunk_size = chunk_size or SEND_CHUNK_SIZE try: pos = buf.tell() buf.seek(0, os.SEEK_END) self.size = buf.tell() - pos buf.seek(pos) except Exception: self.size = None self.first_frame_created = self.exhausted = False def create_frame(self): if self.exhausted: return None buf = self.buf raw = buf.read(self.chunk_size) has_more = True if self.size is None else self.size > buf.tell() fin = 0 if has_more and raw else 1 opcode = 0 if self.first_frame_created else self.data_type self.first_frame_created, self.exhausted = True, bool(fin) return ReadOnlyFileBuffer(create_frame(fin, opcode, raw, self.mask)) # }}} conn_id = 0 class UTF8Decoder: # {{{ def __init__(self): self.reset() def __call__(self, data): ans, self.state, self.codep = utf8_decode(data, self.state, self.codep) return ans def reset(self): self.state = 0 self.codep = 0 # }}} class WebSocketConnection(HTTPConnection): # Internal API {{{ in_websocket_mode = False websocket_handler = None def __init__(self, *args, **kwargs): global conn_id HTTPConnection.__init__(self, *args, **kwargs) self.sendq = Queue() self.control_frames = deque() self.cf_lock = Lock() self.sending = None self.send_buf = None self.frag_decoder = UTF8Decoder() self.ws_close_received = self.ws_close_sent = False conn_id += 1 self.websocket_connection_id = conn_id self.stop_reading = False def finalize_headers(self, inheaders): upgrade = inheaders.get('Upgrade', '') key = inheaders.get('Sec-WebSocket-Key', None) conn = {x.strip().lower() for x in inheaders.get('Connection', '').split(',')} if key is None or upgrade.lower() != 'websocket' or 'upgrade' not in conn: return HTTPConnection.finalize_headers(self, inheaders) ver = inheaders.get('Sec-WebSocket-Version', 'Unknown') try: ver_ok = int(ver) >= 13 except Exception: ver_ok = False if not ver_ok: return self.simple_response(http_client.BAD_REQUEST, 'Unsupported WebSocket protocol version: %s' % ver) if self.method != 'GET': return self.simple_response(http_client.BAD_REQUEST, 'Invalid WebSocket method: %s' % self.method) response = HANDSHAKE_STR % as_base64_unicode(sha1((key + GUID_STR).encode('utf-8')).digest()) self.optimize_for_sending_packet() self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.set_state(WRITE, self.upgrade_connection_to_ws, ReadOnlyFileBuffer(response.encode('ascii')), inheaders) def upgrade_connection_to_ws(self, buf, inheaders, event): if self.write(buf): if self.websocket_handler is None: self.websocket_handler = DummyHandler() self.read_frame, self.current_recv_opcode = ReadFrame(), None self.in_websocket_mode = True try: self.websocket_handler.handle_websocket_upgrade(self.websocket_connection_id, weakref.ref(self), inheaders) except Exception as err: self.log.exception('Error in WebSockets upgrade handler:') self.websocket_close(UNEXPECTED_ERROR, 'Unexpected error in handler: %r' % as_unicode(err)) self.handle_event = self.ws_duplex self.set_ws_state() self.end_send_optimization() def set_ws_state(self): if self.ws_close_sent or self.ws_close_received: if self.ws_close_sent: self.ready = False else: self.wait_for = WRITE return if self.send_buf is not None or self.sending is not None: self.wait_for = RDWR else: try: self.sending = self.sendq.get_nowait() except Empty: with self.cf_lock: if self.control_frames: self.wait_for = RDWR else: self.wait_for = READ else: self.wait_for = RDWR if self.stop_reading: if self.wait_for is READ: self.ready = False elif self.wait_for is RDWR: self.wait_for = WRITE def ws_duplex(self, event): if event is READ: self.ws_read() elif event is WRITE: self.ws_write() self.set_ws_state() def ws_read(self): if not self.stop_reading: self.read_frame(self) def ws_data_received(self, data, opcode, frame_starting, frame_finished, is_final_frame_of_message): if opcode in CONTROL_CODES: return self.ws_control_frame(opcode, data) message_starting = self.current_recv_opcode is None if message_starting: if opcode == CONTINUATION: self.log.error('Client sent continuation frame with no message to continue') self.websocket_close(PROTOCOL_ERROR, 'Continuation frame without any message to continue') return self.current_recv_opcode = opcode elif frame_starting and opcode != CONTINUATION: self.log.error('Client sent continuation frame with non-zero opcode') self.websocket_close(PROTOCOL_ERROR, 'Continuation frame with non-zero opcode') return message_finished = frame_finished and is_final_frame_of_message if self.current_recv_opcode == TEXT: if message_starting: self.frag_decoder.reset() empty_data = len(data) == 0 try: data = self.frag_decoder(data) except ValueError: self.frag_decoder.reset() self.log.error('Client sent undecodeable UTF-8') return self.websocket_close(INCONSISTENT_DATA, 'Not valid UTF-8') if message_finished: if (not data and not empty_data) or self.frag_decoder.state: self.frag_decoder.reset() self.log.error('Client sent undecodeable UTF-8') return self.websocket_close(INCONSISTENT_DATA, 'Not valid UTF-8') if message_finished: self.current_recv_opcode = None self.frag_decoder.reset() try: self.handle_websocket_data(data, message_starting, message_finished) except Exception as err: self.log.exception('Error in WebSockets data handler:') self.websocket_close(UNEXPECTED_ERROR, 'Unexpected error in handler: %r' % as_unicode(err)) def ws_control_frame(self, opcode, data): if opcode in (PING, CLOSE): rcode = PONG if opcode == PING else CLOSE if opcode == CLOSE: self.ws_close_received = True self.stop_reading = True if data: try: close_code = unpack_from(b'!H', data)[0] except struct_error: data = pack(b'!H', PROTOCOL_ERROR) + b'close frame data must be at least two bytes' else: try: utf8_decode(data[2:]) except ValueError: data = pack(b'!H', PROTOCOL_ERROR) + b'close frame data must be valid UTF-8' else: if close_code < 1000 or close_code in RESERVED_CLOSE_CODES or (1011 < close_code < 3000): data = pack(b'!H', PROTOCOL_ERROR) + b'close code reserved' else: close_code = NORMAL_CLOSE data = pack(b'!H', close_code) f = ReadOnlyFileBuffer(create_frame(1, rcode, data)) f.is_close_frame = opcode == CLOSE with self.cf_lock: self.control_frames.append(f) elif opcode == PONG: try: self.websocket_handler.handle_websocket_pong(self.websocket_connection_id, data) except Exception: self.log.exception('Error in PONG handler:') self.set_ws_state() def websocket_close(self, code=NORMAL_CLOSE, reason=b''): if isinstance(reason, str): reason = reason.encode('utf-8') self.stop_reading = True reason = reason[:123] if code is None and not reason: f = ReadOnlyFileBuffer(create_frame(1, CLOSE, b'')) else: f = ReadOnlyFileBuffer(create_frame(1, CLOSE, pack(b'!H', code) + reason)) f.is_close_frame = True with self.cf_lock: self.control_frames.append(f) self.set_ws_state() def ws_write(self): if self.ws_close_sent: return if self.send_buf is not None: if self.write(self.send_buf): self.end_send_optimization() if getattr(self.send_buf, 'is_close_frame', False): self.ws_close_sent = True self.send_buf = None else: with self.cf_lock: try: self.send_buf = self.control_frames.popleft() except IndexError: if self.sending is not None: self.send_buf = self.sending.create_frame() if self.send_buf is None: self.sending = None if self.send_buf is not None: self.optimize_for_sending_packet() def close(self): if self.in_websocket_mode: try: self.websocket_handler.handle_websocket_close(self.websocket_connection_id) except Exception: self.log.exception('Error in WebSocket close handler') # Try to write a close frame, just once try: if self.send_buf is None and not self.ws_close_sent: self.websocket_close(SHUTTING_DOWN, 'Shutting down') with self.cf_lock: self.write(self.control_frames.pop()) except Exception: pass Connection.close(self) else: HTTPConnection.close(self) # }}} def send_websocket_message(self, buf, wakeup=True): ''' Send a complete message. This class will take care of splitting it into appropriate frames automatically. `buf` must be a file like object. ''' self.sendq.put(MessageWriter(buf)) self.wait_for = RDWR if wakeup: self.wakeup() def send_websocket_frame(self, data, is_first=True, is_last=True): ''' Useful for streaming handlers that want to break up messages into frames themselves. Note that these frames will be interleaved with control frames, so they should not be too large. ''' opcode = (TEXT if isinstance(data, str) else BINARY) if is_first else CONTINUATION fin = 1 if is_last else 0 frame = create_frame(fin, opcode, data) with self.cf_lock: self.control_frames.append(ReadOnlyFileBuffer(frame)) def send_websocket_ping(self, data=b''): ''' Send a PING to the remote client, it should reply with a PONG which will be sent to the handle_websocket_pong callback in your handler. ''' if isinstance(data, str): data = data.encode('utf-8') frame = create_frame(True, PING, data) with self.cf_lock: self.control_frames.append(ReadOnlyFileBuffer(frame)) def handle_websocket_data(self, data, message_starting, message_finished): ''' Called when some data is received from the remote client. In general the data may not constitute a complete "message", use the message_starting and message_finished flags to re-assemble it into a complete message in the handler. Note that for binary data, data is a mutable object. If you intend to keep it around after this method returns, create a bytestring from it, using tobytes(). ''' self.websocket_handler.handle_websocket_data(self.websocket_connection_id, data, message_starting, message_finished) class DummyHandler: def handle_websocket_upgrade(self, connection_id, connection_ref, inheaders): conn = connection_ref() conn.websocket_close(NORMAL_CLOSE, 'No WebSocket handler available') def handle_websocket_data(self, connection_id, data, message_starting, message_finished): pass def handle_websocket_pong(self, connection_id, data): pass def handle_websocket_close(self, connection_id): pass # Testing {{{ # Run this file with calibre-debug and use wstest to run the Autobahn test # suite class EchoHandler: def __init__(self, *args, **kwargs): self.ws_connections = {} def conn(self, cid): ans = self.ws_connections.get(cid) if ans is not None: ans = ans() return ans def handle_websocket_upgrade(self, connection_id, connection_ref, inheaders): self.ws_connections[connection_id] = connection_ref def handle_websocket_data(self, connection_id, data, message_starting, message_finished): self.conn(connection_id).send_websocket_frame(data, message_starting, message_finished) def handle_websocket_pong(self, connection_id, data): pass def handle_websocket_close(self, connection_id): self.ws_connections.pop(connection_id, None) def run_echo_server(): s = ServerLoop(create_http_handler(websocket_handler=EchoHandler())) with HandleInterrupt(s.wakeup): s.serve_forever() if __name__ == '__main__': # import cProfile # cProfile.runctx('r()', {'r':run_echo_server}, {}, filename='stats.profile') run_echo_server() # }}}