Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Last active September 12, 2022 01:40
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ariankordi/016ac1d24eb45f13efb9e8660b6d62b2 to your computer and use it in GitHub Desktop.
Save ariankordi/016ac1d24eb45f13efb9e8660b6d62b2 to your computer and use it in GitHub Desktop.
my stupid hack for using nooklink for animal crossing: new horizons on pc involving mitmproxy which i have had to repost sorry
hi. this app is a reverse proxy, meaning that when it launches, it runs a server that connects to the official server, like a proxy (except that it only supports one site) and it modifies some requests and responses along the way.
now here is a terrible guide
first: the "iksm.py" file and its functions is copied and modified from splatnet2statink, retrieved from https://github.com/frozenpandaman/splatnet2statink at least at commit a971a21fafdbd026aa8b44d7514119d3ff6073a2. this information should be added in the file itself but it's worth mentioning here
when running any of these files like bruh.py or mitmproxy with bruhx.py, if you see that there's a module missing, just install it (notably requests and hashlib is required)
the main magic script which is the mitmproxy addon and gets tokens from splatnet2statink functions is bruhx.py. execute bruh.py, (without the x), follow the instructions, and then the value it gives you needs to be placed into the "codee" variable in bruhx.py.
bruh.sh starts mitmproxy. at this point we should be able to execute it. if it runs fine, then access http://localhost:8000/ in a browser, wait a few seconds (don't leave! it takes at least 7ish seconds for me) and then proceed.
note that sometimes the app needs to refresh your token, such as when it expires (within like 2 hours iirc), when you restart the script or the first time you launch it. it'll take a relatively long time, again like 7 seconds. please be patient, and when in doubt, look at the console and there should be some activity when it's doing all of this.
if this never works for you, i'm sorry
i hope this is at least somewhat understandable and helpful to you
])</script><script>
// handles nso app-specific functions
// really these should all be handled server-side
// persistentstorage should use the server, and
// requestgamewebtoken especially should
// but these will work for now, along with
// manually assigning a gamewebtoken
/*// if the current params aren't what they are above, add them and refresh
if(window.document.location.search !== queryParamsShouldBe) {
window.document.location.search = queryParamsShouldBe;
}*/
// default query params if there are none
const queryParamsDefault = 'lang=en-US&na_country=US&na_lang=en-US';
// na_country is required so here's a lazy check for it
if(!window.document.location.search.includes('na_country')) {
// if na_country doesn't exist then just add the default params
window.document.location.search = queryParamsDefault;
}
// redirect to root when application starts
// if application is anywhere else on start then it will not load
if(window.document.location.pathname != '/') {
// apply default query params if they're blank
if(window.document.location.search == '') {
window.document.location = '/?' + queryParamsDefault;
} else {
window.document.location.pathname = '/';
}
}
// ditched for storing in a cookie
/*
const persistentDataRepresentation = {
// skips walkthrough
marked: true,
// my userid. todo remove this if i release this or something i do'nt know
userId: '0x'
// persistent data will also contain language-country
// code as in the lang query parameter but it's only
// stored when the language is changed, example:
//language: 'en-GB'
};
*/
//document.cookie = '_gtoken=' + gtoken;
// game token (x-gamewebtoken) request
window.requestGameWebToken = () => {
window.console.log('requestgametoken called');
/*setTimeout(() => {
window.onGameWebTokenReceive(gtoken);
}, 10);*/
fetch('/_/request_gamewebtoken')
.then(response => {
return response.text();
})
.then(data => {
// data is now gtoken
window.onGameWebTokenReceive(data);
});
};
// persistent data request
window.restorePersistentData = () => {
window.console.log('restorepersistentdata called');
/*// respond with persistentData
window.onPersistentDataRestore(JSON.stringify(persistentData));
*/
// get persistentdata from cookie (stolen from https://stackoverflow.com/a/15724300)
let cookieParts = ('; ' + document.cookie).split('; nso-persistent-storage=');
// "initialize" restoredpersistentdata by making it blank
// in case we can't find one with the steps below
var restoredPersistentData = '';
// this is stupid and might not be stable
if(cookieParts.length === 2) {
restoredPersistentData = cookieParts.pop().split(';').shift();
}
// respond with restored persistent data
window.onPersistentDataRestore(restoredPersistentData);
};
window.storePersistentData = (input) => {
window.console.log('persistentdatastore called', input);
// store input in cookie "nso-persistent-storage"
// to expire in three days
// get a date (stolen from https://stackoverflow.com/a/23081260)
let expiryDate = new Date();
expiryDate.setDate(new Date().getDate() + 3);
// input is a string (json)
// set cookie
document.cookie = 'nso-persistent-storage=' + input + '; expires=' + expiryDate.toUTCString() + '; path=/';
window.onPersistentDataStore();
};
</script>
#!/usr/bin/python3
# so that requests will log urls
#import logging
#logging.basicConfig(level=logging.DEBUG)
import iksm
# use this to log in and get the value of "codee" then exit i guess
print(iksm.log_in('1.5.4')); from sys import exit; exit(0)
#codee = ''
#token = iksm.get_cookie(codee, 'en-US', '')
# this code is dead
#print(token[1])
#bruh = open('bruht.js', 'w+')
#bruh.write(
#"""\"\"><script>
#const gtoken = '""" + token[1] + """';
#</script>""")
#bruh.close()
#print('done?? idk')
# use this to log in every time
#iksm.get_cookie(iksm.log_in(''), 'en-US', '')
#!/bin/sh
# starts the mitmdump command that we want
# requires mitmproxy package
mitmdump --listen-port 8000 --mode \
reverse:https://web.sd.lp1.acbaa.srv.nintendo.net/ \
--modify-body [~s[https://web.sd.lp1.acbaa.srv.nintendo.net[ \
--modify-body :~s:'\]\)</script>':@bruh.js \
-s bruhx.py
#!/usr/bin/python3
from mitmproxy import http
import iksm
import time
# use this to log in and get the value of "codee" then exit i guess
#print(iksm.log_in('1.5.4')); from sys import exit; exit(0)
codee = ''
token_global = ['', 0]
def get_token_or_cached():
print('get_token_or_cached!')
global token_global
# is the expiry time greater than the current time?
if token_global[1] > int(time.time()):
print('retrieving gamewebtoken from cache')
# token HAS NOT EXPIRED, return the current one
return token_global[0]
else:
print('retrieving gamewebtoken from iksm function')
# get a new token
token = iksm.get_cookie(codee, 'en-US', '1.5.4')
# so token[1] is the token, token[2] is the expiry tim
future_expiry_time = (int(time.time()) + token[2])
token_global = [token[1], future_expiry_time]
return token[1]
def request(flow):
#print(flow.request.path)
# if path is / but has a query param
if flow.request.path[:2] == '/?':
print('getting x-gamewebtoken')
flow.intercept()
# get token o.k.
token = get_token_or_cached()
flow.request.headers['X-GameWebToken'] = token
# add DNT header which makes the server able to send us cookies
# important. _gtoken cookie is NOT sent without this.
flow.request.headers['DNT'] = '0'
print('added gamewebtoken header. now resuming')
flow.resume()
if flow.request.path == '/_/request_gamewebtoken':
#print('we are intercepting this')
print('request_gamewebtoken')
token = get_token_or_cached()
flow.response = http.HTTPResponse.make(
200,
token.encode()
)
# fix cookies to work with plain http
def response(flow):
# basically remove "Secure" from all set-cookie headers
cookie_headers = flow.response.headers.get_all('Set-Cookie')
for key, value in enumerate(cookie_headers):
cookie_headers[key] = value.replace('Secure; ', '')
flow.response.headers.set_all('Set-Cookie', cookie_headers)
{
"_comment": "this file is left here for compatibility reasons, the main script may fail if this is not here"
}
# splatnet2statink, retrieved from https://github.com/frozenpandaman/splatnet2statink at commit 15c7375afc3e5161c258bbdf363625b017b872a2
# original license is gplv3 please don't kill me even though i think i am being compliant
# eli fessler
# clovervidia
from __future__ import print_function
from builtins import input
import requests, json, re, sys
import os, base64, hashlib
import uuid, time, random, string
import webbrowser
session = requests.Session()
version = "unknown"
# 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")
def log_in(ver):
'''Logs in to a Nintendo Account and returns a session_token.'''
global version
version = ver
auth_state = base64.urlsafe_b64encode(os.urandom(36))
auth_code_verifier = base64.urlsafe_b64encode(os.urandom(32))
auth_cv_hash = hashlib.sha256()
auth_cv_hash.update(auth_code_verifier.replace(b"=", b""))
auth_code_challenge = base64.urlsafe_b64encode(auth_cv_hash.digest())
app_head = {
'Host': 'accounts.nintendo.com',
'Connection': 'keep-alive',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Linux; Android 7.1.2; Pixel Build/NJH47D; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8n',
'DNT': '1',
'Accept-Encoding': 'gzip,deflate,br',
}
body = {
'state': auth_state,
'redirect_uri': 'npf71b963c1b7b6d119://auth',
'client_id': '71b963c1b7b6d119',
'scope': 'openid user user.birthday user.mii user.screenName',
'response_type': 'session_token_code',
'session_token_code_challenge': auth_code_challenge.replace(b"=", b""),
'session_token_code_challenge_method': 'S256',
'theme': 'login_form'
}
url = 'https://accounts.nintendo.com/connect/1.0.0/authorize'
r = session.get(url, headers=app_head, params=body)
post_login = r.history[0].url
print("\nMake sure you have fully read the \"Cookie generation\" section of the readme before proceeding. To manually input a cookie instead, enter \"skip\" at the prompt below.")
print("\nNavigate to this URL in your browser:")
print(post_login)
webbrowser.open(post_login)
print("Log in, right click the \"Select this account\" button, copy the link address, and paste it below:")
while True:
try:
use_account_url = input("")
if use_account_url == "skip":
return "skip"
session_token_code = re.search('de=(.*)&', use_account_url)
return get_session_token(session_token_code.group(1), auth_code_verifier)
except KeyboardInterrupt:
print("\nBye!")
sys.exit(1)
except AttributeError:
print("Malformed URL. Please try again, or press Ctrl+C to exit.")
print("URL:", end=' ')
except KeyError: # session_token not found
print("\nThe URL has expired. Please log out and back into your Nintendo Account and try again.")
sys.exit(1)
def get_session_token(session_token_code, auth_code_verifier):
'''Helper function for log_in().'''
app_head = {
'User-Agent': 'OnlineLounge/1.10.0 NASDKAPI Android',
'Accept-Language': 'en-US',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '540',
'Host': 'accounts.nintendo.com',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip'
}
body = {
'client_id': '71b963c1b7b6d119',
'session_token_code': session_token_code,
'session_token_code_verifier': auth_code_verifier.replace(b"=", b"")
}
url = 'https://accounts.nintendo.com/connect/1.0.0/api/session_token'
r = session.post(url, headers=app_head, data=body)
return json.loads(r.text)["session_token"]
def get_cookie(session_token, userLang, ver):
'''Returns a new cookie provided the session_token.'''
global version
version = ver
timestamp = int(time.time())
guid = str(uuid.uuid4())
app_head = {
'Host': 'accounts.nintendo.com',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/json; charset=utf-8',
'Accept-Language': userLang,
'Content-Length': '439',
'Accept': 'application/json',
'Connection': 'Keep-Alive',
'User-Agent': 'OnlineLounge/1.10.0 NASDKAPI Android'
}
body = {
'client_id': '71b963c1b7b6d119', # Splatoon 2 service
'session_token': session_token,
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token'
}
url = "https://accounts.nintendo.com/connect/1.0.0/api/token"
r = requests.post(url, headers=app_head, json=body)
id_response = json.loads(r.text)
# get user info
try:
app_head = {
'User-Agent': 'OnlineLounge/1.10.0 NASDKAPI Android',
'Accept-Language': userLang,
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(id_response["access_token"]),
'Host': 'api.accounts.nintendo.com',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip'
}
except:
print("Not a valid authorization request. Please delete config.txt and try again.")
print("Error from Nintendo (in api/token step):")
print(json.dumps(id_response, indent=2))
sys.exit(1)
url = "https://api.accounts.nintendo.com/2.0.0/users/me"
r = requests.get(url, headers=app_head)
user_info = json.loads(r.text)
nickname = user_info["nickname"]
# get access token
app_head = {
'Host': 'api-lp1.znc.srv.nintendo.net',
'Accept-Language': userLang,
'User-Agent': 'com.nintendo.znca/1.10.0 (Android/7.1.2)',
'Accept': 'application/json',
'X-ProductVersion': '1.10.0',
'Content-Type': 'application/json; charset=utf-8',
'Connection': 'Keep-Alive',
'Authorization': 'Bearer',
# 'Content-Length': '1036',
'X-Platform': 'Android',
'Accept-Encoding': 'gzip'
}
body = {}
try:
idToken = id_response["access_token"]
flapg_nso = call_flapg_api(idToken, guid, timestamp, "nso")
parameter = {
'f': flapg_nso["f"],
'naIdToken': flapg_nso["p1"],
'timestamp': flapg_nso["p2"],
'requestId': flapg_nso["p3"],
'naCountry': user_info["country"],
'naBirthday': user_info["birthday"],
'language': user_info["language"]
}
except SystemExit:
sys.exit(1)
except:
print("Error(s) from Nintendo:")
print(json.dumps(id_response, indent=2))
print(json.dumps(user_info, indent=2))
sys.exit(1)
body["parameter"] = parameter
url = "https://api-lp1.znc.srv.nintendo.net/v1/Account/Login"
r = requests.post(url, headers=app_head, json=body)
splatoon_token = json.loads(r.text)
try:
idToken = splatoon_token["result"]["webApiServerCredential"]["accessToken"]
flapg_app = call_flapg_api(idToken, guid, timestamp, "app")
except:
print("Error from Nintendo (in Account/Login step):")
print(json.dumps(splatoon_token, indent=2))
sys.exit(1)
# get splatoon access token
try:
app_head = {
'Host': 'api-lp1.znc.srv.nintendo.net',
'User-Agent': 'com.nintendo.znca/1.10.0 (Android/7.1.2)',
'Accept': 'application/json',
'X-ProductVersion': '1.10.0',
'Content-Type': 'application/json; charset=utf-8',
'Connection': 'Keep-Alive',
'Authorization': 'Bearer {}'.format(splatoon_token["result"]["webApiServerCredential"]["accessToken"]),
'Content-Length': '37',
'X-Platform': 'Android',
'Accept-Encoding': 'gzip'
}
except:
print("Error from Nintendo (in Account/Login step):")
print(json.dumps(splatoon_token, indent=2))
sys.exit(1)
body = {}
parameter = {
# acnh id i gues
'id': 4953919198265344,
'f': flapg_app["f"],
'registrationToken': flapg_app["p1"],
'timestamp': flapg_app["p2"],
'requestId': flapg_app["p3"]
}
body["parameter"] = parameter
url = "https://api-lp1.znc.srv.nintendo.net/v2/Game/GetWebServiceToken"
r = requests.post(url, headers=app_head, json=body)
splatoon_access_token = json.loads(r.text)
# get cookie
try:
app_head = {
'Host': 'app.splatoon2.nintendo.net',
'X-IsAppAnalyticsOptedIn': 'false',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Encoding': 'gzip,deflate',
'X-GameWebToken': splatoon_access_token["result"]["accessToken"],
'Accept-Language': userLang,
'X-IsAnalyticsOptedIn': 'false',
'Connection': 'keep-alive',
'DNT': '0',
'User-Agent': 'Mozilla/5.0 (Linux; Android 7.1.2; Pixel Build/NJH47D; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36',
'X-Requested-With': 'com.nintendo.znca'
}
except:
print("Error from Nintendo (in Game/GetWebServiceToken step):")
print(json.dumps(splatoon_access_token, indent=2))
sys.exit(1)
url = "https://app.splatoon2.nintendo.net/?lang={}".format(userLang)
r = requests.get(url, headers=app_head)
#return nickname, r.cookies["iksm_session"]
return nickname, splatoon_access_token["result"]["accessToken"], splatoon_access_token["result"]["expiresIn"]
def get_hash_from_s2s_api(id_token, timestamp):
'''Passes an id_token and timestamp to the s2s API and fetches the resultant hash from the response.'''
# check to make sure we're allowed to contact the API. stop spamming my web server pls
config_file = open(config_path, "r")
config_data = json.load(config_file)
config_file.close()
try:
num_errors = config_data["api_errors"]
except:
num_errors = 0
if num_errors >= 5:
print("Too many errors received from the splatnet2statink API. Further requests have been blocked until the \"api_errors\" line is manually removed from config.txt. If this issue persists, please contact @frozenpandaman on Twitter/GitHub for assistance.")
sys.exit(1)
# proceed normally
try:
api_app_head = { 'User-Agent': "splatnet2statink/{}".format(version) }
api_body = { 'naIdToken': id_token, 'timestamp': timestamp }
api_response = requests.post("https://elifessler.com/s2s/api/gen2", headers=api_app_head, data=api_body)
return json.loads(api_response.text)["hash"]
except:
print("Error from the splatnet2statink API:\n{}".format(json.dumps(json.loads(api_response.text), indent=2)))
# add 1 to api_errors in config
config_file = open(config_path, "r")
config_data = json.load(config_file)
config_file.close()
try:
num_errors = config_data["api_errors"]
except:
num_errors = 0
num_errors += 1
config_data["api_errors"] = num_errors
config_file = open(config_path, "w") # from write_config()
config_file.seek(0)
config_file.write(json.dumps(config_data, indent=4, sort_keys=True, separators=(',', ': ')))
config_file.close()
sys.exit(1)
def call_flapg_api(id_token, guid, timestamp, type):
'''Passes in headers to the flapg API (Android emulator) and fetches the response.'''
try:
api_app_head = {
'x-token': id_token,
'x-time': str(timestamp),
'x-guid': guid,
'x-hash': get_hash_from_s2s_api(id_token, timestamp),
'x-ver': '3',
'x-iid': type
}
api_response = requests.get("https://flapg.com/ika2/api/login?public", headers=api_app_head)
f = json.loads(api_response.text)["result"]
return f
except:
try: # if api_response never gets set
if api_response.text:
print(u"Error from the flapg API:\n{}".format(json.dumps(json.loads(api_response.text), indent=2, ensure_ascii=False)))
elif api_response.status_code == requests.codes.not_found:
print("Error from the flapg API: Error 404 (offline or incorrect headers).")
else:
print("Error from the flapg API: Error {}.".format(api_response.status_code))
except:
pass
sys.exit(1)
def enter_cookie():
'''Prompts the user to enter their iksm_session cookie'''
new_cookie = input("Go to the page below to find instructions to obtain your iksm_session cookie:\nhttps://github.com/frozenpandaman/splatnet2statink/wiki/mitmproxy-instructions\nEnter it here: ")
while len(new_cookie) != 40:
new_cookie = input("Cookie is invalid. Please enter it again.\nCookie: ")
return new_cookie
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment