#################################################################################
# This file is part of Mana Launcher.
# Copyright (C) 2021 Jesusalva <jesusalva@tmw2.org>
#
# Distributed under the MIT license, except for Steam parts.
#################################################################################
init python:
def status_update(smsg="", pc=-1):
global progress, statusmsg
if pc > 0 and pc != progress:
progress=min(pc, 100)
if (smsg != ""):
statusmsg=smsg
stdout(smsg)
def validate_server(srv):
name="Unknown Server"; ok=False
try:
name=srv["Name"]
stdout("Validating server \"%s\"..." % name)
print("Host: %s" % srv["Host"])
print("Port: %04d" % srv["Port"])
print("Desc: %s" % srv["Desc"])
print("Link: %s" % srv["Link"])
print("News: %s" % srv["News"])
print("Back: %s" % srv["Back"])
print("UUID: %s" % srv["UUID"])
print("Help: %s" % srv["Help"])
print("Online List: %s" % srv["Online"])
print("Policy: %s" % srv["Policy"])
if not "Type" in srv.keys():
srv["Type"]="evol2"
stdout("Server \"%s\" is valid! Downloading assets..." % name)
ok=True
except:
traceback.print_exc()
stdout("Validation for server \"%s\" FAILED!" % name)
## Fetch server background (optional) if successful
if (ok):
srv["onlcnt"] = -1
srv["onlist"] = []
"""
bgname="bg_%s.png" % (name.replace(" ", ""))
if os.path.exists(get_path(bgname)):
return ok
try:
stdout("Fetching background for server \"%s\"" % bgname)
r=requests.get(srv["Back"], timeout=10.0)
with open(bgname, 'wb') as fd:
for chunk in r.iter_content(chunk_size=128):
fd.write(chunk)
except:
traceback.print_exc()
stdout("Background error for server \"%s\"." % name)
"""
return ok
def update_serverlist(host):
try:
r=None
## Attempt for a localized version first
if _preferences.language is not None:
r=requests.get("%s/server_list.%s.json" % (host, _preferences.language), timeout=10.0)
if (r.status_code == 404):
stdout("No server list for language: %s" % _preferences.language)
r=None
## Obtain regular version if localized version failed/skipped
if r is None:
r=requests.get("%s/server_list.json" % host, timeout=10.0)
if (r.status_code != 200):
raise AssertionError("Mirror %s seems to be down!\nReturned error %03d\n" % (host.replace("https://", "").replace("http://", ""), r.status_code))
j=json.loads(r.text)
## If we reached here, then everything is likely fine
## We can now build the persistent data
status_update(_("Fetching server list and assets..."), 20)
persistent.serverlist = []
slist=len(j)
for server in j:
if (validate_server(server)):
persistent.serverlist.append(server)
status_update(pc=int(10+(70.0/slist)))
## If we were truly successful, save this host as our mirror
persistent.host = host
return True
except:
traceback.print_exc()
return False
#############################################################################
def CONFIGURE_LAUNCHER():
global progress, statusmsg, responsive, has_steam, vaultId, vaultToken
statusmsg="Loading user configuration..."
#########################################################################
## Strictly for debugging purposes, skip everything
if USE_DUMMY_DATA:
if persistent.host is None:
persistent.host=HOST_LIST[0]
if persistent.serverlist is None or persistent.serverlist == []:
persistent.serverlist=[{"Name": "Moubootaur Legends", "Host": "server.tmw2.org", "Port": 6901, "Desc": "Ready to become a Moubootaur Legend now?\n\nIn Moubootaur Legends universe, a being known as the Monster King threatens humanity, sending hordes of monsters and causing havoc for purposes unknown for anyone. And yet, in the shadows, a greater threat looms over the horizon, waiting for the time of its ressurection...\nTake arms, for heroes are not born but forged with sweat and effort, and become a Moubootaur Legend now.", "Link": "https://moubootaurlegends.org/", "News": "https://updates.tmw2.org/news.txt", "Back": "tmw2", "UUID": "5936870052ae411c8f0271907f8cf2e4", "Help": "https://discord.gg/J4gcaqM", "Online": "https://tmw2.org/online.json", "Policy": "https://tmw2.org/legal"}]
vaultId = 99; vaultToken = "token"; progress = 100
return
#########################################################################
# If persistent data is not yet set, it must be created
# This block is 1~60%
# But first we check for updates
if persistent.host is not None:
stdout("Fetching data from %s" % persistent.host, True)
retry=3
while retry > 0:
try:
retry-=1
rv=requests.get("%s/version.txt" % persistent.host, timeout=10.0)
if (rv.status_code != 200):
raise AssertionError("Mirror %s seems to be down!\nReturned error %03d" % (persistent.host.replace("https://", "").replace("http://", ""), rv.status_code))
# Everything went fine! Update server list if needed
if (persistent.version != rv.text):
if not update_serverlist(persistent.host):
raise Exception("Mirror serverlist error")
persistent.version=rv.text
stdout("Version updated to %s" % persistent.version, True)
retry=0
except:
traceback.print_exc()
if retry < 1:
persistent.host=None
stdout("Too many failures, seeking a new mirror.")
## Either our original mirror died, or it was never a thing
## Maybe you're offline but in any case:
## Seek a new mirror and update server list accordingly.
if persistent.host is None:
status_update(_("Welcome to the Mana Launcher.\nBuilding user data, please wait..."), 1)
time.sleep(1.0)
# Determine from where we should fetch updates
for host in HOST_LIST:
try:
if not update_serverlist(host):
raise Exception("Mirror serverlist error")
break
except:
stdout("An error happened on mirror \"%s\"." % host.split("//")[1])
stdout("\nTrying next mirror...\n")
## If we don't have a host - DIE HARD
if (persistent.host is None):
stdout("No mirror could be found.", True)
status_update(_("{color=#f00}{b}ERROR: No active mirror found.\nMana Launcher needs to be updated.{/b}{/color}"))
responsive=False
return
## Everything was successful!
#########################################################################
# Check for clients, this block is 60~80%
status_update(_("Checking for installed clients..."), 60)
r=handle_client()
## Oh sh--
if not r:
time.sleep(0.4)
responsive = False
time.sleep(2.6)
status_update(_("{color=#F00}ERROR: \"%s\" client is NOT supported!{/color}" % persistent.evol2cli), 70)
return
status_update(pc=70)
#time.sleep(0.1)
#########################################################################
# Before starting, we must check for Vault or Steam credentials
# This block is 1~20%
status_update(_("Verifying credentials..."), 80)
try:
if not persistent.steam:
raise Exception("Steam login was disabled!")
stdout("Steam Status: %s" % str(steam.initialized))
if steam.init() != True:
raise Exception("Steam is not running!")
status_update(_("Attempting Steam authentication..."), 81)
accId = steam.get_account_id()
stdout("Steam login active, user %d" % accId)
## Retrieve new ticket ("token"), send it in Base64 to the API
token = steam.get_session_ticket()
auth = {"accId": accId, "token": base64.b64encode(token)}
time.sleep(0.75)
status_update(_("Waiting for Vault reply..."), 85)
## Request the Vault for the ticket validation
r = vault.post(VAULT_HOST+"/steam_auth", json=auth, timeout=15.0)
## Ticket error, we die here.
if (r.status_code == 406):
status_update(_("{color=#f00}{b}STEAM AUTHENTICATION ERROR.\nSteam refused authentication. Try restarting the app.{/b}{/color}"))
responsive=False
return
## Intercept rate-limiting (429) and retry once
if (r.status_code == 429 or r.status_code == 500):
status_update(_("Rate limited! Trying again 15s..."), 85)
time.sleep(5.0)
status_update(_("Rate limited! Trying again 10s..."), 85)
time.sleep(5.0)
status_update(_("Rate limited! Trying again 5s..."), 85)
time.sleep(5.0)
status_update(_("Rate limited! Trying again..."), 85)
r = vault.post(VAULT_HOST+"/steam_auth", json=auth, timeout=15.0)
## If still unsucessful, give up on Steam Auth
if (r.status_code != 200):
raise Exception("Vault returned code %d" % r.status_code)
## Receive the Vault Token
status_update(_("Waiting for Vault reply..."), 90)
stdout("Steam result: (%d) %s" % (r.status_code, ifte(config.developer, r.text, accId)))
auth2 = r.json()
vaultId = auth2["vaultId"]
vaultToken = auth2["token"]
# If everything went well, inform Steam support is ON
# Enable all Steam features and skip Vault-only auth
has_steam = True
stdout("Steam session initialized successfully", True)
status_update(_("Steam session initialized successfully"), 99)
renpy.notify("Welcome, %s" % steam.get_persona_name())
time.sleep(0.25)
except:
# NO FALLBACK: if Steam Login is on, do not try vault (no multiacc)
if persistent.steam:
traceback.print_exc()
status_update(_("{color=#f00}{b}STEAM AUTHENTICATION ERROR.\nIf the issue persists, try closing the app and opening again.{/b}{/color}"))
responsive=False
return
# Prepare to do Vault authentication
status_update(_("Steam auth disabled, logging on Vault..."), 80)
vaultId = 0
vaultToken = "MANUAL"
#########################################
####### TODO FIXME
# Must return and let a prompt for username & password
# (Or Email, in the case of the modern Vault)
# If vaultId is zero
#status_update("{color=#F00}VaultError: Not yet implemented{/color}")
#responsive = False
#return
####### TODO FIXME
#########################################
status_update(pc=100)
return
def ONLINE_LISTING():
global running, responsive
while (running and responsive):
for s in persistent.serverlist:
try:
r=requests.get("%s" % s["Online"], timeout=5.0)
if (r.status_code != 200):
raise AssertionError("Online list for %s is down!\nReturned error %03d\n" % (s["Name"], r.status_code))
j=json.loads(r.text)
s["onlcnt"]=len(j)
s["onlist"]=list(j)
except:
s["onlcnt"]=-1
s["onlist"]=[]
pass
time.sleep(60)
return
def onl_cnt(ent):
if ent["onlcnt"] < 0:
return "?"
else:
return "%d" % ent["onlcnt"]
def onl_list(ent):
s=""; i=0; l=len(ent["onlist"])
while i < l:
s+=str(ent["onlist"][i])
i+=1
if (i < l):
s+=", "
return s
#################################################################################
screen register_method():
## Ensure other screens do not get input while this screen is displayed.
modal True
zorder 200
style_prefix "confirm"
add "gui/overlay/confirm.png"
frame:
vbox:
xalign .5
yalign .5
spacing 30
label _("{b}Vault Authentication{/b}\n\nWhich method do you want to use to login on TMW Vault?\n{size=14}An account will be automatically created if it does not exists already. This implies accepting the {a=%s}Terms of Service.{/a}{/size}" % ("https://tmw2.org/legal")):
style "confirm_prompt"
xalign 0.5
hbox:
xalign 0.5
spacing 100
#textbutton _("Email Auth") action None#Return(1)
#textbutton _("Pass+2FA Auth") action Return(2)
textbutton _("Standard") action Return(1)
textbutton _("Mouseless") action Return(2)
screen register_input(prompt, mask=""):
## Ensure other screens do not get input while this screen is displayed.
modal True
zorder 200
style_prefix "confirm"
add "gui/overlay/confirm.png"
frame:
vbox:
xalign .5
yalign .5
spacing 30
label _(prompt):
style "confirm_prompt"
xalign 0.5
null height 24
input:
xalign 0.5
id "input"
copypaste True
if mask != "":
mask mask
screen notice(prompt):
## Ensure other screens do not get input while this screen is displayed.
modal True
zorder 200
style_prefix "confirm"
add "gui/overlay/confirm.png"
frame:
vbox:
xalign .5
yalign .5
spacing 30
label _(prompt):
style "confirm_prompt"
xalign 0.5
hbox:
xalign 0.5
spacing 100
textbutton _("OK") action Return()
label register:
$ status_update(" ", 80)
# Automatic login
if persistent.autologin and persistent.email and persistent.passd and persistent.totp:
python:
print("Automatic login ON")
key = base64.b32decode(persistent.totp.encode('utf-8'), True)
msg = struct.pack(">Q", int(time.time()/30))
h = hmac.new(key, msg, hashlib.sha1).digest()
if LEGACY:
o = ord(h[19]) & 15
else:
o = (h[19] & 15)
_return = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
_return = "%06d" % _return
print("TOTP: %s" % _return)
del key, msg, h, o
if LEGACY:
password = str(bytearray((x ^ int(persistent.rhash/mp.sub) for x in bytearray(persistent.passd, 'utf-8'))))
else:
password = bytearray((x ^ int(persistent.rhash/mp.sub) for x in persistent.passd)).decode('utf-8')
code2FA = _return
data = {"mail": persistent.email,
"pass": password,
"totp": code2FA[:6]
}
r = vault.post(VAULT_HOST+"/user_auth", json=data)
del password, data
# Python end
if (r.status_code != 200):
call screen notice(_("Vault returned error %d\n\nAutomatic login failed." % r.status_code))
# Manual login
if persistent.vmethod is None:
call screen register_method
$ persistent.vmethod = _return
else:
$ _return = persistent.vmethod
$ status_update(pc=85)
$ method = int(_return)
if method == 1:
jump register_vault
$ email=""
while email == "":
call screen register_input(_("Please insert your {b}email{/b}."))
$ email=str(_return)
if not "@" in email or not "." in email:
call screen notice(_("Please make sure you enter a valid email!"))
$ email=""
$ status_update(pc=90)
##########################################
## What we'll do now depends on the method
## Email-Auth (the default)
if method == 1:
$ user = {"email": email,
"confirmed": True
}
$ r = vault.put(VAULT_HOST+"/vault/session", json=user)
# Wait for Vault to send you an email
if (r.status_code != 200):
call screen notice(_("Vault returned error %d\n\nPlease try again later." % r.status_code))
return
$ status_update(pc=92)
call screen notice(_("An email was sent with your API token.\n\nYou'll need it shortly, so check your email."))
$ status_update(pc=95)
call screen register_input(_("Please insert the {b}token{/b} you received on your email."))
$ token = _return
$ user["key"] = token
$ status_update(pc=97)
#$ r = vault.get(VAULT_HOST+"/vault/session?token=%s&email=%s" % (token, email)) # FIXME: HTTP formating for email
# FIXME: Continue from here
call screen notice(_("{b}INTERNAL SERVER ERROR{/b}\n\nSeems like someone messed up on the APIs!\nThis login method seems to be temporaly unavailable, please choose another one."))
# Cleanup
$ del user
$ del token
##########################################
## What we'll do now depends on the method
## 2FA-Auth
elif method == 2:
$ password = ""
while len(password) < 4:
call screen register_input(_("Please insert your {b}Password{/b}.\nIt has to be at least 4 characters long."), "*")
$ password = _return
# We must send the password on plain-text; That's why we use SSL
$ status_update(pc=92)
if persistent.totp is None:
call screen register_input(_("If you already have an account, please insert your {b}2FA code{/b}.\n\n{u}Otherwise, a new account will be created and details will be sent to your email.{/u}"))
else:
python:
key = base64.b32decode(persistent.totp.encode('utf-8'), True)
msg = struct.pack(">Q", int(time.time()/30))
h = hmac.new(key, msg, hashlib.sha1).digest()
if LEGACY:
o = ord(h[19]) & 15
else:
o = (h[19] & 15)
_return = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
_return = "%06d" % _return
print("TOTP: %s" % _return)
del key, msg, h, o
$ code2FA = _return
$ status_update(pc=95)
$ data = {"mail": email,
"pass": password,
"totp": code2FA[:6]
}
$ r = vault.post(VAULT_HOST+"/user_auth", json=data)
# Wait for Vault to confirm.
if (r.status_code != 200):
call screen notice(_("Vault returned error %d\n\nPlease try again later." % r.status_code))
return
# Check if we have success
python:
try:
status_update(pc=98)
stdout("Vault result: (%d) %s" % (r.status_code, ifte(config.developer, r.text, "OK")))
auth2 = r.json()
vaultId = auth2["vaultId"]
vaultToken = auth2["token"]
except:
traceback.print_exc()
stdout("Error - Vault result is bad.")
# Maybe we got a message informing this is a new account?
try:
if (auth2["status"] == 1):
status_update("Creating account and logging in...")
renpy.notify("Account created! Check email.")
time.sleep(1.0)
except:
pass
$ del data
$ del code2FA
############
## Cleanup
$ del method
$ del email
if vaultId:
$ status_update(_("Success!"), 100)
else:
$ status_update(_("{color=#F00}Failure!{/color}"), pc=100)
return
#################################################################################
label set2fa:
call screen register_input(_("Please insert your {b}2FA Secret{/b} {i}or the link{/i} sent to you by email.\n\n{size=18}{color=#f00}WARNING:{/color} Will be saved locally without cryptography. We advise using Google Authenticator or similar instead.{/size}"))
if _return != "":
python:
if _return.startswith("otpauth:"):
try:
tmp=_return.split("secret=")
_return=tmp[1].split("&")[0]
print("OTP Token: %s" % _return)
except:
renpy.call_screen("notice", _("Invalid OTPAuth URL.\nEnsure you used the URL sent to you by email!"))
$ persistent.totp = _return
else:
$ persistent.totp = None
return
label savevm:
if persistent.vmethod is not None:
$ persistent.vmethod = None
return
call screen register_method
$ persistent.vmethod = _return
return
label resetm:
$ persistent.email = None
return
label resetp:
$ persistent.rhash = None
$ persistent.passd = None
$ mp.sub = None
return