191 lines
5.5 KiB
GDScript
191 lines
5.5 KiB
GDScript
extends Node
|
|
|
|
|
|
const GATEWAY_URI: String = "https://newgrounds.io/gateway_v3.php"
|
|
const SESSION_FILE: String = "user://ngio.pr"
|
|
const EMPTY_SESSION: Dictionary = {
|
|
expired = true,
|
|
id = "",
|
|
passport_url = "",
|
|
remember = false,
|
|
user = null,
|
|
}
|
|
|
|
|
|
var app_id: String = "" # app id on newgrounds
|
|
var aes_key := PoolByteArray([]) # AES-128/Base64 encryption key
|
|
var session: Dictionary = EMPTY_SESSION
|
|
var keys_loaded: bool = false # whether id and key have been loaded from ini file
|
|
|
|
var http := HTTPRequest.new() # http request node
|
|
var aes := AESContext.new() # aes encryption
|
|
var rng := RandomNumberGenerator.new()
|
|
|
|
|
|
var _response: Dictionary = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
# load app id from ngio.ini
|
|
var ini = ConfigFile.new()
|
|
var err = ini.load("res://ngio.ini")
|
|
if err != OK:
|
|
push_error("Failed to load ngio data. Will not be able to access scoreboards.")
|
|
return
|
|
app_id = ini.get_value("ngio", "id", "")
|
|
if app_id == "":
|
|
push_error("Failed to load ngio data. Will not be able to access scoreboards.")
|
|
keys_loaded = false
|
|
return
|
|
else:
|
|
keys_loaded = true
|
|
|
|
# attempt to load aes key
|
|
var key = Marshalls.base64_to_raw(ini.get_value("ngio", "key", ""))
|
|
if key.size() == 16:
|
|
aes_key = key
|
|
|
|
# initialize HTTPRequest
|
|
add_child(http)
|
|
http.connect("request_completed", self, "_http_request_completed")
|
|
|
|
# initialize rng for encryption
|
|
rng.randomize()
|
|
|
|
# try to load saved session
|
|
if not yield(load_saved_session(), "completed"):
|
|
yield(start_new_session(), "completed")
|
|
|
|
|
|
## attempts to load a saved newgrounds.io session
|
|
## returns true if the loaded session is valid
|
|
func load_saved_session() -> bool:
|
|
var ini = ConfigFile.new()
|
|
var err = ini.load(SESSION_FILE)
|
|
# fail if can't load ngio.pr
|
|
if err != OK:
|
|
session = EMPTY_SESSION
|
|
session.id = ini.get_value("ngio", "session_id", "")
|
|
#const url = new URL(location.href);
|
|
#this.session_id = url.searchParams.get('ngio_session_id');
|
|
if OS.has_feature('JavaScript'):
|
|
var sp_session = JavaScript.eval("""
|
|
new URL(window.location.href).searchParams.get('ngio_session_id')
|
|
""")
|
|
if sp_session != null:
|
|
session.id = sp_session
|
|
# check session is valid
|
|
var response = yield(request_execute("App.checkSession"), "completed")
|
|
if has_result(response):
|
|
var result = response.result
|
|
if result.data.success and not result.data.session.expired:
|
|
session = result.data.session
|
|
return true
|
|
session = EMPTY_SESSION
|
|
ini.set_value("ngio", "session_id", "")
|
|
ini.save(SESSION_FILE)
|
|
return false
|
|
|
|
|
|
## start new session
|
|
func start_new_session() -> bool:
|
|
var response = yield(request_execute("App.startSession"), "completed")
|
|
if has_result(response):
|
|
var result = response.result
|
|
if result.data.success and not result.data.session.expired:
|
|
session = result.data.session
|
|
return true
|
|
session = EMPTY_SESSION
|
|
return false
|
|
|
|
|
|
## repeatedly checks session until it is logged in or cancelled
|
|
func passport_check() -> bool:
|
|
# attempt for maximum of 5 minutes
|
|
var attempts = 60
|
|
while attempts > 0:
|
|
attempts -= 1
|
|
yield(get_tree().create_timer(5.0), "timeout")
|
|
var response = yield(request_execute("App.checkSession"), "completed")
|
|
if has_result(response):
|
|
var result = response.result
|
|
if result.data.success:
|
|
if result.data.session.user:
|
|
session = result.data.session
|
|
if session.remember:
|
|
var ini = ConfigFile.new()
|
|
ini.set_value("ngio", "session_id", session.id)
|
|
ini.save(SESSION_FILE)
|
|
return true
|
|
else:
|
|
return false
|
|
return false
|
|
|
|
|
|
## checks if a response is valid and succeeded
|
|
func has_result(response: Dictionary) -> bool:
|
|
return "success" in response and response.success
|
|
|
|
|
|
## requests the provided component be executed, do not call async :/
|
|
# may call with either single or multiple components
|
|
func request_execute(component: String, parameters: Dictionary = {}, echo: String = "", encrypt: bool = false) -> Dictionary:
|
|
# build request headers
|
|
var headers = [
|
|
"Content-Type: application/x-www-form-urlencoded",
|
|
]
|
|
# build execute object
|
|
var execute = {
|
|
component = component,
|
|
parameters = parameters,
|
|
echo = echo,
|
|
}
|
|
# use encryption if a valid key is available
|
|
# TODO: implementation messed up in some way. encryption is fine, but
|
|
# formatting is wrong in some way that newgrounds can not understand
|
|
# if encrypt and aes_key.size() == 16:
|
|
if false:
|
|
# convert to bytes
|
|
var data = to_json(execute).to_utf8()
|
|
# pad to 16 bytes alignment
|
|
var padding = PoolByteArray([])
|
|
padding.resize((16 - data.size() % 16) % 16)
|
|
padding.fill(0)
|
|
data.append_array(padding)
|
|
# generate random initialization vector
|
|
var iv = PoolByteArray([])
|
|
iv.resize(16)
|
|
for i in iv.size():
|
|
iv[i] = rng.randi() % 0xFF
|
|
# encrypt data
|
|
aes.start(AESContext.MODE_CBC_ENCRYPT, aes_key, iv)
|
|
var encrypted = aes.update(data)
|
|
aes.finish()
|
|
# compose secure execute object
|
|
execute = {
|
|
secure = Marshalls.raw_to_base64(iv + encrypted)
|
|
}
|
|
# compose request body
|
|
var request = {
|
|
app_id = app_id,
|
|
session_id = session.id,
|
|
execute = execute,
|
|
}
|
|
var body = "input=" + to_json(request).percent_encode()
|
|
# make request
|
|
var err = http.request(GATEWAY_URI, headers, true, HTTPClient.METHOD_POST, body)
|
|
if err != OK:
|
|
push_error("Invalid HTTP request")
|
|
# yield response
|
|
yield(http, "request_completed")
|
|
return _response
|
|
|
|
|
|
## called when the HTTPRequest gets a responce
|
|
func _http_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void:
|
|
if response_code == 200:
|
|
var dict = parse_json(body.get_string_from_utf8())
|
|
if typeof(dict) == TYPE_DICTIONARY:
|
|
_response = dict
|
|
return
|
|
_response = {success = false}
|