From 487ab596da2195b14dc784d0cbeba398abd85d1e Mon Sep 17 00:00:00 2001 From: eli fessler Date: Wed, 26 Oct 2022 16:31:08 -1000 Subject: [PATCH] major refactor to address #62 & improve structure --- iksm.py | 148 ++++++++++++++++++++++++++++++++++++++++++------------- s3s.py | 26 +++++----- utils.py | 67 ++----------------------- 3 files changed, 130 insertions(+), 111 deletions(-) diff --git a/iksm.py b/iksm.py index 6acf73f..077c5db 100644 --- a/iksm.py +++ b/iksm.py @@ -6,35 +6,112 @@ import base64, hashlib, json, os, re, sys import requests from bs4 import BeautifulSoup -session = requests.Session() -S3S_VERSION = "unknown" -NSOAPP_VERSION = "2.3.1" +S3S_VERSION = "unknown" +NSOAPP_VERSION = "unknown" +NSOAPP_VER_FALLBACK = "2.3.1" +WEB_VIEW_VERSION = "unknown" +WEB_VIEW_VER_FALLBACK = "1.0.0-5644e7a2" # fallback for current splatnet 3 ver +SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net" # functions in this file & call stack: -# get_nsoapp_version() -# log_in() -> get_session_token() -# get_gtoken() -> call_imink_api() -> f -# get_bullet() -# enter_tokens() +# - get_nsoapp_version() +# - get_web_view_ver() +# - log_in() -> get_session_token() +# - get_gtoken() -> call_f_api() +# - get_bullet() +# - enter_tokens() -# place config.txt in same directory as script (bundled or not) -if getattr(sys, 'frozen', False): - app_path = os.path.dirname(sys.executable) -elif __file__: - app_path = os.path.dirname(__file__) -config_path = os.path.join(app_path, "config.txt") +session = requests.Session() def get_nsoapp_version(): - '''Fetches the current Nintendo Switch Online app version from the Apple App Store.''' + '''Fetches the current Nintendo Switch Online app version from the Apple App Store and sets it globally.''' - try: - page = requests.get("https://apps.apple.com/us/app/nintendo-switch-online/id1234806557") - soup = BeautifulSoup(page.text, 'html.parser') - elt = soup.find("p", {"class": "whats-new__latest__version"}) - ver = elt.get_text().replace("Version ", "").strip() - return ver - except: + global NSOAPP_VERSION + if NSOAPP_VERSION != "unknown": # already set return NSOAPP_VERSION + else: + try: + page = requests.get("https://apps.apple.com/us/app/nintendo-switch-online/id1234806557") + soup = BeautifulSoup(page.text, 'html.parser') + elt = soup.find("p", {"class": "whats-new__latest__version"}) + ver = elt.get_text().replace("Version ", "").strip() + + NSOAPP_VERSION = ver + + return NSOAPP_VERSION + except: # error with web request + return NSOAPP_VER_FALLBACK + + +def get_web_view_ver(bhead=[], gtoken=""): + '''Finds & parses the SplatNet 3 main.js file to fetch the current site version and sets it globally.''' + + global WEB_VIEW_VERSION + if WEB_VIEW_VERSION != "unknown": + return WEB_VIEW_VERSION + else: + app_head = { + 'Upgrade-Insecure-Requests': '1', + 'Accept': '*/*', + 'DNT': '1', + 'X-AppColorScheme': 'DARK', + 'X-Requested-With': 'com.nintendo.znca', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document' + } + app_cookies = { + '_dnt': '1' # Do Not Track + } + + if bhead: + app_head["User-Agent"] = bhead.get("User-Agent") + app_head["Accept-Encoding"] = bhead.get("Accept-Encoding") + app_head["Accept-Language"] = bhead.get("Accept-Language") + if gtoken: + app_cookies["_gtoken"] = gtoken # X-GameWebToken + + home = requests.get(SPLATNET3_URL, headers=app_head, cookies=app_cookies) + if home.status_code != 200: + return WEB_VIEW_VER_FALLBACK + + soup = BeautifulSoup(home.text, "html.parser") + main_js = soup.select_one("script[src*='static']") + + if not main_js: # failed to parse html for main.js file + return WEB_VIEW_VER_FALLBACK + + main_js_url = SPLATNET3_URL + main_js.attrs["src"] + + app_head = { + 'Accept': '*/*', + 'X-Requested-With': 'com.nintendo.znca', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Dest': 'script' + } + if bhead: + app_head["User-Agent"] = bhead.get("User-Agent") + app_head["Referer"] = bhead.get("Referer") + app_head["Accept-Encoding"] = bhead.get("Accept-Encoding") + app_head["Accept-Language"] = bhead.get("Accept-Language") + + main_js_body = requests.get(main_js_url, headers=app_head, cookies=app_cookies) + if main_js_body.status_code != 200: + return iksm.WEB_VIEW_VER_FALLBACK + + pattern = r"\b(?P[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\d+\.\d+\.\d+)" + match = re.search(pattern, main_js_body.text) + if match is None: + return iksm.WEB_VIEW_VER_FALLBACK + + version, revision = match.group("version"), match.group("revision") + ver_string = f"{version}-{revision[:8]}" + + WEB_VIEW_VERSION = ver_string + + return WEB_VIEW_VERSION def log_in(ver, app_user_agent): @@ -128,7 +205,7 @@ def get_session_token(session_token_code, auth_code_verifier): def get_gtoken(f_gen_url, session_token, ver): - '''Provided the session_token, returns a GameWebToken and account info.''' + '''Provided the session_token, returns a GameWebToken JWT and account info.''' nsoapp_version = get_nsoapp_version() @@ -184,7 +261,7 @@ def get_gtoken(f_gen_url, session_token, ver): body = {} try: id_token = id_response["id_token"] - f, uuid, timestamp = call_imink_api(id_token, 1, f_gen_url) + f, uuid, timestamp = call_f_api(id_token, 1, f_gen_url) parameter = { 'f': f, @@ -223,7 +300,7 @@ def get_gtoken(f_gen_url, session_token, ver): except: # retry once if 9403/9599 error from nintendo try: - f, uuid, timestamp = call_imink_api(id_token, 1, f_gen_url) + f, uuid, timestamp = call_f_api(id_token, 1, f_gen_url) body["parameter"]["f"] = f body["parameter"]["requestId"] = uuid body["parameter"]["timestamp"] = timestamp @@ -238,7 +315,7 @@ def get_gtoken(f_gen_url, session_token, ver): print("Re-running the script usually fixes this.") sys.exit(1) - f, uuid, timestamp = call_imink_api(id_token, 2, f_gen_url) + f, uuid, timestamp = call_f_api(id_token, 2, f_gen_url) # get web service token app_head = { @@ -270,7 +347,7 @@ def get_gtoken(f_gen_url, session_token, ver): except: # retry once if 9403/9599 error from nintendo try: - f, uuid, timestamp = call_imink_api(id_token, 2, f_gen_url) + f, uuid, timestamp = call_f_api(id_token, 2, f_gen_url) body["parameter"]["f"] = f body["parameter"]["requestId"] = uuid body["parameter"]["timestamp"] = timestamp @@ -286,25 +363,25 @@ def get_gtoken(f_gen_url, session_token, ver): return web_service_token, user_nickname, user_lang, user_country -def get_bullet(web_service_token, web_view_ver, app_user_agent, user_lang, user_country): - '''Returns a bulletToken.''' +def get_bullet(web_service_token, app_user_agent, user_lang, user_country): + '''Given a gtoken, returns a bulletToken.''' app_head = { 'Content-Length': '0', 'Content-Type': 'application/json', 'Accept-Language': user_lang, 'User-Agent': app_user_agent, - 'X-Web-View-Ver': web_view_ver, + 'X-Web-View-Ver': get_web_view_ver(), 'X-NACOUNTRY': user_country, 'Accept': '*/*', - 'Origin': 'https://api.lp1.av5ja.srv.nintendo.net', + 'Origin': SPLATNET3_URL, 'X-Requested-With': 'com.nintendo.znca' } app_cookies = { '_gtoken': web_service_token, # X-GameWebToken '_dnt': '1' # Do Not Track } - url = "https://api.lp1.av5ja.srv.nintendo.net/api/bullet_tokens" + url = f'{SPLATNET3_URL}/api/bullet_tokens' r = requests.post(url, headers=app_head, cookies=app_cookies) if r.status_code == 401: @@ -332,8 +409,8 @@ def get_bullet(web_service_token, web_view_ver, app_user_agent, user_lang, user_ return bullet_token -def call_imink_api(id_token, step, f_gen_url): - '''Passes in an naIdToken to the imink API and fetches the response (comprised of an f token, UUID, and timestamp).''' +def call_f_api(id_token, step, f_gen_url): + '''Passes an naIdToken to the f generation API (default: imink) & fetches the response (f token, UUID, and timestamp).''' try: api_head = { @@ -366,7 +443,8 @@ def call_imink_api(id_token, step, f_gen_url): def enter_tokens(): '''Prompts the user to enter a gtoken and bulletToken.''' - print("Go to the page below to find instructions to obtain your gtoken and bulletToken:\nhttps://github.com/frozenpandaman/s3s/wiki/mitmproxy-instructions\n") + print("Go to the page below to find instructions to obtain your gtoken and bulletToken:") + print("https://github.com/frozenpandaman/s3s/wiki/mitmproxy-instructions\n") new_gtoken = input("Enter your gtoken: ") while len(new_gtoken) != 926: diff --git a/s3s.py b/s3s.py index 2b1a6b2..f60589c 100755 --- a/s3s.py +++ b/s3s.py @@ -8,8 +8,7 @@ import argparse, datetime, json, os, shutil, re, requests, sys, time, uuid import msgpack import iksm, utils -A_VERSION = "0.1.9" -WEB_VIEW_VERSION = "unknown" # set in prefetch_checks() +A_VERSION = "0.1.10" DEBUG = False @@ -39,10 +38,6 @@ except (IOError, ValueError): CONFIG_DATA = json.load(config_file) config_file.close() -DEFAULT_USER_AGENT = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) ' \ - 'AppleWebKit/537.36 (KHTML, like Gecko) ' \ - 'Chrome/94.0.4606.61 Mobile Safari/537.36' - # SET GLOBALS API_KEY = CONFIG_DATA["api_key"] # for stat.ink USER_LANG = CONFIG_DATA["acc_loc"][:5] # nintendo account info @@ -53,6 +48,9 @@ SESSION_TOKEN = CONFIG_DATA["session_token"] # for nintendo login F_GEN_URL = CONFIG_DATA["f_gen"] # endpoint for generating f (imink API by default) # SET HTTP HEADERS +DEFAULT_USER_AGENT = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) ' \ + 'AppleWebKit/537.36 (KHTML, like Gecko) ' \ + 'Chrome/94.0.4606.61 Mobile Safari/537.36' APP_USER_AGENT = str(CONFIG_DATA.get("app_user_agent", DEFAULT_USER_AGENT)) @@ -90,12 +88,12 @@ def headbutt(): 'Authorization': f'Bearer {BULLETTOKEN}', # update every time it's called with current global var 'Accept-Language': USER_LANG, 'User-Agent': APP_USER_AGENT, - 'X-Web-View-Ver': WEB_VIEW_VERSION, + 'X-Web-View-Ver': iksm.WEB_VIEW_VERSION, 'Content-Type': 'application/json', 'Accept': '*/*', - 'Origin': 'https://api.lp1.av5ja.srv.nintendo.net', + 'Origin': iksm.SPLATNET3_URL, 'X-Requested-With': 'com.nintendo.znca', - 'Referer': f'https://api.lp1.av5ja.srv.nintendo.net/?lang={USER_LANG}&na_country={USER_COUNTRY}&na_lang={USER_LANG}', + 'Referer': f'{iksm.SPLATNET3_URL}?lang={USER_LANG}&na_country={USER_COUNTRY}&na_lang={USER_LANG}', 'Accept-Encoding': 'gzip, deflate' } return graphql_head @@ -107,12 +105,12 @@ def prefetch_checks(printout=False): if printout: print("Validating your tokens...", end='\r') + global WEB_VIEW_VERSION + WEB_VIEW_VERSION = iksm.get_web_view_ver() + if SESSION_TOKEN == "" or GTOKEN == "" or BULLETTOKEN == "": gen_new_tokens("blank") - global WEB_VIEW_VERSION - WEB_VIEW_VERSION = utils.get_web_view_ver(headbutt(), GTOKEN) - sha = utils.translate_rid["HomeQuery"] test = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(sha), headers=headbutt(), cookies=dict(_gtoken=GTOKEN)) if test.status_code != 200: @@ -130,7 +128,7 @@ def gen_new_tokens(reason, force=False): manual_entry = False if force != True: # unless we force our way through if reason == "blank": - print("Blank token(s).") + print("Blank token(s). ") elif reason == "expiry": print("The stored tokens have expired.") else: @@ -160,7 +158,7 @@ def gen_new_tokens(reason, force=False): else: print("Attempting to generate new gtoken and bulletToken...") new_gtoken, acc_name, acc_lang, acc_country = iksm.get_gtoken(F_GEN_URL, SESSION_TOKEN, A_VERSION) - new_bullettoken = iksm.get_bullet(new_gtoken, WEB_VIEW_VERSION, APP_USER_AGENT, acc_lang, acc_country) + new_bullettoken = iksm.get_bullet(new_gtoken, APP_USER_AGENT, acc_lang, acc_country) CONFIG_DATA["gtoken"] = new_gtoken # valid for 2 hours CONFIG_DATA["bullettoken"] = new_bullettoken # valid for 2 hours CONFIG_DATA["acc_loc"] = acc_lang + "|" + acc_country diff --git a/utils.py b/utils.py index ad3b22c..31f372c 100644 --- a/utils.py +++ b/utils.py @@ -5,10 +5,10 @@ import base64, datetime, json, re, sys, uuid import requests from bs4 import BeautifulSoup +import iksm -SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net" -GRAPHQL_URL = "https://api.lp1.av5ja.srv.nintendo.net/api/graphql" -FALLBACK_WEB_VIEW_VERSION = "1.0.0-5644e7a2" # fallback for current splatnet 3 ver +SPLATNET3_URL = iksm.SPLATNET3_URL +GRAPHQL_URL = f'{SPLATNET3_URL}/api/graphql' S3S_NAMESPACE = uuid.UUID('b3a2dbf5-2c09-4792-b78c-00b548b70aeb') SUPPORTED_KEYS = [ @@ -18,7 +18,7 @@ SUPPORTED_KEYS = [ ] # SHA256 hash database for SplatNet 3 GraphQL queries -# full list: https://github.com/samuelthomas2774/nxapi/discussions/11#discussioncomment-3737698 +# full list: https://github.com/samuelthomas2774/nxapi/discussions/11#discussioncomment-3614603 translate_rid = { 'HomeQuery': 'dba47124d5ec3090c97ba17db5d2f4b3', # blank vars 'LatestBattleHistoriesQuery': '7d8b560e31617e981cf7c8aa1ca13a00', # INK / blank vars - query1 @@ -30,63 +30,6 @@ translate_rid = { 'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e' # SR / req "coopHistoryDetailId" - query2 } -def get_web_view_ver(bhead, gtoken): - '''Find & parse the SplatNet 3 main.js file for the current site version.''' - - app_head = { # bhead should have all fields from headbutt() & gtoken will be valid - 'Upgrade-Insecure-Requests': '1', - 'User-Agent': bhead["User-Agent"], - 'Accept': '*/*', - 'DNT': '1', - 'X-AppColorScheme': 'DARK', - 'X-Requested-With': 'com.nintendo.znca', - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-User': '?1', - 'Sec-Fetch-Dest': 'document', - 'Accept-Encoding': bhead["Accept-Encoding"], - 'Accept-Language': bhead["Accept-Language"] - } - app_cookies = { - '_gtoken': gtoken, # X-GameWebToken - '_dnt': '1' # Do Not Track - } - home = requests.get(SPLATNET3_URL, headers=app_head, cookies=app_cookies) - if home.status_code != 200: - return FALLBACK_WEB_VIEW_VERSION - - soup = BeautifulSoup(home.text, "html.parser") - main_js = soup.select_one("script[src*='static']") - - if not main_js: # failed to parse html for main.js file - return FALLBACK_WEB_VIEW_VERSION - - main_js_url = SPLATNET3_URL + main_js.attrs["src"] - - app_head = { - 'User-Agent': bhead["User-Agent"], - 'Accept': '*/*', - 'X-Requested-With': 'com.nintendo.znca', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-Mode': 'no-cors', - 'Sec-Fetch-Dest': 'script', - 'Referer': bhead["Referer"], - 'Accept-Encoding': bhead["Accept-Encoding"], - 'Accept-Language': bhead["Accept-Language"] - } - - main_js_body = requests.get(main_js_url, headers=app_head, cookies=app_cookies) - if main_js_body.status_code != 200: - return FALLBACK_WEB_VIEW_VERSION - - pattern = r"\b(?P[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P\d+\.\d+\.\d+)" - match = re.search(pattern, main_js_body.text) - if match is None: - return FALLBACK_WEB_VIEW_VERSION - - version, revision = match.group("version"), match.group("revision") - return f"{version}-{revision[:8]}" - def set_noun(which): '''Returns the term to be used when referring to the type of results in question.''' @@ -100,7 +43,7 @@ def set_noun(which): def b64d(string): - '''Base64 decode a string and cut off the SplatNet prefix.''' + '''Base64 decodes a string and cut off the SplatNet prefix.''' thing_id = base64.b64decode(string).decode('utf-8') thing_id = thing_id.replace("VsStage-", "")