def send_message(self, message, end=True, binary=False): """Send message. Args: message: text in unicode or binary in str to send. binary: send message as binary frame. Raises: BadOperationException: when called on a server-terminated connection or called with inconsistent message type or binary parameter. """ if self._request.server_terminated: raise BadOperationException( 'Requested send_message after sending out a closing handshake') if binary and isinstance(message, unicode): raise BadOperationException( 'Message for binary frame must be instance of str') try: self._write(self._writer.build(message, end, binary)) except ValueError, e: raise BadOperationException(e)
def send_message(self, message, end=True, binary=False): """Send message. Args: message: unicode string to send. binary: not used in hixie75. Raises: BadOperationException: when called on a server-terminated connection. """ if not end: raise BadOperationException( 'StreamHixie75 doesn\'t support send_message with end=False') if binary: raise BadOperationException( 'StreamHixie75 doesn\'t support send_message with binary=True') if self._request.server_terminated: raise BadOperationException( 'Requested send_message after sending out a closing handshake') self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='', wait_response=True): """Closes a WebSocket connection. Note that this method blocks until it receives acknowledgement to the closing handshake. Args: code: Status code for close frame. If code is None, a close frame with empty body will be sent. reason: string representing close reason. wait_response: True when caller want to wait the response. Raises: BadOperationException: when reason is specified with code None or reason is not an instance of both str and unicode. """ if self._request.server_terminated: self._logger.debug( 'Requested close_connection but server is already terminated') return # When we receive a close frame, we call _process_close_message(). # _process_close_message() immediately acknowledges to the # server-initiated closing handshake and sets server_terminated to # True. So, here we can assume that we haven't received any close # frame. We're initiating a closing handshake. if code is None: if reason is not None and len(reason) > 0: raise BadOperationException( 'close reason must not be specified if code is None') reason = '' else: if not isinstance(reason, str) and not isinstance(reason, unicode): raise BadOperationException( 'close reason must be an instance of str or unicode') self._send_closing_handshake(code, reason) self._logger.debug('Initiated closing handshake (code=%r, reason=%r)', code, reason) if (code == common.STATUS_GOING_AWAY or code == common.STATUS_PROTOCOL_ERROR) or not wait_response: # It doesn't make sense to wait for a close frame if the reason is # protocol error or that the server is going away. For some of # other reasons, it might not make sense to wait for a close frame, # but it's not clear, yet. return # TODO(ukai): 2. wait until the /client terminated/ flag has been set, # or until a server-defined timeout expires. # # For now, we expect receiving closing handshake right after sending # out closing handshake. message = self.receive_message() if message is not None: raise ConnectionTerminatedException( 'Didn\'t receive valid ack for closing handshake')
def send_message(self, message, end=True, binary=False): """Send message. Args: message: text in unicode or binary in str to send. binary: send message as binary frame. Raises: BadOperationException: when called on a server-terminated connection or called with inconsistent message type or binary parameter. """ if self._request.server_terminated: raise BadOperationException( 'Requested send_message after sending out a closing handshake') if binary and isinstance(message, unicode): raise BadOperationException( 'Message for binary frame must be instance of str') for message_filter in self._options.outgoing_message_filters: message = message_filter.filter(message, end, binary) try: # Set this to any positive integer to limit maximum size of data in # payload data of each frame. MAX_PAYLOAD_DATA_SIZE = -1 if MAX_PAYLOAD_DATA_SIZE <= 0: self._write(self._writer.build(message, end, binary)) return bytes_written = 0 while True: end_for_this_frame = end bytes_to_write = len(message) - bytes_written if (MAX_PAYLOAD_DATA_SIZE > 0 and bytes_to_write > MAX_PAYLOAD_DATA_SIZE): end_for_this_frame = False bytes_to_write = MAX_PAYLOAD_DATA_SIZE frame = self._writer.build( message[bytes_written:bytes_written + bytes_to_write], end_for_this_frame, binary) self._write(frame) bytes_written += bytes_to_write # This if must be placed here (the end of while block) so that # at least one frame is sent. if len(message) <= bytes_written: break except ValueError, e: raise BadOperationException(e)
def create_closing_handshake_body(code, reason): body = '' if code is not None: if (code > common.STATUS_USER_PRIVATE_MAX or code < common.STATUS_NORMAL_CLOSURE): raise BadOperationException('Status code is out of range') if (code == common.STATUS_NO_STATUS_RECEIVED or code == common.STATUS_ABNORMAL_CLOSURE or code == common.STATUS_TLS_HANDSHAKE): raise BadOperationException('Status code is reserved pseudo ' 'code') encoded_reason = reason.encode('utf-8') body = struct.pack('!H', code) + encoded_reason return body
def receive_message(self): """Receive a WebSocket frame and return its payload an unicode string. Returns: payload unicode string in a WebSocket frame. Raises: ConnectionTerminatedException: when read returns empty string. BadOperationException: when called on a client-terminated connection. """ if self._request.client_terminated: raise BadOperationException( 'Requested receive_message after receiving a closing ' 'handshake') while True: # Read 1 byte. # mp_conn.read will block if no bytes are available. # Timeout is controlled by TimeOut directive of Apache. frame_type_str = self.receive_bytes(1) frame_type = ord(frame_type_str) if (frame_type & 0x80) == 0x80: # The payload length is specified in the frame. # Read and discard. length = self._read_payload_length_hixie75() if length > 0: _ = self.receive_bytes(length) # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the # /client terminated/ flag and abort these steps. if not self._enable_closing_handshake: continue if frame_type == 0xFF and length == 0: self._request.client_terminated = True if self._request.server_terminated: self._logger.debug( 'Received ack for server-initiated closing ' 'handshake') return None self._logger.debug( 'Received client-initiated closing handshake') self._send_closing_handshake() self._logger.debug( 'Sent ack for client-initiated closing handshake') return None else: # The payload is delimited with \xff. bytes = self._read_until('\xff') # The WebSocket protocol section 4.4 specifies that invalid # characters must be replaced with U+fffd REPLACEMENT # CHARACTER. message = bytes.decode('utf-8', 'replace') if frame_type == 0x00: return message
def _send_closing_handshake(self, code, reason): if code >= (1 << 16) or code < 0: raise BadOperationException('Status code is out of range') encoded_reason = reason.encode('utf-8') if len(encoded_reason) + 2 > 125: raise BadOperationException( 'Application data size of close frames must be 125 bytes or ' 'less') frame = create_close_frame( struct.pack('!H', code) + encoded_reason, self._options.mask_send) self._request.server_terminated = True self._write(frame)
def _send_closing_handshake(self, code, reason): body = '' if code is not None: if (code > common.STATUS_USER_PRIVATE_MAX or code < common.STATUS_NORMAL_CLOSURE): raise BadOperationException('Status code is out of range') if (code == common.STATUS_NO_STATUS_RECEIVED or code == common.STATUS_ABNORMAL_CLOSURE or code == common.STATUS_TLS_HANDSHAKE): raise BadOperationException('Status code is reserved pseudo ' 'code') encoded_reason = reason.encode('utf-8') body = struct.pack('!H', code) + encoded_reason frame = create_close_frame(body, self._options.mask_send, self._options.outgoing_frame_filters) self._request.server_terminated = True self._write(frame)
def _send_closing_handshake(self): if not self._enable_closing_handshake: raise BadOperationException( 'Closing handshake is not supported in Hixie 75 protocol') self._request.server_terminated = True # 5.3 the server may decide to terminate the WebSocket connection by # running through the following steps: # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the # start of the closing handshake. self._write('\xff\x00')
def _create_control_frame(opcode, body, mask, frame_filters): frame = Frame(opcode=opcode, payload=body) for frame_filter in frame_filters: frame_filter.filter(frame) if len(frame.payload) > 125: raise BadOperationException( 'Payload data size of control frames must be 125 bytes or less') header = create_header(frame.opcode, len(frame.payload), frame.fin, frame.rsv1, frame.rsv2, frame.rsv3, mask) return _build_frame(header, frame.payload, mask)
def send_message(self, message, end=True): """Send message. Args: message: unicode string to send. Raises: BadOperationException: when called on a server-terminated connection. """ if self._request.server_terminated: raise BadOperationException( 'Requested send_message after sending out a closing handshake') self._write(self._writer.build(message, end))
def send_ping(self, body): raise BadOperationException( 'StreamHixie75 doesn\'t support send_ping')
def receive_message(self): """Receive a WebSocket frame and return its payload as a text in unicode or a binary in str. Returns: payload data of the frame - as unicode instance if received text frame - as str instance if received binary frame or None iff received closing handshake. Raises: BadOperationException: when called on a client-terminated connection. ConnectionTerminatedException: when read returns empty string. InvalidFrameException: when the frame contains invalid data. UnsupportedFrameException: when the received frame has flags, opcode we cannot handle. You can ignore this exception and continue receiving the next frame. """ if self._request.client_terminated: raise BadOperationException( 'Requested receive_message after receiving a closing ' 'handshake') while True: # mp_conn.read will block if no bytes are available. # Timeout is controlled by TimeOut directive of Apache. frame = self._receive_frame_as_frame_object() # Check the constraint on the payload size for control frames # before extension processes the frame. # See also http://tools.ietf.org/html/rfc6455#section-5.5 if (common.is_control_opcode(frame.opcode) and len(frame.payload) > 125): raise InvalidFrameException( 'Payload data size of control frames must be 125 bytes or ' 'less') for frame_filter in self._options.incoming_frame_filters: frame_filter.filter(frame) if frame.rsv1 or frame.rsv2 or frame.rsv3: raise UnsupportedFrameException( 'Unsupported flag is set (rsv = %d%d%d)' % (frame.rsv1, frame.rsv2, frame.rsv3)) message = self._get_message_from_frame(frame) if message is None: continue for message_filter in self._options.incoming_message_filters: message = message_filter.filter(message) if self._original_opcode == common.OPCODE_TEXT: # The WebSocket protocol section 4.4 specifies that invalid # characters must be replaced with U+fffd REPLACEMENT # CHARACTER. try: return message.decode('utf-8') except UnicodeDecodeError, e: raise InvalidUTF8Exception(e) elif self._original_opcode == common.OPCODE_BINARY: return message
def receive_message(self): """Receive a WebSocket frame and return its payload an unicode string. Returns: payload unicode string in a WebSocket frame. None iff received closing handshake. Raises: BadOperationException: when called on a client-terminated connection. ConnectionTerminatedException: when read returns empty string. InvalidFrameException: when the frame contains invalid data. UnsupportedFrameException: when the received frame has flags, opcode we cannot handle. You can ignore this exception and continue receiving the next frame. """ if self._request.client_terminated: raise BadOperationException( 'Requested receive_message after receiving a closing ' 'handshake') while True: # mp_conn.read will block if no bytes are available. # Timeout is controlled by TimeOut directive of Apache. opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame() if rsv1 or rsv2 or rsv3: raise UnsupportedFrameException( 'Unsupported flag is set (rsv = %d%d%d)' % (rsv1, rsv2, rsv3)) if opcode == common.OPCODE_CONTINUATION: if not self._received_fragments: if fin: raise InvalidFrameException( 'Received a termination frame but fragmentation ' 'not started') else: raise InvalidFrameException( 'Received an intermediate frame but ' 'fragmentation not started') if fin: # End of fragmentation frame self._received_fragments.append(bytes) message = ''.join(self._received_fragments) self._received_fragments = [] else: # Intermediate frame self._received_fragments.append(bytes) continue else: if self._received_fragments: if fin: raise InvalidFrameException( 'Received an unfragmented frame without ' 'terminating existing fragmentation') else: raise InvalidFrameException( 'New fragmentation started without terminating ' 'existing fragmentation') if fin: # Unfragmented frame self._original_opcode = opcode message = bytes if is_control_opcode(opcode) and len(message) > 125: raise InvalidFrameException( 'Application data size of control frames must be ' '125 bytes or less') else: # Start of fragmentation frame if is_control_opcode(opcode): raise InvalidFrameException( 'Control frames must not be fragmented') self._original_opcode = opcode self._received_fragments.append(bytes) continue if self._original_opcode == common.OPCODE_TEXT: # The WebSocket protocol section 4.4 specifies that invalid # characters must be replaced with U+fffd REPLACEMENT # CHARACTER. return message.decode('utf-8', 'replace') elif self._original_opcode == common.OPCODE_CLOSE: self._request.client_terminated = True # Status code is optional. We can have status reason only if we # have status code. Status reason can be empty string. So, # allowed cases are # - no application data: no code no reason # - 2 octet of application data: has code but no reason # - 3 or more octet of application data: both code and reason if len(message) == 1: raise InvalidFrameException( 'If a close frame has status code, the length of ' 'status code must be 2 octet') elif len(message) >= 2: self._request.ws_close_code = struct.unpack( '!H', message[0:2])[0] self._request.ws_close_reason = message[2:].decode( 'utf-8', 'replace') self._logger.debug('Initiated flush read') self.flushread() if self._request.server_terminated: self._logger.debug( 'Received ack for server-initiated closing ' 'handshake') return None self._logger.debug( 'Received client-initiated closing handshake') self._send_closing_handshake(common.STATUS_NORMAL, '') self._logger.debug( 'Sent ack for client-initiated closing handshake') return None elif self._original_opcode == common.OPCODE_PING: try: handler = self._request.on_ping_handler if handler: handler(self._request, message) continue except AttributeError, e: pass self._send_pong(message) elif self._original_opcode == common.OPCODE_PONG: # TODO(tyoshino): Add ping timeout handling. inflight_pings = deque() while True: try: expected_body = self._ping_queue.popleft() if expected_body == message: # inflight_pings contains pings ignored by the # other peer. Just forget them. self._logger.debug( 'Ping %r is acked (%d pings were ignored)', expected_body, len(inflight_pings)) break else: inflight_pings.append(expected_body) except IndexError, e: # The received pong was unsolicited pong. Keep the # ping queue as is. self._ping_queue = inflight_pings self._logger.debug('Received a unsolicited pong') break
def receive_message(self): """Receive a WebSocket frame and return its payload as a text in unicode or a binary in str. Returns: payload data of the frame - as unicode instance if received text frame - as str instance if received binary frame or None iff received closing handshake. Raises: BadOperationException: when called on a client-terminated connection. ConnectionTerminatedException: when read returns empty string. InvalidFrameException: when the frame contains invalid data. UnsupportedFrameException: when the received frame has flags, opcode we cannot handle. You can ignore this exception and continue receiving the next frame. """ if self._request.client_terminated: raise BadOperationException( 'Requested receive_message after receiving a closing ' 'handshake') while True: # mp_conn.read will block if no bytes are available. # Timeout is controlled by TimeOut directive of Apache. frame = self._receive_frame_as_frame_object() for frame_filter in self._options.incoming_frame_filters: frame_filter.filter(frame) if frame.rsv1 or frame.rsv2 or frame.rsv3: raise UnsupportedFrameException( 'Unsupported flag is set (rsv = %d%d%d)' % (frame.rsv1, frame.rsv2, frame.rsv3)) if frame.opcode == common.OPCODE_CONTINUATION: if not self._received_fragments: if frame.fin: raise InvalidFrameException( 'Received a termination frame but fragmentation ' 'not started') else: raise InvalidFrameException( 'Received an intermediate frame but ' 'fragmentation not started') if frame.fin: # End of fragmentation frame self._received_fragments.append(frame.payload) message = ''.join(self._received_fragments) self._received_fragments = [] else: # Intermediate frame self._received_fragments.append(frame.payload) continue else: if self._received_fragments: if frame.fin: raise InvalidFrameException( 'Received an unfragmented frame without ' 'terminating existing fragmentation') else: raise InvalidFrameException( 'New fragmentation started without terminating ' 'existing fragmentation') if frame.fin: # Unfragmented frame if (common.is_control_opcode(frame.opcode) and len(frame.payload) > 125): raise InvalidFrameException( 'Application data size of control frames must be ' '125 bytes or less') self._original_opcode = frame.opcode message = frame.payload else: # Start of fragmentation frame if common.is_control_opcode(frame.opcode): raise InvalidFrameException( 'Control frames must not be fragmented') self._original_opcode = frame.opcode self._received_fragments.append(frame.payload) continue if self._original_opcode == common.OPCODE_TEXT: # The WebSocket protocol section 4.4 specifies that invalid # characters must be replaced with U+fffd REPLACEMENT # CHARACTER. try: return message.decode('utf-8') except UnicodeDecodeError, e: raise InvalidUTF8Exception(e) elif self._original_opcode == common.OPCODE_BINARY: return message