mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
init
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user