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