################################################################################# # This file is part of Mana Launcher. # Copyright (C) 2021 Jesusalva # # 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"; vaultOTP=""; 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\n" % 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!") if steam is None: raise Exception("Steam Status was disabled!") #if steam.periodic is None: # raise Exception("Steam is crazy!") #assert config.periodic_callbacks.index(steam.periodic) is not None #if sys.modules["_renpysteam"] is None: # 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).decode()} 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"] vaultOTP = auth2["otp"] # 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" vaultOTP = "" ######################################### ####### 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 _("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") code2FA = calcOTP(base64.b32decode(persistent.totp.encode('utf-8'), True)) 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') 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): if (r.status_code not in [401, 403]): call screen notice(_("Vault returned error %d\n\nAutomatic login failed." % r.status_code)) else: call screen notice(_("Vault returned error %d (incorrect login/password)\n\nAutomatic login failed." % r.status_code)) else: $ stdout("Vault result: (%d) %s" % (r.status_code, ifte(config.developer, r.text, "OK"))) $ auth2 = r.json() $ vaultId = auth2["vaultId"] $ vaultToken = auth2["token"] $ vaultOTP = auth2["otp"] return # 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 ## 2FA-Auth if 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}")) $ code2FA = _return else: $ code2FA = calcOTP(base64.b32decode(persistent.totp.encode('utf-8'), True)) $ 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): if (r.status_code not in [401, 403]): call screen notice(_("Vault returned error %d\n\nPlease try again later." % r.status_code)) else: call screen notice(_("Vault returned error %d (incorrect login/password)\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"] vaultOTP = auth2["otp"] 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