Last active
September 23, 2023 04:30
-
-
Save ariankordi/60d79c7fb23bb24d11c400b65dc45ece to your computer and use it in GitHub Desktop.
two versions of my "nso reverse proxy" (deranged) (and CURSED)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
in order to make the scripts happy, there is supposed to be a folder here called "app" | |
it is actually just from the nxapi electron app, it SHOULD be just resources/app | |
after that, you DO need to authenticate to nxapi - do something like this: node app/dist/bundle/cli-bundle.js nso auth | |
MORE GENERAL EXPLANATION: | |
this is a reverse proxy that's meant to help me access splatnet 3 in my browser, leveraging other tools to do so | |
bruh.js, bruhx.py, and bruh.sh are more or less carried over from a previous hack for nooklink that actually just used a hacked up iksm from splatnet2statink: https://gist.github.com/ariankordi/016ac1d24eb45f13efb9e8660b6d62b2 | |
this calls out to nxapi, which, you would need to download the app folder for AND authenticate to. | |
the proxy is meant to be used via a systemd socket spawning it and then later killing it, this ensures it literally only runs when it needs to. then, you can use a browser extension to selectively proxy that nintendo site over and voila, it SHOULD load correctly. it SHOULD. it should work for nooklink and any nso app site you know the address of | |
mitmproxy will probably consume more and more memory the more you use it, like a perpetual memory leak... | |
so, this mitmproxy-based backend I ditched in favor of what you see in bruhy.py, which is a standalone web server meant to work with the userscript you find here | |
this approach will NOT! work with nooklink because it can't inject into the request headers but that's fine enough with me | |
you will see that there's an nso-reverse and nso-reverse-actual. the -actual service and the commented lines are for the mitmproxy approach but NOT!! for bruhy.py - it can run standalone and serve the systemd sockets directly - OR NOT! should work without that nonsense as well. | |
TL;DR: | |
there's a mitmproxy and standalone version of this same thing, which pretty much just helps let you access splatnet 3 and other nso apps in your browser (e.g, api.lp1.av5ja.srv.nintendo.net). the standalone one (bruhy.py) is faster and more maintained | |
it calls out to nxapi and is basically, literally just a web server that serves tokens from it, but you need to do some setup related to nxapi before you can use it | |
sorry this is so crap i am mostly releasing this for my own sake at this point | |
lmk if you have any questions about this, my discord username CURRENTLY is .arian. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<head><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; | |
}*/ | |
// 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# starts the mitmdump command that we want | |
# requires mitmproxy package | |
cd "$(dirname "$0")" | |
#/usr/local/bin/mitmdump --listen-port 8001 --mode \ | |
#reverse:https://web.sd.lp1.acbaa.srv.nintendo.net/ \ | |
#--modify-body [~s[https://api.lp1.av5ja.srv.nintendo.net[ \ | |
mitmdump --listen-port 36017 \ | |
--modify-body :~s:'<head>':@bruh.js \ | |
-s bruhx.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
from mitmproxy import http | |
import subprocess | |
import json | |
import time | |
import os | |
import tempfile | |
# services that i use | |
services = {} | |
tokens_global = {} | |
new_token = False | |
# reload tokens_global from a file | |
token_state_filename = os.path.join(tempfile.gettempdir(), 'nso-reverse-proxy-token-state.json') | |
try: | |
open(token_state_filename, 'x') | |
except FileExistsError: | |
pass | |
try: | |
token_state_file = open(token_state_filename, 'r') | |
except FileNotFoundError: | |
open(token_state_filename, 'x') | |
else: | |
# if the file has content | |
read_test = token_state_file.read(1) | |
token_state_file.seek(0) | |
if read_test: | |
#print('token state loading') | |
tokens_global_read = json.load(token_state_file) | |
print('token state loaded') | |
# only add non-expired entries | |
for a in tokens_global_read.keys(): | |
if tokens_global_read[a][1] > int(time.time()): | |
tokens_global[a] = tokens_global_read[a] | |
#print(tokens_global) | |
services_output = '' | |
try: | |
# first, try seeing if the cached version is defined | |
services_output = tokens_global['services'][0] | |
except KeyError: | |
# if not, then actually use the | |
services_output = subprocess.check_output('node app/dist/bundle/cli-bundle.js nso webservices --json', shell=True) | |
# make it expire in 2 weeks | |
tokens_global['services'] = [services_output.decode(), time.time() + 604800] | |
new_token = True | |
try: | |
services_json = json.loads(services_output) | |
except Exception as e: | |
print('output: ', services_output) | |
raise e | |
with open(token_state_filename, 'w') as token_state_file: | |
json.dump(tokens_global, token_state_file) | |
token_state_file.close() | |
for a in services_json: | |
hostname = a['uri'].rsplit('://')[1].strip('/') | |
services[hostname] = a['id'] | |
#tokens_global = {} | |
def get_token_or_cached(id): | |
id = str(id) | |
print('get_token_or_cached: ' + id) | |
global tokens_global | |
# is the expiry time greater than the current time? | |
#try: | |
# print(tokens_global[id]) | |
#except: | |
# pass | |
if id in tokens_global and (tokens_global[id][1] > int(time.time())): | |
print('retrieving gamewebtoken from cache') | |
# token HAS NOT EXPIRED, return the current one | |
return tokens_global[id][0] | |
else: | |
print('retrieving gamewebtoken from nxapi bundl') | |
# get a new token | |
output = subprocess.check_output('node app/dist/bundle/cli-bundle.js nso webservicetoken ' + id + ' --json', shell=True) | |
try: | |
putput = json.loads(output) | |
except Exception as e: | |
print('output: ', output) | |
raise e | |
# so token[1] is the token, token[2] is the expiry tim | |
future_expiry_time = (int(time.time()) + putput['token']['expiresIn']) | |
global new_token | |
new_token = True | |
tokens_global[id] = [putput['token']['accessToken'], future_expiry_time] | |
return putput['token']['accessToken'] | |
def request(flow): | |
#print(flow.request.path) | |
# if path is / and lazy hack to see if it is just / with queery param | |
if flow.request.path == "/" or flow.request.path[:2] == '/?': | |
print('getting x-gamewebtoken') | |
flow.intercept() | |
# get token o.k. | |
#try: | |
token = get_token_or_cached(services[flow.request.pretty_host]) | |
#except KeyError: | |
# print('"' + flow.request.pretty_host + '" is not a host in the services list, passing through') | |
# flow.resume() | |
# return | |
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('request_gamewebtoken') | |
token = get_token_or_cached(services[flow.request.pretty_host]) | |
flow.response = http.Response.make( | |
200, | |
token.encode() | |
) | |
global new_token | |
if new_token: | |
print('new_token is set, saving token state') | |
new_token = False | |
with open(token_state_filename, 'w') as token_state_file: | |
json.dump(tokens_global, token_state_file) | |
token_state_file.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
from http.server import BaseHTTPRequestHandler, HTTPServer | |
import subprocess | |
import json | |
import time | |
import os | |
import tempfile | |
#import time | |
import socket | |
import http | |
from threading import Lock | |
# array of services from nso to match origin with | |
services = {} | |
# are needed since since tokens_global | |
# and can_save_token_state are both called during handlers | |
# but keep in mind it is not used or locked outside of handlers | |
tokens_global_mutex = Lock() | |
# stores tokens for each web service | |
tokens_global = {} | |
# is set to True when token state has been changed and can be saved | |
can_save_token_state = False | |
# reload tokens_global from a file | |
token_state_filename = os.path.join(tempfile.gettempdir(), 'nso-reverse-proxy-token-state.json') | |
try: | |
open(token_state_filename, 'x') | |
except FileExistsError: | |
pass | |
try: | |
token_state_file = open(token_state_filename, 'r') | |
except FileNotFoundError: | |
open(token_state_filename, 'x') | |
else: | |
# if the file has content | |
read_test = token_state_file.read(1) | |
token_state_file.seek(0) | |
if read_test: | |
#print('token state loading') | |
tokens_global_read = json.load(token_state_file) | |
print('token state loaded') | |
# only add non-expired entries | |
for a in tokens_global_read.keys(): | |
if tokens_global_read[a][1] > int(time.time()): | |
tokens_global[a] = tokens_global_read[a] | |
#print(tokens_global) | |
services_output = '' | |
try: | |
# first, try seeing if the cached version is defined | |
services_output = tokens_global['services'][0] | |
except KeyError: | |
# if not, then actually use the | |
services_output = subprocess.check_output(['node', 'app/dist/bundle/cli-bundle.js', 'nso', 'webservices', '--json']) | |
# make it expire in 2 weeks | |
tokens_global['services'] = [services_output.decode(), time.time() + 604800] | |
can_save_token_state = True | |
try: | |
services_json = json.loads(services_output) | |
except Exception as e: | |
print('output: ', services_output) | |
raise e | |
with open(token_state_filename, 'w') as token_state_file: | |
json.dump(tokens_global, token_state_file) | |
token_state_file.close() | |
for a in services_json: | |
hostname = a['uri'].rsplit('://')[1].strip('/') | |
services[hostname] = a['id'] | |
#tokens_global = {} | |
def get_token_or_cached(id): | |
id = str(id) | |
print('get_token_or_cached: ' + id) | |
global tokens_global | |
# is the expiry time greater than the current time? | |
#try: | |
# print(tokens_global[id]) | |
#except: | |
# pass | |
if id in tokens_global and (tokens_global[id][1] > int(time.time())): | |
print('retrieving gamewebtoken from cache') | |
# token HAS NOT EXPIRED, return the current one | |
return tokens_global[id][0] | |
else: | |
print('retrieving gamewebtoken from nxapi bundl') | |
# get a new token | |
output = subprocess.check_output(['node', 'app/dist/bundle/cli-bundle.js', 'nso', 'webservicetoken', id, '--json']) | |
try: | |
putput = json.loads(output) | |
except Exception as e: | |
print('output: ', output) | |
raise e | |
# so token[1] is the token, token[2] is the expiry tim | |
future_expiry_time = (int(time.time()) + putput['token']['expiresIn']) | |
global can_save_token_state | |
with tokens_global_mutex: | |
can_save_token_state = True | |
tokens_global[id] = [putput['token']['accessToken'], future_expiry_time] | |
return putput['token']['accessToken'] | |
class RequestHandler(BaseHTTPRequestHandler): | |
def do_GET(self): | |
if self.path == '/_/request_gamewebtoken': | |
print('request_gamewebtoken') | |
#breakpoint() | |
if 'Origin' in self.headers: | |
# use Origin request header without protocol | |
pretty_host_i_think = self.headers['Origin'].split('://')[1] | |
else: | |
# set default to splatnet 3 i guess lmoa | |
pretty_host_i_think = 'api.lp1.av5ja.srv.nintendo.net' | |
token = get_token_or_cached(services[pretty_host_i_think]) | |
self.send_response(200) | |
self.send_header('Access-Control-Allow-Origin', '*') | |
self.send_header('Access-Control-Allow-Private-Network', 'true') | |
self.end_headers() | |
#self.wfile.write(b'seggggse') | |
self.wfile.write(token.encode()) | |
global can_save_token_state | |
if can_save_token_state: | |
print('can_save_token_state is set, saving token state') | |
with tokens_global_mutex: | |
can_save_token_state = False | |
with open(token_state_filename, 'w') as token_state_file: | |
json.dump(tokens_global, token_state_file) | |
token_state_file.close() | |
print('... saved') | |
# prevent browser script from not sending this and having an aneurysm | |
def do_OPTIONS(self): | |
self.send_response(200) | |
self.send_header('Access-Control-Allow-Origin', '*') | |
self.send_header('Access-Control-Allow-Private-Network', 'true') | |
self.end_headers() | |
# listens on systemd socket activation | |
# if you don't know what this is then good just ignore it' | |
class SockInheritHTTPServer(http.server.HTTPServer): | |
def __init__(self, address_info, handler, bind_and_activate=True): | |
# Note that we call it with bind_and_activate = False. | |
http.server.HTTPServer.__init__(self, address_info, handler, | |
bind_and_activate=False) | |
# The socket from systemd needs to be set AFTER calling the parent's | |
# class's constructor, otherwise HTTPServer.__init__() will re-set | |
# self.socket() and the handover won't work. | |
self.socket = socket.fromfd(3, http.server.HTTPServer.address_family, http.server.HTTPServer.socket_type) | |
if bind_and_activate: | |
self.server_activate() | |
# will only listen on systemd socket if LISTEN_FDS is in env | |
sd_listen = 'LISTEN_FDS' in os.environ | |
try: | |
if not sd_listen: | |
server = HTTPServer(('localhost', 36017), RequestHandler) | |
# wait forever for incoming http requests! | |
#server.serve_forever() | |
# the above is already ran later on so | |
else: | |
# The connection/port/host doesn't really matter as we don't allocate the | |
# socket ourselves. Pass it in as localhost:80 | |
server = SockInheritHTTPServer(('localhost', 80), RequestHandler) | |
# code below makes this server close after 40 minutes to save memory | |
# however this is probably not necessary | |
""" | |
# shut down in 40 minutes | |
runtime = 2400 | |
server.timeout = 1 | |
start = time.monotonic() | |
end = start + runtime | |
while time.monotonic() < end: | |
server.handle_request() | |
server.server_close() | |
""" | |
server.serve_forever() | |
except KeyboardInterrupt: | |
server.socket.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
Description=nintendo switch online reverse proxyyyyyy???????? | |
StopWhenUnneeded=true | |
[Service] | |
WorkingDirectory=/home/arian/Downloads/nso reverse proxy | |
#ExecStart=/usr/local/bin/mitmdump --listen-host 127.0.0.1 --listen-port 36018 --modify-body :~s:'<head>':@bruh.js -s bruhx.py | |
ExecStart=/usr/bin/python3 bruhy.py | |
[Install] | |
WantedBy=multi-user.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
Description=nintendo switch online reverse proxy | |
#Requires=nso-reverse-actual.service | |
#After=nso-reverse-actual.service | |
Requires=%p.socket | |
After=%p.socket | |
[Service] | |
#ExecStartPre=/bin/sleep 4 | |
#ExecStart=/lib/systemd/systemd-socket-proxyd 127.0.0.1:36018 --exit-idle-time=40min | |
WorkingDirectory=/home/arian/Downloads/nso reverse proxy | |
ExecStart=/usr/bin/python3 bruhy.py | |
Environment=NXAPI_USER_AGENT="adhoc script by \".arian.\" on discord" | |
[Install] | |
WantedBy=multi-user.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
Description=nintendo switch online reverse proxy | |
[Socket] | |
ListenStream=127.0.0.1:36017 | |
[Install] | |
WantedBy=sockets.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name splatnet 3 gamewebtoken handler | |
// @namespace Violentmonkey Scripts | |
// @match https://api.lp1.av5ja.srv.nintendo.net/* | |
// @grant none | |
// @version 1.0 | |
// @author - | |
// @description 6/22/2023, 12:29:03 AM | |
// ==/UserScript== | |
//document.cookie = '_gtoken=' + gtoken; | |
// game token (x-gamewebtoken) request | |
window.requestGameWebToken = () => { | |
window.console.log('requestgametoken called'); | |
/*setTimeout(() => { | |
window.onGameWebTokenReceive(gtoken); | |
}, 10);*/ | |
fetch('http://localhost:36017/_/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(); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment