mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
240 lines
7.4 KiB
Python
240 lines
7.4 KiB
Python
import asyncio
|
|
import random
|
|
import struct
|
|
|
|
from core import error
|
|
from core.backend import Backend, BackendSession, Chat, ChatSession
|
|
from core.client import Client
|
|
from core.models import LoginOption, ContactList
|
|
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.chat import ChatEventHandler
|
|
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
|
|
is_service_connection = False
|
|
|
|
for i, d in enumerate(bos_cookies):
|
|
for cookie in d.items():
|
|
if cookie[0] == cookie_tlv.data:
|
|
self.logger.info('found BOS cookie')
|
|
|
|
if hasattr(cookie[1], 'user') and getattr(cookie[1].user, 'uuid', None) is not None:
|
|
# cookie maps to an existing BackendSession (placed there by OSERVICE__SERVICE_REQUEST)
|
|
# reuse it directly so the main session is not booted and its substatus is preserved
|
|
self.context.bs = cookie[1]
|
|
self.context.user = cookie[1].user
|
|
self.bs = cookie[1]
|
|
is_service_connection = True
|
|
else:
|
|
if isinstance(cookie[1], dict):
|
|
uuid = cookie[1].get('uuid')
|
|
version = cookie[1].get('version', '?')
|
|
self.client = Client('aim', version, self.client.via)
|
|
self.context.client = self.client
|
|
elif getattr(cookie[1], 'uuid', None) is not None:
|
|
uuid = cookie[1].uuid
|
|
else:
|
|
uuid = cookie[1]
|
|
|
|
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)
|
|
|
|
if not is_service_connection:
|
|
# only initialize these keys on the primary BOS connection
|
|
# so other service connections don't wipe the chat state built up so far
|
|
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) |