from typing import Optional, Any, Tuple from datetime import datetime, timedelta from io import BytesIO from email.parser import Parser from email.header import decode_header from email.utils import parsedate_to_datetime from urllib.parse import unquote, parse_qsl from pathlib import Path import re, secrets, base64, random, json, mimetypes, uuid, PyRSS2Gen, settings, util.misc from markupsafe import Markup from aiohttp import web from core import models from core.backend import Backend, BackendSession from util.avatar import get_img_type, find_avatar_file from ..misc import gen_mail_data, format_oim, cid_format, puid_format from .util import find_element, get_tag_localname, render, preprocess_soap, unknown_soap, bool_to_str LOGIN_PATH = '/login.srf' TMPL_DIR = 'front/msn/http/tmpl' ETC_DIR = 'front/msn/etc' PP = 'Passport1.4' def register(app: web.Application) -> None: util.misc.add_to_jinja_env(app, 'msn', TMPL_DIR, globals = { 'date_format': util.misc.date_format, 'cid_format': cid_format, 'puid_format': puid_format, 'bool_to_str': bool_to_str, 'contact_is_favorite': _contact_is_favorite, 'datetime': datetime, }) # MSN >= 5 app.router.add_get('/nexus-mock', handle_nexus) app.router.add_get('/rdr/pprdr.asp', handle_nexus) app.router.add_get(LOGIN_PATH, handle_login) app.router.add_get('/svcs/mms/tabs.asp', handle_tabs) app.router.add_get('/svcs/mms/portal.asp', handle_portal) app.router.add_get('/svcs/mms/ads.asp', handle_msn5ads) # MSN >= 6 app.router.add_get('/etc/MsgrConfig', handle_msgrconfig) app.router.add_post('/etc/MsgrConfig', handle_msgrconfig) # TODO: this is stupid, find a way to make this case-insensitive app.router.add_get('/Config/MsgrConfig.asmx', handle_msgrconfig) app.router.add_post('/Config/MsgrConfig.asmx', handle_msgrconfig) app.router.add_get('/config/MsgrConfig.asmx', handle_msgrconfig) app.router.add_post('/config/MsgrConfig.asmx', handle_msgrconfig) app.router.add_get('/start', handle_today) # MSN >= 7.5 app.router.add_route('OPTIONS', '/NotRST.srf', handle_not_rst) app.router.add_post('/NotRST.srf', handle_not_rst) app.router.add_post('/RST.srf', handle_rst) app.router.add_post('////RST.srf', handle_rst) # libpurple what the FUCK, also this doesn't even work app.router.add_post('/RST2.srf', lambda req: handle_rst(req, rst2 = True)) # MSN >= 8 app.router.add_post('/storageservice/SchematizedStore.asmx', handle_storageservice) app.router.add_post('/ppsecure/sha1auth.srf', handle_sha1auth) app.router.add_post('/rsi/rsi.asmx', handle_rsi) app.router.add_post('/OimWS/oim.asmx', handle_oim) app.router.add_get('/svcs/msn-video-feeds/', handle_videofeed) app.router.add_get('/svcs/feed', lambda req: handle_videofeed(req, get_videos = True)) # MSN >= 14 app.router.add_post('/whatsnew/whatsnewservice.asmx', handle_whatsnew) app.router.add_get('/svcs/tab={id}', handle_tabs_newer) # MSN >= 15 app.router.add_post('/sqm/messenger/sqmserver.dll', handle_blankok) app.router.add_post('/sqm/WindowsLive/sqmserver.dll', handle_blankok) app.router.add_post('/uploaddata.aspx', handle_blankok) app.router.add_get('/ppcrlcheck.srf', handle_ppcrlcheck) app.router.add_post('/profile/profile.asmx', handle_blankok) # TODO: do the thing app.router.add_get(r'/~Live.ConfigServer/{junk:.*}/~op-GetClientConfig/{tail:.*}', handle_msgrconfig16) app.router.add_get(r'/~Live.ConfigServer.SuiteUpdate/{tail:.*}', handle_suiteupdate) app.router.add_get(r'/~Live.ConfigServer.PSA/{tail:.*}', handle_socialint) app.router.add_get('/svcs/GameBrowser', handle_gamebrowser) # Misc app.router.add_get('/{i}meen_{locale}/{id}', handle_msn_redirect) async def handle_blankok(req: web.Request) -> web.Response: return web.Response(status = 200) async def handle_storageservice(req: web.Request) -> web.Response: backend = req.app['backend'] header, action, bs, token = await preprocess_soap(req) assert bs is not None soapaction = (req.headers.get('SOAPAction') or '') if soapaction.startswith('"') and soapaction.endswith('"'): soapaction = soapaction[1:-1] storage_ns = ('w10' if soapaction.startswith('http://www.msn.com/webservices/storage/w10/') else '2008') action_str = get_tag_localname(action) now_str = util.misc.date_format(datetime.utcnow()) user = bs.user cachekey = secrets.token_urlsafe(172) cid = cid_format(user.uuid) if action_str == 'GetProfile': roaming_info = backend.user_service.get_roaming_info(user) assert roaming_info is not None # Resolve the active avatar using the md5 stored in users.avatar. # find_avatar_file returns None if no avatar has been uploaded yet. avatar_md5 = user.avatar mime = None image_size = 0 image_thumb_size = 0 if avatar_md5: full_file = find_avatar_file(user.uuid, avatar_md5, small=False) thumb_file = find_avatar_file(user.uuid, avatar_md5, small=True) if full_file and full_file.is_file(): ext = full_file.suffix.lstrip('.').lower() mime = ext image_size = full_file.stat().st_size if thumb_file and thumb_file.is_file(): image_thumb_size = thumb_file.stat().st_size return render(req, 'msn:storageservice/GetProfileResponse.xml', { 'storage_ns': storage_ns, 'cachekey': cachekey, 'cid': cid, 'pptoken1': token, 'user': user, 'now': now_str, 'mime': mime, 'size_static': image_size, 'size_small': image_thumb_size, 'roaming_info': roaming_info, 'host': settings.USERSTORAGE_HOST, }) if action_str == 'FindDocuments': # TODO: FindDocuments return render(req, 'msn:storageservice/FindDocumentsResponse.xml', { 'storage_ns': storage_ns, 'cachekey': cachekey, 'cid': cid, 'pptoken1': token, 'user': user, }) if action_str == 'UpdateProfile': delete_psm = False delete_name = False delete_avatar = False # TODO: More properties? # Update to roaming name/message # ``` # # # 862d987eb60b7a63!106 # # Update # Society is betrayal # Prosperity is the best medicine. :) # # # # ``` # Remove roaming message # ``` # # # bb4542ce2eacdbde!106 # # Update # %walkingphas3r% # 0 # # # # # true # # # # ``` expression_profile = find_element(action, 'ExpressionProfile') name = find_element(expression_profile, 'DisplayName') message = find_element(expression_profile, 'PersonalStatus') attributes_to_delete = find_element(action, 'profileAttributesToDelete/ExpressionProfileAttributes') if attributes_to_delete is not None: # `PersonalStatus` and `DisplayName` is the only known attribute that has the ability to be deleted delete_psm = find_element(attributes_to_delete, 'PersonalStatus') or False assert isinstance(delete_psm, bool) delete_name = find_element(attributes_to_delete, 'DisplayName') or False assert isinstance(delete_name, bool) name = find_element(action, 'DisplayName') message = find_element(action, 'PersonalStatus') if name: backend.user_service.save_single_roaming(user, { 'name': name }) if message and not delete_psm: backend.user_service.save_single_roaming(user, { 'message': message }) if delete_psm: backend.user_service.save_single_roaming(user, { 'message': '' }) if delete_name: backend.user_service.save_single_roaming(user, { 'name': '' }) return render(req, 'msn:storageservice/UpdateProfileResponse.xml', { 'storage_ns': storage_ns, 'cachekey': cachekey, 'cid': cid, 'pptoken1': token, 'user': user }) if action_str == 'DeleteRelationships': # TODO: DeleteRelationships return render(req, 'msn:storageservice/DeleteRelationshipsResponse.xml', { 'storage_ns': storage_ns, 'cachekey': cachekey, 'cid': cid, 'pptoken1': token, 'user': user, }) if action_str in ('CreateDocument', 'UpdateDocument'): op = 'Update' if action_str == 'UpdateDocument' else 'Create' return handle_document(req, action, op, storage_ns, user, cid, token, backend) if action_str == 'CreateRelationships': # TODO: CreateRelationships return render(req, 'msn:storageservice/CreateRelationshipsResponse.xml', { 'storage_ns': storage_ns, 'cachekey': cachekey, 'cid': cid, 'pptoken1': token, 'user': user, }) if action_str in { 'ShareItem' }: # TODO: ShareItem return unknown_soap(req, header, action, expected = True) return unknown_soap(req, header, action) def handle_document( req: web.Request, action: Any, op_type: str, # 'Create' or 'Update' storage_ns: str, user: models.User, cid: str, token: str, backend: Backend, ) -> web.Response: streamtype = find_element(action, 'DocumentStreamType') if streamtype == 'UserTileStatic': raw_b64 = find_element(action, 'Data') if raw_b64: try: data = base64.b64decode(raw_b64) except Exception: return web.HTTPInternalServerError(text='') err = _store_avatar_from_bytes(backend, user, data) if err is not None: return err return render(req, 'msn:storageservice/{}DocumentResponse.xml'.format(op_type), { 'storage_ns': storage_ns, 'cid': cid, 'pptoken1': token, 'user': user, }) def _store_avatar_from_bytes( backend: Backend, user: models.User, data: bytes, ) -> Optional[web.Response]: ext = get_img_type(data) if ext is None: return web.HTTPInternalServerError(text='AZUL: Unsupported image format') try: backend.user_service.save_avatar(user, data, ext) except Exception: return web.HTTPInternalServerError(text='') return None async def handle_sha1auth(req: web.Request) -> web.Response: post = await req.post() token_data = post.get('token') if token_data is None: return web.HTTPInternalServerError() token_fields = dict(parse_qsl(str(token_data))) if 'ru' not in token_fields: return web.HTTPInternalServerError() return web.HTTPFound(token_fields['ru']) async def handle_ppcrlcheck(req: web.Request) -> web.Response: response = '\r\n\t\r\n\t\r\n' return web.HTTPOk(body=response) async def handle_rsi(req: web.Request) -> web.Response: _, action, bs, token = await preprocess_soap_rsi(req) if token is None or bs is None: return render(req, 'msn:oim/Fault.validation.xml', status = 500) action_str = get_tag_localname(action) user = bs.user backend = req.app['backend'] if action_str == 'GetMetadata': return render(req, 'msn:oim/GetMetadataResponse.xml', { 'md': gen_mail_data(user, backend, on_ns = False, e_node = False), }) if action_str == 'GetMessage': oim_uuid = find_element(action, 'messageId') oim_markAsRead = find_element(action, 'alsoMarkAsRead') oim = backend.user_service.get_oim_single(user, oim_uuid, mark_read = oim_markAsRead is True) return render(req, 'msn:oim/GetMessageResponse.xml', { 'oim_data': format_oim(oim), }) if action_str == 'DeleteMessages': messageIds = action.findall('.//{*}messageIds/{*}messageId') if not messageIds: return render(req, 'msn:oim/Fault.validation.xml', status = 500) for messageId in messageIds: if backend.user_service.get_oim_single(user, str(messageId)) is None: return render(req, 'msn:oim/Fault.validation.xml', status = 500) for messageId in messageIds: backend.user_service.delete_oim(user.uuid, str(messageId)) bs.evt.msn_on_oim_deletion(len(messageIds)) return render(req, 'msn:oim/DeleteMessagesResponse.xml') return render(req, 'msn:Fault.doesnotexist.xml', { 'action': action_str }) async def handle_oim(req: web.Request) -> web.Response: header, _, body_content, bs, _ = await preprocess_soap_oimws(req) soapaction = (req.headers.get('SOAPAction') or '') if soapaction.startswith('"') and soapaction.endswith('"'): soapaction = soapaction[1:-1] owsns = ( 'http://messenger.msn.com/ws/2004/09/oim/' if soapaction.startswith('http://messenger.msn.com/ws/2004/09/oim/') else 'http://messenger.live.com/ws/2006/09/oim/' ) lockkey_result = header.find('.//{*}Ticket').get('lockkey') if bs is None or lockkey_result in (None,''): return render(req, 'msn:oim/Fault.authfailed.xml', { 'owsns': owsns, }, status = 500) backend: Backend = req.app['backend'] user = bs.user detail = user.detail assert detail is not None friendlyname = None friendlyname_str = None friendly_charset = None friendlyname_mime = header.find('.//{*}From').get('friendlyName') email = header.find('.//{*}From').get('memberName') recipient = header.find('.//{*}To').get('memberName') recipient_uuid = backend.util_get_uuid_from_email(recipient) user_display_email = backend.util_get_display_email(user) alias_email = '{}@crosstalk.im'.format(user.username).lower() if email.lower() not in (user.email.lower(), alias_email, user_display_email.lower()) or recipient_uuid is None or not _is_on_al(recipient_uuid, backend, user, detail): return render(req, 'msn:oim/Fault.unavailable.xml', { 'owsns': owsns, }, status = 500) assert req.transport is not None peername = req.transport.get_extra_info('peername') if peername: host = peername[0] else: host = '127.0.0.1' oim_msg_seq = str(find_element(header, 'Sequence/MessageNumber')) if not oim_msg_seq.isnumeric(): return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if friendlyname_mime is not None: try: friendlyname, friendly_charset = decode_header(friendlyname_mime)[0] except: return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if friendly_charset is None: friendly_charset = 'utf-8' if friendlyname is not None: friendlyname_str = friendlyname.decode(friendly_charset) oim_proxy_string = header.find('.//{*}From').get('proxy') try: oim_mime = Parser().parsestr(body_content) except: return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) oim_run_id = str(oim_mime.get('X-OIM-Run-Id')) if oim_run_id is None: return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if not re.match(r'^\{?[A-Fa-f0-9]{8,8}-([A-Fa-f0-9]{4,4}-){3,3}[A-Fa-f0-9]{12,12}\}?', oim_run_id): return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) oim_run_id = oim_run_id.replace('{', '').replace('}', '') if ( 'X-Message-Info', 'Received', 'From', 'To', 'Subject', 'X-OIM-originatingSource', 'X-OIMProxy', 'Message-ID', 'X-OriginalArrivalTime', 'Date', 'Return-Path' ) in oim_mime.keys(): return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if str(oim_mime.get('MIME-Version')) != '1.0': return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if not str(oim_mime.get('Content-Type')).startswith('text/plain'): return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if str(oim_mime.get('Content-Transfer-Encoding')) != 'base64': return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) if str(oim_mime.get('X-OIM-Message-Type')) != 'OfflineMessage': return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) oim_seq_num = str(oim_mime.get('X-OIM-Sequence-Num')) if oim_seq_num != oim_msg_seq: return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) oim_headers = {name: str(value) for name, value in oim_mime.items()} try: i = body_content.index('\n\n') + 2 oim_body = body_content[i:] for oim_b64_line in oim_body.split('\n'): if len(oim_b64_line) > 77: return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) oim_body_normal = oim_body.strip() oim_body_normal = base64.b64decode(oim_body_normal).decode('utf-8') backend.user_service.save_oim( bs, recipient_uuid, oim_run_id, host, oim_body_normal, True, from_friendly = friendlyname_str, from_friendly_charset = friendly_charset, headers = oim_headers, oim_proxy = oim_proxy_string, ) except: return render(req, 'msn:oim/Fault.invalidcontent.xml', { 'owsns': owsns, }, status = 500) return render(req, 'msn:oim/StoreResponse.xml', { 'seq': oim_msg_seq, 'owsns': owsns, }) def _is_on_al(uuid: str, backend: Backend, user: models.User, detail: models.UserDetail) -> bool: contact = detail.contacts.get(uuid) if user.settings.get('BLP', 'AL') == 'AL' and (contact is None or not contact.lists & models.ContactList.BL): return True if user.settings.get('BLP', 'AL') == 'BL' and contact is not None and not contact.lists & models.ContactList.BL: return True if contact is not None: ctc_detail = backend._load_detail(contact.head) assert ctc_detail is not None ctc_me = ctc_detail.contacts.get(user.uuid) if ctc_me is None and contact.head.settings.get('BLP', 'AL') == 'AL': return True if ctc_me is not None and not ctc_me.lists & models.ContactList.BL: return True return False async def preprocess_soap_rsi(req: web.Request) -> Tuple[Any, Any, Optional[BackendSession], str]: from lxml.objectify import fromstring as parse_xml body = await req.read() root = parse_xml(body) token_tag = root.find('.//{*}PassportCookie/{*}*[1]') if get_tag_localname(token_tag) != 't': token = None token = token_tag.text if token is not None: token = token[0:20] backend: Backend = req.app['backend'] bs = backend.util_get_sess_by_token(token) header = find_element(root, 'Header') action = find_element(root, 'Body/*[1]') #if settings.DEBUG: print('Action: {}'.format(get_tag_localname(action))) return header, action, bs, token async def preprocess_soap_oimws(req: web.Request) -> Tuple[Any, str, str, Optional[BackendSession], str]: from lxml.objectify import fromstring as parse_xml body = await req.read() root = parse_xml(body) token = root.find('.//{*}Ticket').get('passport') if token[0:2] == 't=': token = token[2:22] backend: Backend = req.app['backend'] bs = backend.util_get_sess_by_token(token) header = find_element(root, 'Header') body_msgtype = str(find_element(root, 'Body/MessageType')) body_content = str(find_element(root, 'Body/Content')).replace('\r\n', '\n') return header, body_msgtype, body_content, bs, token # TODO: Make this actually accurate async def handle_whatsnew(req: web.Request) -> web.Response: whatsnew = '' with open('config/whatsnew.json', 'rb') as f: whatsnew_list = json.loads(f.read()) f.close() if len(whatsnew_list) > 0: whatsnew_dict = whatsnew_list[0] with open(TMPL_DIR + '/whatsnew/WhatsNewResponse.xml') as fh: whatsnew = fh.read() whatsnew = whatsnew.format( date=whatsnew_dict['date'], url=whatsnew_dict['url'], title=whatsnew_dict['title'], appName=whatsnew_dict['appName'], content=whatsnew_dict['content'] ) return web.HTTPOk(content_type = 'text/xml', text = whatsnew) async def handle_today(req: web.Request) -> web.Response: return render(req, 'msn:svcs/MSNToday.html', { 'title': "CrossTalk Today", 'msn': req.query.get('msn') or False, 'wlm': req.query.get('wlm') or False, 'windowslive': req.query.get('windowslive') or False }) async def handle_gamebrowser(req: web.Request) -> web.Response: return render(req, 'msn:svcs/GameBrowser.html', { 'title': "Game Browser | CrossTalk", }) async def handle_portal(req: web.Request) -> web.Response: return web.HTTPFound(f'http://{settings.TODAY_HOSTs}/start/msn') async def handle_textadredir(req: web.Request) -> web.Response: return web.HTTPFound(f'http://{settings.AD_HOST}/ads/txt') async def handle_banneradredir(req: web.Request) -> web.Response: return web.HTTPFound(f'http://{settings.AD_HOST}/ads/banner') async def handle_msn_redirect(req: web.Request) -> web.Response: i = req.match_info['i'] id = req.match_info['id'] if i == '5': if id == '60': return web.HTTPFound('/svcs/mms/tabs.asp') elif id == '153': return web.HTTPFound('/start') return web.HTTPFound('http://g.msn.com{}'.format(req.path_qs)) async def handle_msn5ads(req: web.Request) -> web.Response: response = 'False10080' return web.HTTPOk(body=response) async def handle_suiteupdate(req: web.Request) -> web.Response: return render(req, 'msn:config/SuiteUpdate.xml') async def handle_socialint(req: web.Request) -> web.Response: return render(req, 'msn:config/Config.PSA.xml') async def handle_tabs(req: web.Request) -> web.Response: with open('config/tabs.json') as fh: tabs_data = json.load(fh) tmpl = req.app['jinja_env'].get_template('msn:svcs/svcs_tabs.xml') tab_tmpl = tmpl.render(tabs=tabs_data) return web.HTTPOk(content_type='text/xml', text='\n' + tab_tmpl) # TODO: Unify the tabs handlers so spaghetti code isn't a problem async def handle_tabs_newer(req: web.Request) -> web.Response: id = int(req.match_info['id']) with open('config/tabs.json') as fh: tabs_data = json.load(fh) tab_data = next((tab for tab in tabs_data if tab["id"] == id), None) if not tab_data: return web.HTTPNotFound(text="Tab not found") tmpl = req.app['jinja_env'].get_template('msn:svcs/svcs_tabs_newer.xml') tab_tmpl = tmpl.render(tab=tab_data) return web.HTTPOk(content_type='text/xml', text=tab_tmpl) async def handle_msgrconfig(req: web.Request) -> web.Response: if req.method == 'POST': body = await req.read() # type: Optional[bytes] else: body = None msgr_config = _get_msgr_config(req, body) if msgr_config == 'INVALID_VER': return web.Response(status = 500) return web.HTTPOk(content_type = 'text/xml', text = msgr_config) async def handle_msgrconfig16(req: web.Request) -> web.Response: body = await req.read() msgr_config = _get_msgr_config_16(req, body) return web.HTTPOk(content_type = 'text/xml', text = msgr_config) def _get_msgr_config(req: web.Request, body: Optional[bytes]) -> str: query = req.query result = None # type: Optional[str] ver = query.get('ver') or '' if ver: if re.match(r'[^\d\.]', ver): return 'INVALID_VER' config_ver = ver.split('.', 4) if 8 <= int(config_ver[0]) <= 9: with open(TMPL_DIR + '/config/MsgrConfig.wlm.8.xml') as fh: config = fh.read() with open('config/tabs.json') as fh: with open(TMPL_DIR + '/svcs/svcs_tabs.xml') as th: template = th.read() tabs_data = json.load(fh) tmpl_before = req.app['jinja_env'].from_string(template) tab_tmpl = tmpl_before.render(tabs=tabs_data, settings=settings) result = config.format(tabs = tab_tmpl) elif int(config_ver[0]) >= 14: with open(TMPL_DIR + '/config/MsgrConfig.wlm.14.xml') as fh: template = fh.read() with open('config/tabs.json') as fh: tabs_data = json.load(fh) tmpl_before = req.app['jinja_env'].from_string(template) tmpl = tmpl_before.render(tabs=tabs_data, settings=settings) result = tmpl elif body is not None: with open(TMPL_DIR + '/config/MsgrConfig.msn.envelope.xml') as fh: envelope = fh.read() with open(TMPL_DIR + '/config/MsgrConfig.msn.xml') as fh: config = fh.read() with open('config/tabs.json') as fh: with open(TMPL_DIR + '/svcs/svcs_tabs.xml') as th: template = th.read() tabs_data = json.load(fh) tmpl_before = req.app['jinja_env'].from_string(template) tab_tmpl = tmpl_before.render(tabs=tabs_data) result = envelope.format(MsgrConfig = config.format(tabs = tab_tmpl, settings = settings)) return result or '' def _get_msgr_config_16(req: web.Request, body: Optional[bytes]) -> str: with open(TMPL_DIR + '/config/MsgrConfig.wlm.16.xml') as fh: template = fh.read() with open('config/tabs.json') as fh: tabs_data = json.load(fh) tmpl_before = req.app['jinja_env'].from_string(template) tmpl = tmpl_before.render(tabs=tabs_data, settings=settings) result = tmpl return result or '' PassportURLs = 'PassportURLs' async def handle_nexus(req: web.Request) -> web.Response: return web.HTTPOk(headers = { PassportURLs: 'DALogin=https://{}{}'.format(settings.LOGIN_HOST, LOGIN_PATH), }) async def handle_login(req: web.Request) -> web.Response: tmp = _extract_pp_credentials(req.headers.get('Authorization') or '') if tmp is None: token = None else: email, pwd = tmp token_tpl = _login(req, email, pwd) if token_tpl is None: raise web.HTTPUnauthorized(headers={ 'WWW-Authenticate': '{}da-status=failed'.format(PP), }) token, _, _ = token_tpl return web.HTTPOk(headers={ 'Authentication-Info': '{}da-status=success,from-PP=\'{}\''.format(PP, token), }) async def handle_not_rst(req: web.Request) -> web.Response: if req.method == 'OPTIONS': return web.HTTPOk(headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'X-User, X-Password, Content-Type', 'Access-Control-Expose-Headers': 'X-Token', 'Access-Control-Max-Age': str(86400), }) email = req.headers.get('X-User') or '' pwd = req.headers.get('X-Password') or '' token_tpl = _login(req, email, pwd, lifetime = 86400) headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Expose-Headers': 'X-Token', } if token_tpl is not None: token, _, _ = token_tpl headers['X-Token'] = token return web.HTTPOk(headers = headers) async def handle_rst(req: web.Request, rst2: bool = False) -> web.Response: from lxml.objectify import fromstring as parse_xml body = await req.read() try: root = parse_xml(body) except: return render(req, 'msn:RST/{}.error.xml'.format('RST2' if rst2 else 'RST')) email = find_element(root, 'Username') pwd = str(find_element(root, 'Password')) if email is None or pwd is None: return render(req, 'msn:RST/{}.error.xml'.format('RST2' if rst2 else 'RST')) backend: Backend = req.app['backend'] token_tpl = _login(req, email, pwd, binary_secret = True, lifetime = 86400) uuid = backend.util_get_uuid_from_email(email) if token_tpl is not None and uuid is not None: token, expiry, bsecret = token_tpl day_before_expiry = expiry - timedelta(days = 1) timez = util.misc.date_format(day_before_expiry) tomorrowz = util.misc.date_format(expiry) time_5mz = util.misc.date_format((day_before_expiry + timedelta(minutes = 5))) # load PUID and CID cid = cid_format(uuid) puid = puid_format(uuid) assert req.transport is not None peername = req.transport.get_extra_info('peername') if peername: host = peername[0] else: host = '127.0.0.1' # get list of requested domains domains = root.findall('.//{*}Address') tmpl = req.app['jinja_env'].get_template('msn:RST/{}.token.xml'.format('RST2' if rst2 else 'RST')) # collect tokens for requested domains, ignore Passport token request tokenxmls = [tmpl.render( i = i + 1, domain = domain, timez = timez, tomorrowz = tomorrowz, pptoken1 = token, binarysecret = bsecret, ) for i, domain in enumerate(domains) if domain != 'http://Passport.NET/tb'] tmpl = req.app['jinja_env'].get_template('msn:RST/{}.xml'.format('RST2' if rst2 else 'RST')) return web.HTTPOk( content_type = 'text/xml', text = (tmpl.render( puidhex = puid, time_5mz = time_5mz, timez = timez, tomorrowz = tomorrowz, cid = cid, email = email, firstname = "John", lastname = "Doe", ip = req.headers.get('X-Forwarded-For', req.remote), pptoken1 = token, tokenxml = Markup(''.join(tokenxmls)), ) if rst2 else tmpl.render( puidhex = puid, timez = timez, tomorrowz = tomorrowz, cid = cid, email = email, firstname = "John", lastname = "Doe", ip = req.headers.get('X-Forwarded-For', req.remote), pptoken1 = token, tokenxml = Markup(''.join(tokenxmls)), )), ) return render(req, 'msn:RST/{}.authfailed.xml'.format('RST2' if rst2 else 'RST'), { 'timez': util.misc.date_format(datetime.utcnow()), }) async def handle_videofeed(req: web.Request, get_videos: bool = False) -> web.Response: if not get_videos: return render(req, 'msn:svcs/videofeedroot.xml') else: with open('config/videos.json', 'rb') as f: vid_data = json.load(f) rss_items = [] for video in vid_data: try: pub_date = parsedate_to_datetime(video['publish_date']) except Exception: pub_date = datetime.now() item = PyRSS2Gen.RSSItem( title=video['title'], link=video['link'], description=video['description'], guid=PyRSS2Gen.Guid(video['link'], isPermaLink=True), pubDate=pub_date, enclosure=PyRSS2Gen.Enclosure( url=video['thumbnail'], length="1", type="image/jpeg" ) ) rss_items.append(item) rss_feed = PyRSS2Gen.RSS2( title="CrossTalk", link="http://crosstalk.im", description="CrossTalk Videos", language="en-us", lastBuildDate=datetime.now(), ttl=10, copyright="© 2023 - 2025 the undergr0und", image=PyRSS2Gen.Image( url="http://hiden.cc/static/icon/misc/hidnet.png", title="CrossTalk Video - InDev", link="http://crosstalk.im" ), items=rss_items ) rss_xml = rss_feed.to_xml(encoding="utf-8") return web.HTTPOk(content_type="text/xml", text=rss_xml) def _extract_pp_credentials(auth_str: str) -> Optional[Tuple[str, str]]: if not auth_str: return None assert auth_str.startswith(PP) auth = {} for part in auth_str[len(PP):].split(','): parts = part.split('=', 1) if len(parts) == 2: auth[unquote(parts[0])] = unquote(parts[1]) email = auth['sign-in'] pwd = auth['pwd'] return email, pwd def _login(req: web.Request, email: str, pwd: str, binary_secret: bool = False, lifetime: int = 30) -> Optional[Tuple[str, datetime, Optional[str]]]: backend: Backend = req.app['backend'] uuid = backend.user_service.login(email, pwd) if uuid is None and email.lower().endswith('@crosstalk.im'): username = email.lower()[:-len('@crosstalk.im')] uuid = backend.user_service.login_with_username(username, pwd) if uuid is None: return None bsecret = None if binary_secret: bsecret = base64.b64encode(secrets.token_bytes(24)).decode('ascii') return (*backend.login_auth_service.create_token('ns/login', [uuid, bsecret], lifetime = lifetime), bsecret) def _contact_is_favorite(user_detail: models.UserDetail, ctc: models.Contact) -> bool: groups = user_detail._groups_by_uuid for group in ctc._groups.copy(): if group.id not in groups: continue if groups[group.id].is_favorite: return True return False