mirror of
https://git.ugnet.gay/CrossTalk/azul.git
synced 2026-05-27 22:59:49 +00:00
922 lines
31 KiB
Python
922 lines
31 KiB
Python
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 ..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) # adding an ending back-slash is required, or else it 404s. ????
|
|
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
|
|
|
|
storage_path = _get_storage_path(user.uuid)
|
|
files = None
|
|
if storage_path.exists() and storage_path.is_dir():
|
|
files = [x for x in storage_path.iterdir() if '_thumb' not in x.stem]
|
|
|
|
mime = None
|
|
image_size = 0
|
|
image_thumb_size = 0
|
|
|
|
if files:
|
|
ext = files[0].suffix
|
|
mime = ext[1:]
|
|
image_path = storage_path / '{}{}'.format(user.uuid, ext)
|
|
image_thumb_path = storage_path / '{}_thumb{}'.format(user.uuid, ext)
|
|
|
|
if image_path.exists():
|
|
image_size = image_path.stat().st_size
|
|
|
|
if image_thumb_path.exists():
|
|
image_thumb_size = image_thumb_path.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
|
|
# ```
|
|
# <UpdateProfile xmlns="http://www.msn.com/webservices/storage/w10">
|
|
# <profile>
|
|
# <ResourceID>862d987eb60b7a63!106</ResourceID>
|
|
# <ExpressionProfile>
|
|
# <FreeText>Update</FreeText>
|
|
# <DisplayName>Society is betrayal</DisplayName>
|
|
# <PersonalStatus>Prosperity is the best medicine. :)</PersonalStatus>
|
|
# </ExpressionProfile>
|
|
# </profile>
|
|
# </UpdateProfile>
|
|
# ```
|
|
|
|
# Remove roaming message
|
|
# ```
|
|
# <UpdateProfile xmlns="http://www.msn.com/webservices/storage/w10">
|
|
# <profile>
|
|
# <ResourceID>bb4542ce2eacdbde!106</ResourceID>
|
|
# <ExpressionProfile>
|
|
# <FreeText>Update</FreeText>
|
|
# <DisplayName>%walkingphas3r%</DisplayName>
|
|
# <Flags>0</Flags>
|
|
# </ExpressionProfile>
|
|
# </profile>
|
|
# <profileAttributesToDelete>
|
|
# <ExpressionProfileAttributes>
|
|
# <PersonalStatus>true</PersonalStatus>
|
|
# </ExpressionProfileAttributes>
|
|
# </profileAttributesToDelete>
|
|
# </UpdateProfile>
|
|
# ```
|
|
|
|
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'):
|
|
return handle_document(req, action, ('Update' if action_str == 'UpdateDocument' else 'Create'), storage_ns, user, cid, token)
|
|
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, type: str, storage_ns: str, user: models.User, cid: str, token: str) -> web.Response:
|
|
from PIL import Image
|
|
|
|
# get image data
|
|
#name = find_element(action, 'Name')
|
|
streamtype = find_element(action, 'DocumentStreamType')
|
|
|
|
if streamtype == 'UserTileStatic':
|
|
mime = find_element(action, 'MimeType')
|
|
#mime = None
|
|
data = find_element(action, 'Data')
|
|
data = base64.b64decode(data)
|
|
|
|
# WLM sends either `png` or `image/png` as the MIME type no matter what type of file is sent over. Guess image type from header
|
|
|
|
# TODO: BMPs
|
|
if data[:6] in (b'GIF87a',b'GIF89a'):
|
|
mime = 'gif'
|
|
elif data[:2] == b'\xff\xd8':
|
|
mime = 'jpeg'
|
|
elif data[:8] == b'\x89PNG\x0d\x0a\x1a\x0a':
|
|
mime = 'png'
|
|
|
|
if mime is not None:
|
|
try:
|
|
image = Image.open(BytesIO(data))
|
|
except:
|
|
return web.HTTPInternalServerError(text = '')
|
|
|
|
# store display picture as file
|
|
path = _get_storage_path(user.uuid)
|
|
path.mkdir(exist_ok = True, parents = True)
|
|
|
|
for old_file in path.glob(f'{user.uuid}.*'):
|
|
if old_file.is_file():
|
|
old_file.unlink()
|
|
for old_thumb in path.glob(f'{user.uuid}_thumb.*'):
|
|
if old_thumb.is_file():
|
|
old_thumb.unlink()
|
|
|
|
image_path = path / '{uuid}.{mime}'.format(uuid = user.uuid, mime = mime)
|
|
|
|
image_path.write_bytes(data)
|
|
|
|
thumb = image.resize((21, 21))
|
|
|
|
thumb_path = path / '{uuid}_thumb.png'.format(uuid = user.uuid)
|
|
thumb.save(str(thumb_path))
|
|
|
|
return render(req, 'msn:storageservice/{}DocumentResponse.xml'.format(type), {
|
|
'storage_ns': storage_ns,
|
|
'cid': cid,
|
|
'pptoken1': token,
|
|
'user': user,
|
|
})
|
|
|
|
async def handle_sha1auth(req: web.Request) -> web.Response:
|
|
# We have no use for any of the actual tokens sent here right now (this is primarily for WLM 8's MSN Today function),
|
|
# so just redirect to the URL specified by `ru`
|
|
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 = '<config>\r\n\t<deviceid minversion="16.000.26889.00"/>\r\n\t<mobilecfg minversion="16.000.26208.0"/>\r\n</config>'
|
|
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)
|
|
|
|
if email != user.email 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://today.msgrsvcs.ctsrv.gay/start/msn')
|
|
|
|
async def handle_textadredir(req: web.Request) -> web.Response:
|
|
return web.HTTPFound(f'http://ctsvcs.advertising.ugnet.gay/ads/txt')
|
|
|
|
async def handle_banneradredir(req: web.Request) -> web.Response:
|
|
return web.HTTPFound(f'http://ctsvcs.advertising.ugnet.gay/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 = '<?xml version="1.0"?><msn-data><RefreshLogin>False</RefreshLogin><RefreshInterval>10080</RefreshInterval></msn-data>'
|
|
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='<?xml version="1.0"?>\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()),
|
|
})
|
|
|
|
def _get_storage_path(uuid: str) -> Path:
|
|
return Path('storage/dp') / uuid[0:1] / uuid[0:2]
|
|
|
|
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: 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
|