This commit is contained in:
Athena Funderburg
2026-05-25 07:05:17 +00:00
commit 4b463a3432
682 changed files with 47796 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
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('<H', header[:2])[0] # type: int
else:
version = struct.unpack('!H', header[:2])[0]
(vendor_id, n, service, status, session_id) = struct.unpack('!HHHII', header[2:]) # type: Tuple[int, int, int, int, int]
assert version in YMSG_DIALECTS
assert e+n <= len(d[i:])
payload = d[e:e+n]
if payload:
parts = payload.split(SEP)
del parts[-1]
assert len(parts) % 2 == 0
for j in range(1, len(parts), 2):
kvs.add(parts[j-1], parts[j])
e += n
return ((YMSGService(service), version, vendor_id, YMSGStatus(status), session_id, kvs), e)
def _truncated_kvs(service: YMSGService, kvs: KVS) -> 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
]