major refactor to address #62 & improve structure

This commit is contained in:
eli fessler 2022-10-26 16:31:08 -10:00
parent a4425bf461
commit 487ab596da
3 changed files with 130 additions and 111 deletions

148
iksm.py
View File

@ -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<revision>[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P<version>\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:

26
s3s.py
View File

@ -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

View File

@ -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<revision>[0-9a-f]{40})\b.*revision_info_not_set\"\),.*?=\"(?P<version>\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-", "")