import io, asyncio, binascii, struct, settings from abc import ABCMeta, abstractmethod from typing import Tuple, Any, Optional, Callable, Iterable from core import error from util.misc import Logger, MultiDict from .misc import YMSGStatus, YMSGService KVS = MultiDict[bytes, bytes] class YMSGCtrlBase(metaclass = ABCMeta): __slots__ = ('logger', 'decoder', 'encoder', 'peername', 'closed', 'close_callback', 'transport', 'session_id') logger: Logger decoder: 'YMSGDecoder' encoder: 'YMSGEncoder' peername: Tuple[str, int] close_callback: Optional[Callable[[], None]] closed: bool transport: Optional[asyncio.WriteTransport] session_id: int def __init__(self, logger: Logger) -> None: self.logger = logger self.decoder = YMSGDecoder(logger) self.encoder = YMSGEncoder(logger) self.peername = ('0.0.0.0', 5050) self.closed = False self.close_callback = None self.transport = None self.session_id = 0 def data_received(self, data: bytes, *, transport: Optional[asyncio.BaseTransport] = None) -> None: if transport is None: transport = self.transport assert transport is not None self.peername = transport.get_extra_info('peername') for y in self.decoder.data_received(data): try: # check version and vendorId if y[1] > 18 or y[2] not in (0, 1, 100): continue if y[4]: self.session_id = y[4] f = getattr(self, '_y_{}'.format(binascii.hexlify(struct.pack('!H', y[0])).decode())) f(*y[1:]) except Exception as ex: self.logger.error(ex) def send_reply(self, service: YMSGService, status: YMSGStatus, session_id: int, kvs: Optional[KVS] = None) -> None: if session_id == 0: session_id = self.session_id try: self.encoder.encode(service, status, session_id, kvs) except error.DataTooLargeToSend: return transport = self.transport if transport is not None: transport.write(self.flush()) def flush(self) -> bytes: return self.encoder.flush() def close(self, **kwargs: Any) -> None: if self.closed: return self.closed = True if self.close_callback: self.close_callback() self._on_close(**kwargs) @abstractmethod def _on_close(self, remove_sess_id: bool = True) -> None: pass class YMSGEncoder: __slots__ = ('_logger', '_buf') _logger: Logger _buf: io.BytesIO def __init__(self, logger: Logger) -> None: self._logger = logger self._buf = io.BytesIO() def encode(self, service: YMSGService, status: YMSGStatus, session_id: int, kvs: Optional[KVS] = None) -> None: payload_list = [] if kvs is not None: k = None # type: Optional[bytes] v = None # type: Optional[bytes] for k, v in kvs.items(): payload_list.extend([k, SEP, v, SEP]) payload = b''.join(payload_list) # TODO: Yahoo!'s servers used to split large payloads into packet chunks, # but there's little information on how it was exactly handled. # Just drop packets if they're too big (for the length field to handle unfortunately) until we can find a solution. if len(payload) > 0xffff: raise error.DataTooLargeToSend() w = self._buf.write w(PRE) # version number and vendor id are replaced with 0x00000000 w(b'\x00\x00\x00\x00') # Have to call `int` on these because they might be an IntEnum, which # get `repr`'d to `EnumName.ValueName`. Grr. w(struct.pack('!HHII', len(payload), int(service), int(status), session_id)) w(payload) self._logger.debug('[Server]', service, status, session_id) if kvs: _truncated_kvs(service, kvs) def flush(self) -> bytes: data = self._buf.getvalue() if data: #self._logger.info('<<<', data) self._buf = io.BytesIO() return data DecodedYMSG = Tuple[YMSGService, int, int, YMSGStatus, int, KVS] class YMSGDecoder: __slots__ = ('logger', '_data', '_i') logger: Logger _data: bytes _i: int def __init__(self, logger: Logger) -> None: self.logger = logger self._data = b'' self._i = 0 def data_received(self, data: bytes) -> Iterable[DecodedYMSG]: if self._data: self._data += data else: self._data = data while self._data: y = self._ymsg_read() if y is None: break yield y def _ymsg_read(self) -> Optional[DecodedYMSG]: try: y, e = _try_decode_ymsg(self._data, self._i) except Exception: print("ERR _ymsg_read", self._data) raise self._data = self._data[e:] self._i = 0 self.logger.debug('[Client]', 'YMSG{}'.format(str(y[1])), y[0], y[3], y[4]) _truncated_kvs(y[0], y[5]) return y def _try_decode_ymsg(d: bytes, i: int) -> Tuple[DecodedYMSG, int]: kvs = MultiDict() # type: KVS e = 20 assert len(d[i:]) >= e assert d[i:i+4] == PRE header = d[i+4:i+e] if header[:2] in (b'\x08\x00',b'\x09\x00',b'\x0a\x00'): version = struct.unpack(' None: restricted_keys = set() if service in (YMSGService.AuthResp, YMSGService.List): restricted_keys.add(b'59') if service in ( YMSGService.Message, YMSGService.MassMessage, YMSGService.ContactNew, YMSGService.FriendAdd, YMSGService.ContactDeny, YMSGService.ConfDecline, YMSGService.ConfMsg,YMSGService.P2PFileXfer, YMSGService.FileTransfer ): restricted_keys.add(b'14') if service in (YMSGService.ConfInvite, YMSGService.ConfAddInvite): restricted_keys.add(b'58') if service in (YMSGService.P2PFileXfer, YMSGService.FileTransfer): restricted_keys.add(b'20') if settings.DEBUG: for k, v in kvs.items(): print('{!r} -> {}'.format(k, (v))) PRE = b'YMSG' SEP = b'\xC0\x80' YMSG_DIALECTS = [ # Not actually supported 18, 17, 16, 8, # Actually supported 15, 14, 13, 12, 11, 10, 9 ]