diff --git a/.gitignore b/.gitignore index 8b905e164..7401c2217 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ addons/* dev-libs/ *.sum .vs/ +source/funkin/backend/utils/GameJoltSecurity.hx \ No newline at end of file diff --git a/building/libs.xml b/building/libs.xml index f78849581..9cc20032b 100644 --- a/building/libs.xml +++ b/building/libs.xml @@ -17,6 +17,7 @@ + diff --git a/project.xml b/project.xml index 3bcdd92c2..45a14243c 100644 --- a/project.xml +++ b/project.xml @@ -66,6 +66,9 @@ + + + @@ -131,6 +134,7 @@ + diff --git a/source/funkin/backend/assets/ModsFolder.hx b/source/funkin/backend/assets/ModsFolder.hx index 0e33f999e..e1db271b7 100644 --- a/source/funkin/backend/assets/ModsFolder.hx +++ b/source/funkin/backend/assets/ModsFolder.hx @@ -3,6 +3,7 @@ package funkin.backend.assets; import flixel.util.FlxSignal.FlxTypedSignal; import funkin.backend.system.MainState; import funkin.backend.utils.CoolUtil; +import funkin.backend.utils.GJUtil; import haxe.ds.StringMap; import haxe.io.Path; import lime.text.Font; @@ -75,9 +76,17 @@ class ModsFolder { } public static function reloadMods() { - if (!__firstTime) + if (!__firstTime) { + #if GAMEJOLT_API + if (GJUtil.active) + GJUtil.logout(); + #end + FlxG.switchState(new MainState()); + } __firstTime = false; + + } /** diff --git a/source/funkin/backend/system/Flags.hx b/source/funkin/backend/system/Flags.hx index 2e61eec20..f2a6cef70 100644 --- a/source/funkin/backend/system/Flags.hx +++ b/source/funkin/backend/system/Flags.hx @@ -28,6 +28,7 @@ class Flags { public static var MOD_NAME:String = ""; public static var MOD_DESCRIPTION:String = ""; public static var MOD_AUTHOR:String = ""; + public static var MOD_VERSION:String = ""; @:lazy public static var MOD_API_VERSION:Null = null; public static var MOD_DOWNLOAD_LINK:String = ""; public static var MOD_DEPENDENCIES:Array = []; @@ -43,6 +44,11 @@ class Flags { public static var MOD_REDIRECT_STATES:Map = []; + @:also(funkin.backend.utils.GameJoltSecurity.gameId) + public static var MOD_GAMEJOLT_GAME_ID:String = ''; + @:also(funkin.backend.utils.GameJoltSecurity.encryptedGameToken) + public static var MOD_GAMEJOLT_TOKEN:String = ''; + // -- Codename's Default Flags -- @:lazy public static var SAVE_PATH:String = haxe.macro.Compiler.getDefine("SAVE_PATH"); @:lazy public static var SAVE_NAME:String = haxe.macro.Compiler.getDefine("SAVE_NAME"); diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 14988dddc..205cebcaa 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -10,6 +10,7 @@ import funkin.backend.assets.ModsFolderLibrary; import funkin.backend.assets.ZipFolderLibrary; import funkin.backend.chart.EventsData; import funkin.backend.system.framerate.Framerate; +import funkin.backend.utils.GJUtil; import funkin.editors.ModConfigWarning; import funkin.menus.TitleState; import haxe.io.Path; @@ -162,6 +163,13 @@ class MainState extends FlxState { if (cast(lib, ZipFolderLibrary).PRELOAD_VIDEOS) cast(lib, ZipFolderLibrary).precacheVideos(); } + #if GAMEJOLT_API + if (FlxG.save.data.gameJoltArray != null) { + var gjDat:Array = FlxG.save.data.gameJoltArray; + GJUtil.attemptLogin(gjDat[0], gjDat[1]); + } + #end + var startState:Class = Flags.DISABLE_WARNING_SCREEN ? TitleState : funkin.menus.WarningState; // In this case if the mod we just loaded a compressed modpack, we can't edit or modify files without decompressing it. diff --git a/source/funkin/backend/utils/GJUtil.hx b/source/funkin/backend/utils/GJUtil.hx new file mode 100644 index 000000000..cc8ee81e3 --- /dev/null +++ b/source/funkin/backend/utils/GJUtil.hx @@ -0,0 +1,321 @@ +package funkin.backend.utils; + +/** + * This is how GameJolt API responses are formatted like. + */ +typedef Response = { + // General + success:Bool, + ?message:String, + // User Fetching + ?users:Array, + // Trophies Fetching + ?trophies:Array, + // Scores Fetching + ?scores:Array, + ?tables:Array, + ?rank:Int, + // Friends Fetching + ?friends:Array<{friend_id:Int}>, + // Data Store Fetching + ?keys:Array<{key:String}>, + ?data:String, + // Time Fetching + ?timestamp:Int, + ?timezone:String, + ?year:Int, + ?month:Int, + ?day:Int, + ?hour:Int, + ?minute:Int, + ?second:Int, + // Batch Reception + ?responses:Array +} + +/** + * The way the scores are fetched from your game API. + * + * @param score The display text of the Score. + * @param sort The Score value. + * @param extra_data If some extra data is attached to this Score, it'll be shown here. + * @param user The username of the User who achieved this Score, if it's a registered User. + * @param user_id The user ID of the User who achieved this Score, if it's a registered User. + * @param guest The name of the user who achieved this Score, if it's a guest user. + * @param stored A short description about when the Score was achieved by the User or Guest. + * @param stored_timestamp A long time stamp (in seconds) of when the Score was achieved by the User or Guest. + */ +typedef Score = { + score:String, + sort:Int, + extra_data:String, + user:String, + user_id:Int, + guest:String, + stored:String, + stored_timestamp:Int +} + +/** + * The way the score tables are fetched from your game API. + * + * @param id The ID of the Score Table. + * @param name The name of the Score Table. + * @param description The description of the Score Table. + * @param primary Whether if this is the Primary Score Table in your game (1) or not (0). + */ +typedef ScoreTable = { + id:Int, + name:String, + description:String, + primary:Bool +} + +/** + * The way the trophies are fetched from your game API. + * + * @param id The ID of the Trophy. + * @param title The title of the Trophy. + * @param description The description of the Trophy. + * @param difficulty The difficulty rank of the Trophy. + * @param image_url The link of the image that represents the Trophy. + * @param achieved Whether this Trophy was achieved or not, it can be a string if it was (with info about how much time ago it was achieved) or bool if not (false). + */ +typedef Trophy = { + id:Int, + title:String, + description:String, + difficulty:String, + image_url:String, + achieved:String +} + +/** + * The way the user data is fetched from the GameJolt API. + * + * @param id The ID of the User. + * @param type The cathegory the User is cataloged like in GameJolt. + * @param username The username of the User. (Also available for guests). + * @param avatar_url The link of the avatar of the User. + * @param signed_up A short description about how long the User have been in GameJolt. + * @param signed_up_timestamp A long time stamp (in seconds) of when the User signed up. + * @param last_logged_in A short description about the last time the User was found active in GameJolt. + * @param last_logged_in_timestamp A long time stamp (in seconds) of the last time the User logged in GameJolt. + * @param status The actual status of the User. + * @param developer_name The display name of the User. (Also available for guests). + * @param developer_website The website of the User. + * @param developer_description The description of the User. + */ +typedef User = { + id:Int, + type:String, + username:String, + avatar_url:String, + signed_up:String, + signed_up_timestamp:Int, + last_logged_in:String, + last_logged_in_timestamp:Int, + status:String, + developer_name:String, + developer_website:String, + developer_description:String +} + +/** + * An enum class to clasify Data Store update functions. + */ +enum DataUpdateType { + Add(n:Int); + Substract(n:Int); + Multiply(n:Int); + Divide(n:Int); + Append(t:String); + Prepend(t:String); +} + +/** + * An enum of every single command currently available to request to GameJolt API. + */ +enum RequestType { + BATCH(parallel:Bool, breakOnError:Bool, requests:Array); + DATA_FETCH(key:String, fromUser:Bool); + DATA_GETKEYS(fromUser:Bool, ?pattern:String); + DATA_REMOVE(key:String, fromUser:Bool); + DATA_SET(key:String, data:String, toUser:Bool); + DATA_UPDATE(key:String, operation:DataUpdateType, toUser:Bool); + FRIENDS; + TIME; + USER_AUTH; + USER_FETCH(userOrID:String); + SESSION_OPEN; + SESSION_PING(active:Bool); + SESSION_CHECK; + SESSION_CLOSE; + SCORES_ADD(score:String, sort:Int, ?extra_data:String, ?table_id:Int); + SCORES_GETRANK(sort:Int, ?table_id:Int); + SCORES_FETCH(fromUser:Bool, ?table_id:Int, ?limit:Int, ?betterThan:Int); + SCORES_TABLES; + TROPHIES_FETCH(?achieved:Bool, ?trophy_id:Int); + TROPHIES_ADD(trophy_id:Int); + TROPHIES_REMOVE(trophy_id:Int); +} + +/** + * GameJolt utility to help with GameJolt functionality. Use this class to determine if your player is logged into GameJolt. + * Will not do anything if there is no provided GameJolt token. + * + * # IMPORTANT + * If you wish to use this utility, please run your GameJolt game's security code through the Codename Engine + * encryption tool on Codename's website. + * Place the output of that into your modpack.ini under the flag `MOD_GAMEJOLT_TOKEN`. + * + * ## DO NOT PLACE YOUR SECURITY KEY RIGHT INTO THE MODPACK.INI!!!! THAT IS A SECURITY ISSUE!!!! + */ +class GJUtil +{ + /** + * Boolean to determine if our player logged in. + */ + public static var loggedIn:Bool = false; + + /** + * The username of the logged in user. + */ + public static var userName(default, set):String; + + /** + * Whether or not the GameJolt utility is operational. + * This cannot be set other than load operations. + */ + public static var active(default, null):Bool = false; + + /** + * Whether or not the utility is executing a call. + */ + static var executing:Bool = false; + + /** + * Helper function in case the session is lost in the middle of the game. + */ + public static var onLostSession:NullVoid> = null; + + /** + * Helper function to simplify the login process. + * @param name Username of user attempting to login. + * @param token User token of user attempting to login. + * @return Bool Whether the attempt was successfull or not. + */ + public static function attemptLogin(name:String, token:String):Bool + { + if(Flags.MOD_GAMEJOLT_GAME_ID != '' && Flags.MOD_GAMEJOLT_TOKEN != '') + active = true + else + return false; + + var ret:Bool = false; + userName = name; + GameJoltSecurity.user_token = token; + send(RequestType.SESSION_OPEN, false, function(err) { + userName = null; + GameJoltSecurity.user_token = null; + }, function(resp) { + trace('GameJolt logged in as ${userName}'); + ret = true; + openfl.Lib.application.onExit.add(onExitApp); + FlxG.signals.postUpdate.add(pingTimer); + FlxG.save.data.gameJoltArray = [userName, token]; + FlxG.save.flush(); + }); + return ret; + } + + static function onExitApp(i:Int) + { + logout(); + } + + static var pingTime:Int = 0; + public static function pingTimer() + { + pingTime += 1; + if (pingTime < 10000) return; + pingTime -= 10000; + pingSession(); + } + + public static function pingSession() + { + send(RequestType.SESSION_PING(true), true, (str) -> { + trace('GameJolt session lost.'); + if (onLostSession != null) onLostSession(); + shutdownFunctions(); + active = false; + }); + } + + public static function logout() + { + if (!active) + return; + + shutdownFunctions(); + send(RequestType.SESSION_CLOSE, false, null, function(resp) { + trace('GameJolt account ${userName} logged out successfully.'); + userName = null; + GameJoltSecurity.user_token = null; + }); + } + + static function shutdownFunctions() + { + FlxG.signals.postUpdate.remove(pingTimer); + openfl.Lib.application.onExit.remove(onExitApp); + onLostSession = null; + } + + public static function send(call:RequestType, async:Bool = false, ?onError:String->Void, ?onComplete:Response->Void, ?onProgress:Array->Void) + { + if (executing || !active) + return; + executing = true; + + @:privateAccess + var resp:Response = GameJoltSecurity.handleRequest(async, call, onProgress); + executing = false; + if (resp.message != null && onError != null) + onError(resp.message); + else if (resp.message == null && onComplete != null) + onComplete(formatImages(resp)); + } + + static function formatImages(res:Response):Response { + if (res.users != null) + for (u in res.users) u.avatar_url = '${u.avatar_url.substring(0, 32)}1000${u.avatar_url.substr(34)}'.replace(".jpg", ".png") + .replace(".webp", ".png"); + if (res.trophies != null) for (t in res.trophies) { + var newUrl:String = ""; + if (t.image_url.startsWith('https://m.')) + newUrl = '${t.image_url.substring(0, 37)}1000${t.image_url.substr(40)}'.replace(".jpg", ".png").replace(".webp", ".png"); + else { + newUrl = "https://s.gjcdn.net/assets/"; + newUrl += switch (t.image_url.substring(24).replace(".jpg", "").replace(".webp", "")) { + case "trophy-bronze-1": "9c2c91d0"; + case "trophy-silver-1": "b46e352e"; + case "trophy-gold-1": "363ce2dc"; + case "trophy-platinum-1": "92e5330d"; + default: ""; + }; + newUrl += ".png"; + } + t.image_url = newUrl; + }; + if (res.responses != null) for (res2 in res.responses) res2 = formatImages(res2); + return res; + } + + static function set_userName(name:String):String + { + loggedIn = (name != null && name != ''); + return userName = name; + } +} \ No newline at end of file diff --git a/source/funkin/backend/utils/GameJoltSecurityPublic.hx b/source/funkin/backend/utils/GameJoltSecurityPublic.hx new file mode 100644 index 000000000..3a51d6f1f --- /dev/null +++ b/source/funkin/backend/utils/GameJoltSecurityPublic.hx @@ -0,0 +1,341 @@ +package funkin.backend.utils; + +import hscript.IHScriptCustomBehaviour; +import funkin.backend.utils.GJUtil; +import funkin.backend.utils.GJUtil.RequestType; +import haxe.crypto.Md5; +import funkin.backend.utils.GJUtil.*; +import haxe.Http; +import haxe.Json; +import openfl.events.*; + +/** + * # A BIG MOTHERFUCKING WARNING + * + * This class handles the raw game keys for GameJolt keys. + * If the raw game keys are made public, people can mess with leaderboards, data, achievements, + * or whatever else is on the game page. + * + * As such, Codename Engine requires players to encrypt keys, and the method of + * encryption is non-disclosable for security reasons, so the original file + * used in distributed CNE builds cannot be shared. + * Instead, we provide this public-facing file for hardcoding purposes. + * + * To use this file, make a copy and rename the copy's filename and class name to + * `GameJoltSecurity.hx`. To modify the encryption method, go to the function + * `set_encryptedGameToken` and modify the code in the first half of the null + * check (`if (tok != null) {}`). + * + * **WE HIGHLY ENCOURAGE YOU TO COME UP WITH AN ENCRYPTION METHOD FOR GAMEJOLT KEYS.** + * HaxeFoundation's crypto package is installed with Codename, you can see the methods + * you can use (as well as the documentation) [here](https://github.com/HaxeFoundation/crypto). + * + * # ***DO NOT LET YOUR PLAYERS PUT THE RAW KEYS IN ANY SOFTCODED FILES!!! WE ARE NOT*** + * ***RESPONSIBLE IF YOU DON'T MAKE YOUR PLAYERS ENCRYPT THEIR KEYS AND THEIR STUFF*** + * ***GETS HACKED!!!!*** + * + * Also for security purposes, this class is unattainable via HScript. + * + * ~ SplatterDash + */ +@:noCustomClass +@:dox(hide) +abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour +{ + /** + * Token for the user if they're logged in. + */ + public static var user_token:String = ''; + + /** + * ID number for the current mod. + */ + public static var gameId:String = ''; + + /** + * The encrypted game token. Set using GAMEJOLT_ENCRYPTED_TOKEN in ini file. + */ + public static var encryptedGameToken(default, set):String; + + /** + * The unencrypted game token. It's insanely hard to get this variable. + */ + @:noPrivateAccess static var revealedGameToken:String; + + /** + * URL sent to GameJolt per request. + */ + @:noPrivateAccess static var url(get, never):String; + + /** + * The previous response created by the API client. Usually for just storage purposes. + */ + static var lastResponse:Response = {success: false, message: "No response yet."}; + + /** + * The current call being processed. + */ + static var curCall:Null = null; + + // hscript - thanks LJ :D + public function hget(name:String):Dynamic + { + return null; + } + + public function hset(name:String, val:Dynamic):Dynamic + { + return null; + } + + static function get_url():String + { + return sign('https://api.gamejolt.com/api/game/v1_2${parseType(curCall)}'); + } + + static function handleRequest(async:Bool = false, data:RequestType, ?onProgress:Array->Void):Response + { + if (encryptedGameToken == null || gameId == null) { + lastResponse = {success: false, message: 'Missing game token and/or game ID.'}; + curCall = null; + return lastResponse; + } + + curCall = data; + + if (async) { + var loader = new openfl.net.URLLoader(); + loader.addEventListener(Event.COMPLETE, function(complete) { + lastResponse = Json.parse(cast(loader.data, String)).response; + if (lastResponse.message != null) { + trace('Response Error: ${lastResponse.message}'); + } + + }); + loader.addEventListener(ProgressEvent.PROGRESS, progress -> { if (onProgress != null) onProgress([progress.bytesLoaded, progress.bytesTotal]);}); + loader.addEventListener(IOErrorEvent.IO_ERROR, function(ioError) { + lastResponse = {success: false, message: 'IO Error: ${ioError.text}'}; + }); + loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, (securityError) -> { + lastResponse = {success: false, message: 'Security Error: ${securityError.text}'}; + }); + loader.load(new openfl.net.URLRequest(url)); + return {success: false, message: "No response yet."}; + } else { + var loader:Http = new Http(url); + loader.onData = function(data) { + lastResponse = Json.parse(data).response; + if (lastResponse.message != null) + trace('Response Error: ${lastResponse.message}'); + }; + loader.onError = function(error) { + lastResponse = {success: false, message: 'Request Error: ${error}'}; + }; + loader.request(false); + } + curCall = null; + return lastResponse; + } + + static function parseType(request:RequestType, signed:Bool = false):String { + var command:String = ""; + var action:String = ""; + var params:Array<{name:String, value:String}> = []; + + switch (request) { + case BATCH(parallel, breakOnError, requests): + command = "batch"; + params.push({name: "parallel", value: '$parallel'}); + params.push({name: "break_on_error", value: '$breakOnError'}); + for (req in requests) params.push({name: "requests[]", value: parseType(req, true)}); + case DATA_FETCH(key, fromUser): + command = "data-store"; + params.push({name: "key", value: key.urlEncode()}); + if (fromUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_GETKEYS(fromUser, pattern): + command = "data-store"; + action = "get-keys"; + if (pattern != null && pattern != "") + params.push({name: "pattern", value: pattern.urlEncode()}); + if (fromUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_REMOVE(key, fromUser): + command = "data-store"; + action = "remove"; + params.push({name: "key", value: key.urlEncode()}); + if (fromUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_SET(key, data, toUser): + command = "data-store"; + action = "set"; + params.push({name: "key", value: key.urlEncode()}); + params.push({name: "data", value: data.urlEncode()}); + if (toUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_UPDATE(key, operation, toUser): + command = "data-store"; + action = "update"; + params.push({name: "key", value: key.urlEncode()}); + if (toUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + switch (operation) { + case Add(n): + params.push({name: 'operation', value: 'add'}); + params.push({name: 'value', value: '$n'}); + case Substract(n): + params.push({name: 'operation', value: 'substract'}); + params.push({name: 'value', value: '$n'}); + case Multiply(n): + params.push({name: 'operation', value: 'multiply'}); + params.push({name: 'value', value: '$n'}); + case Divide(n): + params.push({name: 'operation', value: 'divide'}); + params.push({name: 'value', value: '$n'}); + case Append(t): + params.push({name: 'operation', value: 'append'}); + params.push({name: 'value', value: t.urlEncode()}); + case Prepend(t): + params.push({name: 'operation', value: 'prepend'}); + params.push({name: 'value', value: t.urlEncode()}); + } + case FRIENDS: + command = "friends"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case TIME: + command = "time"; + case USER_AUTH: + command = "users"; + action = "auth"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case USER_FETCH(userOrID): + command = "users"; + var letters:Array = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ_-".split(""); + if (letters.filter(l -> userOrID.contains(l.toUpperCase()) || userOrID.contains(l.toLowerCase())).length > 0) + params.push({name: "username", value: userOrID}); + else + params.push({name: "user_id", value: userOrID.replace(",", "%2C")}); + case SESSION_OPEN: + command = "sessions"; + action = "open"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SESSION_PING(active): + command = "sessions"; + action = "ping"; + params.push({name: "status", value: active ? "active" : "idle"}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SESSION_CHECK: + command = "sessions"; + action = "check"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SESSION_CLOSE: + command = "sessions"; + action = "close"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SCORES_ADD(score, sort, extra_data, table_id): + command = "scores"; + action = "add"; + params.push({name: "score", value: score}); + params.push({name: "sort", value: '$sort'}); + if (extra_data != null && extra_data != "") + params.push({name: "extra_data", value: extra_data.urlEncode()}); + if (table_id != null) + params.push({name: "table_id", value: '$table_id'}); + if (user_token != "") { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } else + params.push({name: "guest", value: GJUtil.userName}); + case SCORES_GETRANK(sort, table_id): + command = "scores"; + action = "get-rank"; + params.push({name: "sort", value: '$sort'}); + if (table_id != null) + params.push({name: "table_id", value: '$table_id'}); + case SCORES_FETCH(fromUser, table_id, limit, betterThan): + command = "scores"; + if (table_id != null) + params.push({name: "table_id", value: '$table_id'}); + if (limit != null) + params.push({name: "limit", value: '$limit'}); + if (betterThan != null) + params.push({name: betterThan < 0 ? "worse_than" : "better_than", value: '${Math.abs(betterThan)}'}); + if (fromUser) { + if (user_token != "") { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } else + params.push({name: "guest", value: GJUtil.userName}); + } + case SCORES_TABLES: + command = "scores"; + action = "tables"; + case TROPHIES_FETCH(achieved, trophy_id): + command = "trophies"; + if (achieved != null) + params.push({name: "achieved", value: '$achieved'}); + if (trophy_id != null) + params.push({name: "trophy_id", value: '$trophy_id'}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case TROPHIES_ADD(trophy_id): + command = "trophies"; + action = "add-achieved"; + params.push({name: "trophy_id", value: '$trophy_id'}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case TROPHIES_REMOVE(trophy_id): + command = "trophies"; + action = "remove-achieved"; + params.push({name: "trophy_id", value: '$trophy_id'}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + + var urlSection:String = '/$command${action != "" ? '/$action' : ""}?game_id=${gameId}${[for (p in params) '&${p.name}=${p.value}'].join("")}'; + if (signed) + urlSection = sign(urlSection).urlEncode(); + return urlSection; + } + + /** + * Setter function for encrypted game token. Also sets revealed game token. + * @param tok + */ + static function set_encryptedGameToken(tok:String) + { + if (tok != null) { + // Encryption method goes here. + revealedGameToken = tok; + } else { + revealedGameToken = null; + } + return encryptedGameToken = tok; + } + + /** + * Signs a piece of URL with Md5. + * @param daUrl The old URL piece. + * @return The new URL piece. + */ + static function sign(daUrl:String):String { + var urlToEncode:String = daUrl + revealedGameToken; + return '$daUrl&signature=${Md5.encode(urlToEncode)}'; + } +} \ No newline at end of file diff --git a/source/hscript/Config.hx b/source/hscript/Config.hx index 3acd554b1..7c8e03f10 100644 --- a/source/hscript/Config.hx +++ b/source/hscript/Config.hx @@ -29,7 +29,7 @@ class Config { // Incase any of your files fail // These are the module names public static final DISALLOW_CUSTOM_CLASSES = [ - + "funkin.backend.utils.GameJoltSecurity", // don't want people getting those gamejolt keys! ]; public static final DISALLOW_ABSTRACT_AND_ENUM = [