Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ariankordi/60d79c7fb23bb24d11c400b65dc45ece to your computer and use it in GitHub Desktop.
Save ariankordi/60d79c7fb23bb24d11c400b65dc45ece to your computer and use it in GitHub Desktop.
two versions of my "nso reverse proxy" (deranged) (and CURSED)
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.
<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>
#!/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
#!/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()
#!/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()
[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
[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
[Unit]
Description=nintendo switch online reverse proxy
[Socket]
ListenStream=127.0.0.1:36017
[Install]
WantedBy=sockets.target
// ==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