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", "") # 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) # 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}