Files
azul/front/oscar/ctrl.py
T
Athena Funderburg 21f38ee3e1 production init
2026-05-26 16:41:23 +00:00

222 lines
6.6 KiB
Python

import asyncio
import random
import struct
from core.backend import Backend, BackendSession, Chat, ChatSession
from core.client import Client
from core.models import LoginOption
from itertools import cycle
from typing import Optional, Callable, Tuple
from util.misc import Logger
from .proto.backend import BackendEventHandler, FOODGROUP_VERSIONS, login, LoginError, bos_cookies
from .proto.snac import OSCARClient, OSCARContext, SNACMessage, foodgroups
from .proto.tlv import unmarshal_tlvs, find_tlv
ROASTING_CHARS = b'\xF3\x26\x81\xC4\x39\x86\xDB\x92\x71\xA3\xB9\xE6\x53\x7A\x95\x7C'
def roast(password: bytes) -> bytes:
chars = cycle(ROASTING_CHARS)
return bytes(byte ^ next(chars) for byte in password)
# TODO(subpurple): as the foodgroups are no longer handled just in this file, might want to combine this and entry
class OSCARCtrl:
logger: Logger
transport: Optional[asyncio.WriteTransport]
close_callback: Optional[Callable[[], None]]
closed: bool
backend: Backend
bs: Optional[BackendSession]
client: Client
oscarClient: OSCARClient
context: OSCARContext
sequence: int = random.randint(0x0000, 0xFFFF)
def __init__(self, logger: Logger, via: str, backend: Backend) -> None:
self.logger = logger
self.transport = None
self.close_callback = None
self.closed = False
self.backend = backend
self.bs = None
self.client = Client('aim', '?', via)
self.context = OSCARContext(self.backend, self.client)
self.oscarClient = OSCARClient(self)
def send_specific_frame(self, frame: int, data: bytes) -> None:
if self.sequence == 0xFFFF:
self.sequence = 0x0000
else:
self.sequence += 1
packet = b''.join([
struct.pack('>BBHH', 0x2A, frame, self.sequence, len(data)),
data
])
self.transport.write(packet)
def send_snac(self, msg: SNACMessage) -> None:
self.send_specific_frame(0x02, msg.marshal())
def on_connect(self) -> None:
self.send_specific_frame(0x01, struct.pack('>L', 1))
def on_signon_frame(self, data: bytes) -> None:
if len(data) > 4:
tlvs = unmarshal_tlvs(data[4:])
if (cookie_tlv := find_tlv(tlvs, 0x0006)) is not None:
found = False
for i, d in enumerate(bos_cookies):
for cookie in d.items():
if cookie[0] == cookie_tlv.data:
if hasattr(cookie[1], 'user') and getattr(cookie[1].user, 'uuid', None) is not None:
uuid = cookie[1].user.uuid
elif getattr(cookie[1], 'uuid', None) is not None:
uuid = cookie[1].uuid
else:
uuid = cookie[1]
self.logger.info('found BOS cookie')
self.context.bs = self.backend.login(uuid,
self.client,
BackendEventHandler(self),
option=LoginOption.BootOthers)
self.context.user = self.context.bs.user
self.bs = self.context.bs
bos_cookies.pop(i)
found = True
break
if found:
break
if found:
# NINA also sends OSERVICE__WELL_KNOWN_URLS right after (should we?)
self.logger.info('[Server] OSERVICE__HOST_ONLINE')
msg = SNACMessage(0x0001, 0x0003)
for foodgroup in FOODGROUP_VERSIONS.keys():
msg.write_u16(foodgroup)
self.send_snac(msg)
self.bs.front_data['oscar0'] = True
self.bs.front_data['oscar0_chats'] = {}
else:
self.logger.info('invalid BOS cookie given')
else:
self.logger.info('Using FLAP-level authentication')
screen_name_tlv = find_tlv(tlvs, 0x0001)
roasted_pw_tlv = find_tlv(tlvs, 0x0002)
screen_name = screen_name_tlv.data.decode()
roasted_pw = roasted_pw_tlv.data
self.logger.info('Screen Name (client-given):', screen_name)
self.logger.info('Password (roasted):', roasted_pw.hex())
context = self.context
error_code = None
if (uuid := context.backend.util_get_uuid_from_username(screen_name)) is None:
error_code = LoginError.UnregisteredScreenname
self.logger.info('Unregistered screenname')
else:
unroasted_pw = roast(roasted_pw)
if context.backend.user_service.login_with_username(screen_name, unroasted_pw.decode()) is None:
error_code = LoginError.IncorrectPassword
self.logger.info('Incorrect password')
self.bs.front_data['oscar0'] = True
self.bs.front_data['oscar0_chats'] = {}
self.send_specific_frame(0x04, login(self.logger, self.context, tlvs, uuid, error_code))
def on_data_frame(self, message: SNACMessage) -> None:
found = False
# kick client off if we haven't authenticated and client is trying to access something outside of BUCP
if message.foodgroup != 0x0017 and self.bs is None:
self.close()
return
for value, cls in foodgroups.items():
if value == message.foodgroup:
if not hasattr(cls, 'logger'):
cls.logger = self.logger
if message.subgroup in cls.subgroups:
found = True
func = cls.subgroups[message.subgroup]
func(cls, self.oscarClient, self.context, message)
if not found:
self.logger.info(f'[Client] Unknown SNAC({hex(message.foodgroup)},{hex(message.subgroup)})')
self.logger.info('[Client]', message.data.hex())
def on_error_frame(self, data: bytes) -> None:
self.logger.info('[Client] Recieved error frame with:', data.hex())
def on_signoff_frame(self, data: bytes) -> None:
self.logger.info('[Client] Recieved signoff frame')
def close(self) -> None:
if self.closed:
return
self.closed = True
if self.close_callback:
self.close_callback()
if self.bs:
self.bs.close()
def _get_private_chat_with(self, other_user_uuid: str) -> Tuple[ChatSession, 'ChatEventHandler']:
bs = self.bs
assert bs is not None
user = bs.user
detail = user.detail
assert detail is not None
other_user = self.backend._load_user_record(other_user_uuid)
if other_user is None:
raise error.ContactNotOnContactList()
other_user_ctc = detail.contacts.get(other_user.uuid)
if other_user_ctc is not None and other_user_ctc.lists & ContactList.BL:
raise error.ContactNotOnContactList()
if other_user_uuid not in bs.front_data['oscar0_chats']:
other_user_detail = self.backend._load_detail(other_user)
if other_user_detail is None: raise error.ContactNotOnline()
ctc_self = other_user_detail.contacts.get(user.uuid)
if ctc_self is not None:
if ctc_self.lists & ContactList.BL: raise error.ContactNotOnline()
chat = self.backend.chat_create()
chat.front_data['oscar0'] = True
# `user` joins
evt = ChatEventHandler(self.backend.loop, self, bs)
cs = chat.join('icbm', bs, evt)
bs.front_data['oscar0_chats'][other_user_uuid] = (cs, evt)
cs.invite(other_user)
elif other_user.status.is_offlineish():
raise error.ContactNotOnline()
return bs.front_data['oscar0_chats'].get(other_user_uuid)