GameMaker Online Highscores

Appsurd

Member
First of all, a happy new year to you all! The GMS2 version is probably going to take a while, since I have been really busying doing stuff for myself.

Thank you for this guide, it's great that 3 years later you're still providing feedback and keeping this thread updated. I've spent the last half hour reading through all of the comments/feedback, what a great morning read. I can't wait to sit down and toy around with this leader board system!
You're welcome! Awesome, didn't expect anyone to read all of them :p

Nobody talks here about GDPR: General Data Protection Regulation. How can it be integrated with the online high score system?
"Holy open ended questions, Batman!"

1. Perhaps trim down the scope of your question to something manageable
2. Start a different topic instead of as part of a comment chain that has not much to do with gdpr
3. Be grateful that gms games no longer sends telemetry data to yoyogames, over which you would have had no control over what is sent or how it is managed
In general, you should apply one rule:
NEVER save people's email adresses, home locations, bank account, etc. until you consider it to be ABSOLUTELY NECESSARY.
If you apply this rule, you will be safe in general, because the GDPR is about personal data, and someone's highscore or username is not part of it. I would even argue that you apply this rule without having to stick to the GDPR, because you are keeping someone else's property which you also need to protect. Finally, just as @chamaeleon said, please open another topic if you would like to hear other people's opinion about GDPR compliance. My tutorial is at least perfectly safe to use (in my opinion).

EDIT: I added this answer to the FAQ in the tutorial
 
Last edited:

Appsurd

Member
Changes:

Minor update V1.1.8:
- Thanks to a user, added a clearification about using a VPN while creating your Altervista account
 

Appsurd

Member
Major update 1.2.3:

- Completely rewrote the tutorial for GMS2
- Renamed some variables for the sake of clarity
- Various minor changes to the sample project and tutorial text
- Added a new paid tutorial: Online Highscore Premium Edition, now available from $20 for just $10.
 

kloac

Member
Hi! how are you?

I 've done and read all the tutorial, but i'm having an issue that i can't resolve. On some Android devices (especially new ones, with the last android system), the highscore table stays charging with the message "Charging highscore table", like they're without internet (but they're with internet).They can't send score to the data base and of course they can't read the high score table either.
I don't know if it's a code problem from GMS, something from Altervista or Android.

I'm using GMS2 (the last version).

Hopefully you can help me with this!
Thanks You!!
 

FrostyCat

Redemption Seeker
On some Android devices (especially new ones, with the last android system), the highscore table stays charging with the message "Charging highscore table", like they're without internet (but they're with internet).They can't send score to the data base and of course they can't read the high score table either.
I don't know if it's a code problem from GMS, something from Altervista or Android.
Android Pie and above block plain HTTP traffic by default. Either you change your Altervista setup to HTTPS, or you add this to Option > Android > Permissions > Inject to Android Application Tag:
Code:
android:usesCleartextTraffic="true"
Do note that the latter approach will disqualify you from Google Play if the submitted information is personally identifiable. Using HTTPS instead of HTTP should always be prioritized, especially for projects done in a production-level capacity.
 

kloac

Member
@FrostyCat Thank You! I can do it :p

I'm having another issue.. how can i get a variable from the Altervista table, and then save it in a Ini file? For example an ID? :)

Thanks you!!
 

FrostyCat

Redemption Seeker
I'm having another issue.. how can i get a variable from the Altervista table, and then save it in a Ini file? For example an ID? :)
This tells me you have not genuinely read the tutorial, only blindly copied from it. Read it again, and actually think things over this time.

The tutorial has clear instructions on how to read values from the database on the PHP side, and how to request and parse them on the GML side. You are perfectly capable of saving yourself in this, you just chose not to. The only difference in your case is that you're trying to get an ID instead of a score (i.e. $row['ID'] instead of $row['score'] on the PHP side), and an additional write to an INI file (which isn't even related to this topic, go look up how to use INI files separately).
 
Z

zicman

Guest
Hi,

Thx so much for the tutorial but there are things i dont understand.
My php SQL skill is veerrry low but when you get a date, you use GET so, in the tutorial :

Why
if($secret_key == $_POST['secret_key'])
and not
if($secret_key == $_GET['secret_key'])


and after we have
$name = $_POST['name'];
$score = $_POST['score'];

and why not
$name = $_GET['name'];
$score = $_GET['score'];


Thx a lot
 

FrostyCat

Redemption Seeker
Hi,

Thx so much for the tutorial but there are things i dont understand.
My php SQL skill is veerrry low but when you get a date, you use GET so, in the tutorial :

Why
if($secret_key == $_POST['secret_key'])
and not
if($secret_key == $_GET['secret_key'])


and after we have
$name = $_POST['name'];
$score = $_POST['score'];

and why not
$name = $_GET['name'];
$score = $_GET['score'];


Thx a lot
Read up: Idempotence
Safe methods are HTTP methods that do not modify resources. For instance, using GET or HEAD on a resource URL, should NEVER change the resource.
Submitting a new score to be saved on a server changes the targeted resource, that means using GET is inappropriate. Reading a date from a server does not change the targeted resource, that means using GET is fine.
 
Z

zicman

Guest
I try to understand ... I maybe need to read it many times ;)

I only can say, if i write (with the good secret key of course) :
if($secret_key == $_POST['secret_key']) => the condition is wrong
When i change by _GET the condition is true (andwrong of course witha bad secret key)

But i dont try trought Gamemaker, i write the adress on chrome (with the good information instead of ***)
http://ftp.***.altervista.org/OnlineHighscores/addscore.php?name=toto&score=666&secret_key=1234
Maybe im wrong and i must work with Gamemaker ....
 
Last edited by a moderator:

FrostyCat

Redemption Seeker
I try to understand ... I maybe need to read it many times ;)

I only can say, if i write (with the good secret key of course) :
if($secret_key == $_POST['secret_key']) => the condition is wrong
When i change by _GET the condition is true (andwrong of course witha bad secret key)

But i dont try trought Gamemaker, i write the adress on chrome (with the good information instead of ***)
http://ftp.***.altervista.org/OnlineHighscores/addscore.php?name=toto&score=666&secret_key=1234
Maybe im wrong and i must work with Gamemaker ....
When you type parameters into the URL like that, it is a GET request, and the parameters end up in $_GET. This is one of the first things you learn when handling requests in PHP.

This is also why if you are learning HTTP APIs, you need a request tester like Postman or cURL (gURL if you prefer a GUI). A browser's address bar is an inadequate substitute.
 
Z

zicman

Guest
Ok I "understand" why my result are not conform.
I thought it was a good idea to try with a browser, unfortunatly it's not the case.
thx
 

Binsk

Member
All scripts and code should work on all platforms, except HTML5. I am not completely aware what the real problem is, but it’s due to connection problems between the server where your game is hosted and the Altervista server. You might solve your problems by placing the following line in ALL your PHP files at line 2 (just after the <?php ):
I had this issue with a game of mine. From what I understand a special check known as a "pre-flight" check is performed before sending data when talking between different servers (I'm still new to this aspect so, grain of salt here).

The pre-flight request method is always "OPTIONS" where I then have to tell what kind of connections are allowed. This is where I did Access-Control-Allow-Origin: * much like specified above but I also had to specify what kind of other request methods were permitted as well as what kinds of headers.

Since the preflight and the actual request are two separate requests I would exit out of the PHP script at the end of the preflight and not process the request until the real thing came.

My code looked something like this:
Code:
<?php
   header("Access-Control-Allow-Origin: *");
   header("Access-Control-Max-Age: 60");
   if ($_SERVER['REQUEST_METHOD'] === "OPTIONS"){
      header("Access-Control-Allow-Methods: POST, OPTIONS");
      header("Access-Control-Allow-Headers: Authorization, Content-Type, Accept, Origin, cache-control"); // This is specific to you
      http_response_code(200);
      die;
   }

   // All the actual handling of input starts here:
If anyone has more info on this that would be great as I kind of stumbled through this solution when I got things working initially. I understand what it all does but I'm not 100% sure on how much of it is required.
 

Appsurd

Member
@Binsk Thanks for your explanation, I'm sure that it could be helpful to others. Unfortunately, I don't know much myself about this, so I can't answer your question.
 
Hi - thank you for this tutorial. I have been trying to copy the code and get a database working on my own website. My question is, with your display.php is it intended that it will print the table directly on the internet page or is it only for GameMaker to receive the data?
 

FrostyCat

Redemption Seeker
Hi - thank you for this tutorial. I have been trying to copy the code and get a database working on my own website. My question is, with your display.php is it intended that it will print the table directly on the internet page or is it only for GameMaker to receive the data?
It displays on an internet page, like what all HTTP APIs do.

Browsers work over HTTP. GMS 2 has functions for working over the exact same protocol. So do request testers like Curl and Postman.
 
Hi - I would be grateful for some help. I have an online scoreboard, but it updates perfectly if the http_post_string command is sent from a Windows Desktop version of GameMaker. However, it does not work from the HTML5 version.

My question is, is it possible to send hi-scores to an online database from a program running on HTML5?

Thank you for any comments!
 

FrostyCat

Redemption Seeker
Hi - I would be grateful for some help. I have an online scoreboard, but it updates perfectly if the http_post_string command is sent from a Windows Desktop version of GameMaker. However, it does not work from the HTML5 version.

My question is, is it possible to send hi-scores to an online database from a program running on HTML5?
Upload your game to the same domain as the API, then it will submit properly. This is the same-origin constraint on browser-side JS, which the Manual clearly warns about:
The Manual said:
NOTE: You should be aware that due to XSS protection in browsers, requests to and attempts to load resources from across domains are blocked and may appear to return blank results. Please see the section on Cross Domain Issues for further details.
Alternatively, implement preflight handling on the PHP side as Binsk described earlier on.
 

FrostyCat

Redemption Seeker
The API in the context of this tutorial is the set of PHP scripts that together define how you access the underlying database.

If you uploaded the PHP files to Altervista, then upload your game to the same Altervista Account.
 
Hi - I have been trying to get GameMaker to send scores into my own database, but have had no success! I am using my own site at Protonhosting and not Altervista.

I have no problems in being able to insert, select and print out data from my database. However, my issue is that I have not been able to collect the name and score sent from GameMaker.

So my question is, does anyone have some code that will collect the name and score of data sent from GameMaker (using http:_post_string) or does anyone have any theories why I cannot collect the data??

Thank you for any comments!
Alec Armstrong
 
Last edited:

chamaeleon

Member
Hi - I have been trying to get GameMaker to send scores into my own database, but have had no success! I am using my own site at Protonhosting and not Altervista.

I have no problems in being able to insert, select and print out data from my database. However, my issue is that I have not been able to collect the name and score sent from GameMaker.

So my question is, does anyone have some code that will collect the name and score of data sent from GameMaker (using http:_post_string) or does anyone have any theories why I cannot collect the data??

Thank you for any comments!
Alec Armstrong
Are you using the some of the PHP code from this thread unaltered? I think it would be beneficial for you to ensure you can transmit a successful request using curl towards the PHP API you have on your server. You can also create a new temporary PHP containing
PHP:
<?php
phpinfo();
And display/save the result from the http request (instead of using the normal url for actually storing a score), and examine it for relevant data about parameters.
 
Hi - thank you for your help! I tried a curl and sent a sample name and score to my AddNew.php. AddNew.php had no problems at all receiving and processing these dummy variables.

However, the same cannot be said for receiving a name and score from my GameMaker program! What the database is doing is storing blank values because presumably, GameMaker has called AddNew.php so the commands to input values into the database automatically happen. This suggests that GameMaker is not sending out the right information?

In GameMaker I had been using the following to try and input the values:

HighScoreName="test111";
scoreplayer=55;
http_post_string("mywebsiteaddress/AddNew.php?&id=1" +"&name="+string(HighScoreName,)+"&score="+string(scoreplayer),"");

EDIT - however, after checking this I found it needed to be:
HighScoreName="test111";
scoreplayer=55;
str = "name="+string(HighScoreName,)+"&score="+string(scoreplayer);

http_post_string("mywebsiteaddrss/OnlineHighScores/AddNew.php?",str);

So I am glad to report I have managed to import scores into my own website and can print out the results very nicely including on HTML5!
 
Last edited:

Caio

Member
<b>Fatal error</b>: Uncaught PDOException: SQLSTATE[HY000] [1044] Access denied for user.....
what happened?
 

chamaeleon

Member
<b>Fatal error</b>: Uncaught PDOException: SQLSTATE[HY000] [1044] Access denied for user.....
what happened?
Your database code in PHP is not using a user that has permission to connect to the specified database. What options you have to correct this depends on your hosting solution, I imagine. But essentially it comes to granting sufficient privileges to a database account to perform the actions you require.
 

Caio

Member
Your database code in PHP is not using a user that has permission to connect to the specified database. What options you have to correct this depends on your hosting solution, I imagine. But essentially it comes to granting sufficient privileges to a database account to perform the actions you require.
i use ProtonHosting
 

Caio

Member
Your database code in PHP is not using a user that has permission to connect to the specified database. What options you have to correct this depends on your hosting solution, I imagine. But essentially it comes to granting sufficient privileges to a database account to perform the actions you require.
I gave permission. now it works.
However it is not recording the results
 

Appsurd

Member
I gave permission. now it works.
However it is not recording the results
What do you mean by "is not recording the results" ? Does the tutorial show an empty highscores list? I guess you mean that after you have submitted, then the highscore list is left unchanged. Please send me a PM with your PHP code and GMS scripts/functions, and I'm happy to help you out
 

MCHLV

Member
Hello,


First of all, thank you very much for this great tutorial. This is very detailed and I have seen from the history that it has been continuously improved.
I had really no issue following it to the end. I just could not see any of the images but this was clear enough for me to know what to do.

It is working but if others have the same issue: when I ran it I could not get it working... "Please check your internet connection..." stayed displayed.
Long story short, using HTTPS in GMS http_post_string() did the trick

Thanks again

M.
 
Last edited:

Appsurd

Member
@MCHLV thank you very much for your kind words! I really appreciate it.

@FORUM Does anyone know why the images have disappeared? On my end, in Chrome they are gone but are visible as normal in Edge & Firefox
 
S

Sunfish

Guest
Hi! I've used your scripts for some minor projects with my friends over the last few years and they were fantastic!

I've ran into a very odd issue however, and I don't really know what happened. I've gotten back into making another project, and followed the tutorial once again, but for some reason my players' names are showing up as garbled letters? At first I thought I followed something wrong, and redid the tutorial a second time only for it to happen again. I then checked on my old project from about 2-3 years ago and was surprised to find the names were also garbled there! It's very odd since I have not touched that project ever since then, and it was working the last time I worked on it.

Do you happen to know where I may have messed up? I don't think I misread anything, and made sure not to deviate from any of your code. I'm currently using GMS 1.4 if that helps!

Thank you!

Edit:

Spoke too soon. Realized I forgot to decode the names!
 

Appsurd

Member
Hi! I've used your scripts for some minor projects with my friends over the last few years and they were fantastic!

I've ran into a very odd issue however, and I don't really know what happened. I've gotten back into making another project, and followed the tutorial once again, but for some reason my players' names are showing up as garbled letters? At first I thought I followed something wrong, and redid the tutorial a second time only for it to happen again. I then checked on my old project from about 2-3 years ago and was surprised to find the names were also garbled there! It's very odd since I have not touched that project ever since then, and it was working the last time I worked on it.

Do you happen to know where I may have messed up? I don't think I misread anything, and made sure not to deviate from any of your code. I'm currently using GMS 1.4 if that helps!

Thank you!

Edit:

Spoke too soon. Realized I forgot to decode the names!
No problem, I'm glad that you resolved the problem!
 
C

CROi

Guest
Hello,

Amazing tutorial, thank you so much !
It seems to be running well on my HTML5 game except the player names do not appear in the highscore board.
Would you know what could be the problem ?

Thank you !

EDIT : I finally figured out the problem was that my table row in altervista was called "Name" and not "Name".... Thank you so much for the help and for this great tutorial !
 
Last edited by a moderator:

dudesaydude

Member
Hey there, after carefully reading through the comments on the thread and double checking my project (Only made small adjustments) I noticed that for me it seems to upload the scores very inconsistently. I always get return 1 from scr_send_score, but sometimes my score is not added to the database. As far as I'm aware there are no checks or any reason as to why this should happen and it seems almost random. So despite always returning 1 the database doesn't always add the new score. Our score is actually a time, not a number, and its organized from smallest to largest when displaying the scores (which is working fine). Trying to debug for more information but as far as I can tell everything is doing what it is supposed to be doing, apart from sometimes it will add a score to the database, sometimes it wont, regardless if the scripts all return their correct values. I know this is a kind of vague question but Im happy to consider any suggestions at this point.
 

Appsurd

Member
Hey there, after carefully reading through the comments on the thread and double checking my project (Only made small adjustments) I noticed that for me it seems to upload the scores very inconsistently. I always get return 1 from scr_send_score, but sometimes my score is not added to the database. As far as I'm aware there are no checks or any reason as to why this should happen and it seems almost random. So despite always returning 1 the database doesn't always add the new score. Our score is actually a time, not a number, and its organized from smallest to largest when displaying the scores (which is working fine). Trying to debug for more information but as far as I can tell everything is doing what it is supposed to be doing, apart from sometimes it will add a score to the database, sometimes it wont, regardless if the scripts all return their correct values. I know this is a kind of vague question but Im happy to consider any suggestions at this point.
Hi thanks for your question!
So your "score" is actually a time. Is the time in seconds since epoch? Or is it something like 23:03 ?
I ask because the tutorial uses the INT format in the database for the score, which you probably need to change to STR. If you require more info, please send me a PM.
 

Lolloggo

Member
Hallo Appsurd and community!

Thank you very much for this tutorial and the whole very informative thread!

I have read the thread multiple times, specially the parts about the replay attack vulnerability.
And i think it is not the problem that someone cheats and makes himself a score of 1 million. The bigger problem is
that you can insert 1 million scores with a few lines of code and mess up the whole leaderboard...

I have carefully read what FrostyCat and others have mentioned about improving security with a token like system.

So i have tried to make one that is in my opinion save and not that hard to implement in your setup.

It works like that:

1. Before the score is added to the database, the client (GM) requests a enciphered token from the sever.
2. The server creates a random token, and enciphers the token with a Vigenere style cipher (with key_A) and saves the
unciphered token in a database.
3. Then GM deciphers the received token with key_A. And enciphers the token again with key_B (the keys A and B need to be different
and should have the same lenght as the created token for best vigenere security, maybe 16-20 and random with no similar characters).
Then the enciphered token gets url encoded and GM sends it to the server (to your addscore.php as the secret_key).
4. The server deciphers the token with key_B and checks if the token is in the DB, if so, the score will be added in the highscore database
and the used token will be deleted.

I know that the Vigenere cipher is not the state of the art security, but for this purpose it is ok i think.

Ok, so first we need to make a new table, i named it TOKEN with 3 rows like the DB for the highscores.

Code:
#    Name    Typ                  Standard                Extra
1    id      int(11)                                      AUTO_INCREMENT
2    token   varchar(100)
3    time    timestamp            current_timestamp()
I made the third row, because we can alternatively delete rows that are older than 1 minute or whatever.
This is optional, but we have the row and we can decide later.

On the Server we need one additional PHP file for generating the token...
I called it token.php:
PHP:
<?php
/// Random key generator
/**
* Generate a random string, using a cryptographically secure
* pseudorandom number generator (random_int)
*
* This function uses type hints now (PHP 7+ only), but it was originally
* written for PHP 5 as well.
*
* For PHP 7, random_int is a PHP core function
* For PHP 5.x, depends on https://github.com/paragonie/random_compat
*
* @param int $length      How many characters do we want?
* @param string $keyspace A string of all possible characters
*                         to select from
* @return string
*/
function random_str(
    int $length = 64,
    string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
): string {
    if ($length < 1) {
        throw new \RangeException("Length must be a positive integer");
    }
    $pieces = [];
    $max = mb_strlen($keyspace, '8bit') - 1;
    for ($i = 0; $i < $length; ++$i) {
        $pieces []= $keyspace[random_int(0, $max)];
    }
    return implode('', $pieces);
}

/// Vigenere style cipher
// vigenere_ascii(in,key,mode)
//
//  Returns the given string enciphered or deciphered
//  using a simple Vigenere style cipher, and filtering
//  out non-printable characters.
//
//      in          input, string
//      key         enciphering key, string
//      mode        0 = decipher, 1 = encipher
//
// GMLscripts.com/license
function vigenere_ascii($in, $key, $mode)
{
    $out = "";
    $inLen = strlen($in);
    $keyLen = strlen($key);
    $loVal = 32;
    $hiVal = 126;
    $span = ($hiVal - $loVal) + 1;
    for ($pos=0; $pos<$inLen; $pos+=1) {
        $inChar = substr($in, $pos, 1);
        $keyChar = substr($key, $pos % $keyLen, 1);
        $inVal = min(max($loVal, ord($inChar)), $hiVal) - $loVal;
        $keyVal = min(max($loVal, ord($keyChar)), $hiVal) - $loVal;
        if ($mode) {
            $outVal = (($inVal + $keyVal) % $span) + $loVal;
        }else{
            $outVal = (($span + $inVal - $keyVal) % $span) + $loVal;
        }
        $outChar = chr($outVal);
        $out = $out . $outChar;
    }
    return $out;
}


// Generate a random token
$token = random_str(10);

// Key A for the Vigenere cipher
$key_A = "cipherA";

// Encipher the token
$enciphered_token = vigenere_ascii($token, $key_A, 0);

// Connect to database
$db = new PDO('mysql:host=localhost;dbname=yourname', 'user','pass');
// Prepare statement
$statement = $db->prepare("INSERT INTO TOKEN (token) VALUES (?)");
// Safe the token in the DB
$statement->execute(array($token));

echo $enciphered_token;

Your addscore.php needs to be modified, i called it addscore_token.php:
PHP:
<?php
/// Vigenere style cipher
// vigenere_ascii(in,key,mode)
//
//  Returns the given string enciphered or deciphered
//  using a simple Vigenere style cipher, and filtering
//  out non-printable characters.
//
//      in          input, string
//      key         enciphering key, string
//      mode        0 = decipher, 1 = encipher
//
// GMLscripts.com/license
function vigenere_ascii($in, $key, $mode)
{
    $out = "";
    $inLen = strlen($in);
    $keyLen = strlen($key);
    $loVal = 32;
    $hiVal = 126;
    $span = ($hiVal - $loVal) + 1;
    for ($pos=0; $pos<$inLen; $pos+=1) {
        $inChar = substr($in, $pos, 1);
        $keyChar = substr($key, $pos % $keyLen, 1);
        $inVal = min(max($loVal, ord($inChar)), $hiVal) - $loVal;
        $keyVal = min(max($loVal, ord($keyChar)), $hiVal) - $loVal;
        if ($mode) {
            $outVal = (($inVal + $keyVal) % $span) + $loVal;
        }else{
            $outVal = (($span + $inVal - $keyVal) % $span) + $loVal;
        }
        $outChar = chr($outVal);
        $out = $out . $outChar;
    }
    return $out;
}


// Connect to database
$db = new PDO('mysql:host=localhost;dbname=yourname', 'user','pass');

// The token
$token = $_POST['token'];

// Key B for the Vigenere cipher
$key_B = "cipherB";

// Decipher the token
$deciphered_token = vigenere_ascii($token, $key_B, 1);

// Search for token in DB
$sql = "SELECT * FROM TOKEN WHERE token = '$deciphered_token'";
$search = $db->query($sql)->fetch();
$db_token = $search['token'];

// If token exists in DB, then insert name and score
if($db_token){
    // Prepare statement
    $sql = "INSERT INTO yourDB VALUES (NULL, :name, :score)";
    $stmt = $db->prepare($sql);
    $stmt->bindParam(':name', $name, PDO::PARAM_STR);
    $stmt->bindParam(':score', $score, PDO::PARAM_INT);

    // Get name and score from URL string
    $name = $_POST['name'];
    $score = $_POST['score'];

    // Execute statement
    $stmt->execute();
  
    // Delete token from DB
    $db->query("DELETE FROM TOKEN WHERE token = '$deciphered_token'");
  
    echo '1';
}
else
{
    echo '0';
}
// Delete tokens older than 1 minute from DB (optional)
//$db->query("DELETE FROM TOKEN WHERE time < DATE_SUB(now(), interval 1 Minute)"); //maybe 30 SECOND ?
?>
Thats all for the server side. Now we make some changes in GMS.

First insert the vigenere_ascii script to the script folder. (Thanks to xot)
GML:
/// vigenere_ascii(in,key,mode)
//
//  Returns the given string enciphered or deciphered
//  using a simple Vigenere style cipher, and filtering
//  out non-printable characters.
//
//      in          input, string
//      key         enciphering key, string
//      mode        0 = decipher, 1 = encipher
//
/// GMLscripts.com/license
{
    var in, key, mode, out;
    in = argument0;
    key = argument1;
    mode = argument2;
    out = "";
    var inLen, keyLen, pos, inChar, keyChar, outChar;
    var inVal, keyVal, outVal, loVal, hiVal, span;
    inLen = string_length(in);
    keyLen = string_length(key);
    loVal = 32;//org 32
    //loVal = 97;
    hiVal = 126;//org 126
    //hiVal = 122;
    span = (hiVal - loVal) + 1;
    for (pos=0; pos<inLen; pos+=1) {
        inChar = string_char_at(in, pos+1);
        keyChar = string_char_at(key, (pos mod keyLen)+1);
        inVal = min(max(loVal, ord(inChar)), hiVal) - loVal;
        keyVal = min(max(loVal, ord(keyChar)), hiVal) - loVal;
        if (mode) {
            outVal = ((inVal + keyVal) mod span) + loVal;
        }else{
            outVal = ((span + inVal - keyVal) mod span) + loVal;
        }
        outChar = chr(outVal);
        out = out + outChar;
    }
    return out;
}
Then i have changed your scr_send_score to scr_send_score_token:
GML:
/// @description scr_send_score_token(name,score,token)
/// @param name        name of the player
/// @param score    the achieved score
/// @param token    the token (enciphered and url_encoded)
//
// Script:      Sends the player’s score to the database in Altervista
// Date:        2020-01-18
// Copyright:   Appsurd
// Edited by:    Lolloggo

var name = url_encode(base64_encode(string(argument0)));
var args = "name="+name+"&score="+string(argument1)+"&token="+string(argument2);
http_post_string("https://yourhosting/path/addscore_token.php", args);
Remove your obj_send from the room, and make an object called obj_send_token,
it gets created in the async - http event in the obj_highscore:
GML:
if (ds_map_find_value(async_load, "id") == get_highscores)
{
    if (ds_map_find_value(async_load, "status") == 0)
    {
        text2 = string(ds_map_find_value(async_load, "result"));
      
        instance_create_layer(x,y,"Instances_Depth_0",obj_send_token); // create the obj_send_token
      
        if (text2 == "IOException" or text2 == "")
        {
            text1 = "Please check your internet connection...";
            text2 = "";
        }
        else
        {
            text1 = "Ready";
            alarm[1] = -1;
        }
    }
}
The obj_send_token looks like this:
create:
GML:
get_token = 0
token = 0;
decipher = 0;
encipher = 0;
url_token = 0;

key_A = "cipherA"; // same as in token.php
key_B = "cipherB"; // same as in addscore_token.php
alarm 1:
GML:
room_restart();
draw:
GML:
draw_set_colour(c_lime);
draw_rectangle(400,10,520,30,false);
draw_set_colour(c_white);
draw_set_halign(fa_middle);
draw_set_valign(fa_middle);
draw_text(460,20,"Send score");
draw_set_valign(fa_top);
global left released:
GML:
if point_in_rectangle(mouse_x,mouse_y,400,10,520,30)
{
  
    get_token = http_get("https://yourhosting/path/token.php");
  
}
async - http:
GML:
if ds_map_find_value(async_load, "id") == get_token
{
    if ds_map_find_value(async_load, "status") == 0
    {
        token = ds_map_find_value(async_load, "result");
        show_debug_message(token);
      
        decipher = vigenere_ascii(token,key_A,1);
        show_debug_message(decipher);
      
        encipher = vigenere_ascii(decipher,key_B,0);
        show_debug_message(encipher);
      
        url_token = url_encode(encipher);
        show_debug_message(url_token);
      
        show_debug_message("-------------");
              
        scr_send_score_token("Guest", "1250",url_token);

        alarm[1] = room_speed;
      
    }
    else
    {
        url_token = 0;
    }
}

Thats it. Its just for testing purposes. But i think you get what i mean.
For me it is the first time that i have done things in SQL, and without your tutorial it wont be possible.
I hope that i have not forgotten anything and that it will work for you.
Dont be to harsh in your commments because i am very new to it.
Thank you again for getting me into it.

I forgot to say that i have tested this in GMS2 with Windows as target.

Maybe that will be useful for you or others.

Please tell me what you think of it.

Regards

Lolloggo
 
Last edited:

Appsurd

Member
Hallo Appsurd and community!

Thank you very much for this tutorial and the whole very informative thread!

I have read the thread multiple times, specially the parts about the replay attack vulnerability.
And i think it is not the problem that someone cheats and makes himself a score of 1 million. The bigger problem is
that you can insert 1 million scores with a few lines of code and mess up the whole leaderboard...

I have carefully read what FrostyCat and others have mentioned about improving security with a token like system.

So i have tried to make one that is in my opinion save and not that hard to implement in your setup.

It works like that:

1. Before the score is added to the database, the client (GM) requests a enciphered token from the sever.
2. The server creates a random token, and enciphers the token with a Vigenere style cipher (with key_A) and saves the
unciphered token in a database.
3. Then GM deciphers the received token with key_A. And enciphers the token again with key_B (the keys A and B need to be different
and should have the same lenght as the created token for best vigenere security, maybe 16-20 and random with no similar characters).
Then the enciphered token gets url encoded and GM sends it to the server (to your addscore.php as the secret_key).
4. The server deciphers the token with key_B and checks if the token is in the DB, if so, the score will be added in the highscore database
and the used token will be deleted.

I know that the Vigenere cipher is not the state of the art security, but for this purpose it is ok i think.

Ok, so first we need to make a new table, i named it TOKEN with 3 rows like the DB for the highscores.

Code:
#    Name    Typ                  Standard                Extra
1    id      int(11)                                      AUTO_INCREMENT
2    token   varchar(100)
3    time    timestamp            current_timestamp()
I made the third row, because we can alternatively delete rows that are older than 1 minute or whatever.
This is optional, but we have the row and we can decide later.

On the Server we need one additional PHP file for generating the token...
I called it token.php:
PHP:
<?php
/// Random key generator
/**
* Generate a random string, using a cryptographically secure
* pseudorandom number generator (random_int)
*
* This function uses type hints now (PHP 7+ only), but it was originally
* written for PHP 5 as well.
*
* For PHP 7, random_int is a PHP core function
* For PHP 5.x, depends on https://github.com/paragonie/random_compat
*
* @param int $length      How many characters do we want?
* @param string $keyspace A string of all possible characters
*                         to select from
* @return string
*/
function random_str(
    int $length = 64,
    string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
): string {
    if ($length < 1) {
        throw new \RangeException("Length must be a positive integer");
    }
    $pieces = [];
    $max = mb_strlen($keyspace, '8bit') - 1;
    for ($i = 0; $i < $length; ++$i) {
        $pieces []= $keyspace[random_int(0, $max)];
    }
    return implode('', $pieces);
}

/// Vigenere style cipher
// vigenere_ascii(in,key,mode)
//
//  Returns the given string enciphered or deciphered
//  using a simple Vigenere style cipher, and filtering
//  out non-printable characters.
//
//      in          input, string
//      key         enciphering key, string
//      mode        0 = decipher, 1 = encipher
//
// GMLscripts.com/license
function vigenere_ascii($in, $key, $mode)
{
    $out = "";
    $inLen = strlen($in);
    $keyLen = strlen($key);
    $loVal = 32;
    $hiVal = 126;
    $span = ($hiVal - $loVal) + 1;
    for ($pos=0; $pos<$inLen; $pos+=1) {
        $inChar = substr($in, $pos, 1);
        $keyChar = substr($key, $pos % $keyLen, 1);
        $inVal = min(max($loVal, ord($inChar)), $hiVal) - $loVal;
        $keyVal = min(max($loVal, ord($keyChar)), $hiVal) - $loVal;
        if ($mode) {
            $outVal = (($inVal + $keyVal) % $span) + $loVal;
        }else{
            $outVal = (($span + $inVal - $keyVal) % $span) + $loVal;
        }
        $outChar = chr($outVal);
        $out = $out . $outChar;
    }
    return $out;
}


// Generate a random token
$token = random_str(10);

// Key A for the Vigenere cipher
$key_A = "cipherA";

// Encipher the token
$enciphered_token = vigenere_ascii($token, $key_A, 0);

// Connect to database
$db = new PDO('mysql:host=localhost;dbname=yourname', 'user','pass');
// Prepare statement
$statement = $db->prepare("INSERT INTO TOKEN (token) VALUES (?)");
// Safe the token in the DB
$statement->execute(array($token));

echo $enciphered_token;

Your addscore.php needs to be modified, i called it addscore_token.php:
PHP:
<?php
/// Vigenere style cipher
// vigenere_ascii(in,key,mode)
//
//  Returns the given string enciphered or deciphered
//  using a simple Vigenere style cipher, and filtering
//  out non-printable characters.
//
//      in          input, string
//      key         enciphering key, string
//      mode        0 = decipher, 1 = encipher
//
// GMLscripts.com/license
function vigenere_ascii($in, $key, $mode)
{
    $out = "";
    $inLen = strlen($in);
    $keyLen = strlen($key);
    $loVal = 32;
    $hiVal = 126;
    $span = ($hiVal - $loVal) + 1;
    for ($pos=0; $pos<$inLen; $pos+=1) {
        $inChar = substr($in, $pos, 1);
        $keyChar = substr($key, $pos % $keyLen, 1);
        $inVal = min(max($loVal, ord($inChar)), $hiVal) - $loVal;
        $keyVal = min(max($loVal, ord($keyChar)), $hiVal) - $loVal;
        if ($mode) {
            $outVal = (($inVal + $keyVal) % $span) + $loVal;
        }else{
            $outVal = (($span + $inVal - $keyVal) % $span) + $loVal;
        }
        $outChar = chr($outVal);
        $out = $out . $outChar;
    }
    return $out;
}


// Connect to database
$db = new PDO('mysql:host=localhost;dbname=yourname', 'user','pass');

// The token
$token = $_POST['token'];

// Key B for the Vigenere cipher
$key_B = "cipherB";

// Decipher the token
$deciphered_token = vigenere_ascii($token, $key_B, 1);

// Search for token in DB
$sql = "SELECT * FROM TOKEN WHERE token = '$deciphered_token'";
$search = $db->query($sql)->fetch();
$db_token = $search['token'];

// If token exists in DB, then insert name and score
if($db_token){
    // Prepare statement
    $sql = "INSERT INTO QUERSENKEN VALUES (NULL, :name, :score)";
    $stmt = $db->prepare($sql);
    $stmt->bindParam(':name', $name, PDO::PARAM_STR);
    $stmt->bindParam(':score', $score, PDO::PARAM_INT);

    // Get name and score from URL string
    $name = $_POST['name'];
    $score = $_POST['score'];

    // Execute statement
    $stmt->execute();
   
    // Delete token from DB
    $db->query("DELETE FROM TOKEN WHERE token = '$deciphered_token'");
   
    echo '1';
}
else
{
    echo '0';
}
// Delete tokens older than 1 minute from DB (optional)
//$db->query("DELETE FROM TOKEN WHERE time < DATE_SUB(now(), interval 1 Minute)"); //maybe 30 SECOND ?
?>
Thats all for the server side. Now we make some changes in GMS.

First insert the vigenere_ascii script to the script folder. (Thanks to xot)
GML:
/// vigenere_ascii(in,key,mode)
//
//  Returns the given string enciphered or deciphered
//  using a simple Vigenere style cipher, and filtering
//  out non-printable characters.
//
//      in          input, string
//      key         enciphering key, string
//      mode        0 = decipher, 1 = encipher
//
/// GMLscripts.com/license
{
    var in, key, mode, out;
    in = argument0;
    key = argument1;
    mode = argument2;
    out = "";
    var inLen, keyLen, pos, inChar, keyChar, outChar;
    var inVal, keyVal, outVal, loVal, hiVal, span;
    inLen = string_length(in);
    keyLen = string_length(key);
    loVal = 32;//org 32
    //loVal = 97;
    hiVal = 126;//org 126
    //hiVal = 122;
    span = (hiVal - loVal) + 1;
    for (pos=0; pos<inLen; pos+=1) {
        inChar = string_char_at(in, pos+1);
        keyChar = string_char_at(key, (pos mod keyLen)+1);
        inVal = min(max(loVal, ord(inChar)), hiVal) - loVal;
        keyVal = min(max(loVal, ord(keyChar)), hiVal) - loVal;
        if (mode) {
            outVal = ((inVal + keyVal) mod span) + loVal;
        }else{
            outVal = ((span + inVal - keyVal) mod span) + loVal;
        }
        outChar = chr(outVal);
        out = out + outChar;
    }
    return out;
}
Then i have changed your scr_send_score to scr_send_score_token:
GML:
/// @description scr_send_score_token(name,score,token)
/// @param name        name of the player
/// @param score    the achieved score
/// @param token    the token (enciphered and url_encoded)
//
// Script:      Sends the player’s score to the database in Altervista
// Date:        2020-01-18
// Copyright:   Appsurd
// Edited by:    Lolloggo

var name = url_encode(base64_encode(string(argument0)));
var args = "name="+name+"&score="+string(argument1)+"&token="+string(argument2);
http_post_string("https://yourhosting/path/addscore_token.php", args);
Remove your obj_send from the room, and make an object called obj_send_token,
it gets created in the async - http event in the obj_highscore:
GML:
if (ds_map_find_value(async_load, "id") == get_highscores)
{
    if (ds_map_find_value(async_load, "status") == 0)
    {
        text2 = string(ds_map_find_value(async_load, "result"));
       
        instance_create_layer(x,y,"Instances_Depth_0",obj_send_token); // create the obj_send_token
       
        if (text2 == "IOException" or text2 == "")
        {
            text1 = "Please check your internet connection...";
            text2 = "";
        }
        else
        {
            text1 = "Ready";
            alarm[1] = -1;
        }
    }
}
The obj_send_token looks like this:
create:
GML:
get_token = 0
token = 0;
decipher = 0;
encipher = 0;
url_token = 0;

key_A = "cipherA"; // same as in token.php
key_B = "cipherB"; // same as in addscore_token.php
alarm 1:
GML:
room_restart();
draw:
GML:
draw_set_colour(c_lime);
draw_rectangle(400,10,520,30,false);
draw_set_colour(c_white);
draw_set_halign(fa_middle);
draw_set_valign(fa_middle);
draw_text(460,20,"Send score");
draw_set_valign(fa_top);
global left released:
GML:
if point_in_rectangle(mouse_x,mouse_y,400,10,520,30)
{
   
    get_token = http_get("https://yourhosting/path/token.php");
   
}
async - http:
GML:
if ds_map_find_value(async_load, "id") == get_token
{
    if ds_map_find_value(async_load, "status") == 0
    {
        token = ds_map_find_value(async_load, "result");
        show_debug_message(token);
       
        decipher = vigenere_ascii(token,key_A,1);
        show_debug_message(decipher);
       
        encipher = vigenere_ascii(decipher,key_B,0);
        show_debug_message(encipher);
       
        url_token = url_encode(encipher);
        show_debug_message(url_token);
       
        show_debug_message("-------------");
               
        scr_send_score_token("Guest", "1250",url_token);

        alarm[1] = room_speed;
       
    }
    else
    {
        url_token = 0;
    }
}

Thats it. Its just for testing purposes. But i think you get what i mean.
For me it is the first time that i have done things in SQL, and without your tutorial it wont be possible.
I hope that i have not forgotten anything and that it will work for you.
Dont be to harsh in your commments because i am very new to it.
Thank you again for getting me into it.

I forgot to say that i have tested this in GMS2 with Windows as target.

Maybe that will be useful for you or others.

Please tell me what you think of it.

Regards

Lolloggo
Lolloggo, thanks for your contribution to this tutorial! I really appreciate that you took so much time to look into safe server communication. There's, however, a few considerations that I want to point out.

This tutorial is a beginner's tutorial about online highscores and introduces some basic communications with your webserver. I tried to explain everything as simple as possible, and the focus is rather on the explanation rather than the implementation. Please don't use this tutorial to build advanced Login systems, because this tutorial is not intended for processing user-sensitive data such as names, email addresses, home address, financial stuff, etc. If you really like to use safe communications, you should definitely have a look at this website explaining PHP frameworks. These framework are basically "extensions" of PHP and come with lots of useful functions. So if you really want to continue with advanced/secure PHP, please use a PHP framework for both security and faster development.

Second, as you already mentioned, the Vigenere cipher is far from "secure". Yes, it requires a bit of work to decipher Vigenere, but it's still waaay too easy to break in. Again, like I said, please consider using a PHP framework if you're really looking for real security. I believe most PHP frameworks use either RSA or Elliptic curves, which are the current state-of-the-art encryption methods for browser communication. PS: if you click on the "lock" icon in HTTPS websites in the browser and you click through some of the settings, you can actually see which encryption method is used. Apparently, Yoyogames uses RSA.

Since this is a beginner's tutorial, I don't believe your scripts should be added to the tutorial, because it would only distract from the real message. Nevertheless, other people may find your code useful, but, and I want want to emphasise this again: Don't use any personal data and send it with this communication method. Use a PHP framework instead!
 

Lolloggo

Member
Thanks for your feedback Appsurd.

Your considerations about my "security-like" extension to your scripts are absolutely correct and it was never my intention
to reinvent the wheel for a realy secure server communication.

You are absolutely right to say:

Don't use any personal or sensitive data and send it with the communication method i have mentioned.

Everyone must know that. I am sorry that i have forgotten to state this in my post and it is good that you pointed that out.

My methode is only supposed to give this communication (a highscore table with a player name and a score) a kind of "security".
And keep it as simple as possible.

And when someone invests the time and work to get the vigenere key just for cheating,
than he is suposed to be the #1 in the highscore. :)


Regards

Lolloggo
 

Appsurd

Member
Thanks for your feedback Appsurd.

Your considerations about my "security-like" extension to your scripts are absolutely correct and it was never my intention
to reinvent the wheel for a realy secure server communication.

You are absolutely right to say:

Don't use any personal or sensitive data and send it with the communication method i have mentioned.

Everyone must know that. I am sorry that i have forgotten to state this in my post and it is good that you pointed that out.

My methode is only supposed to give this communication (a highscore table with a player name and a score) a kind of "security".
And keep it as simple as possible.

And when someone invests the time and work to get the vigenere key just for cheating,
than he is suposed to be the #1 in the highscore. :)


Regards

Lolloggo
Thanks for the input Lolloggo, really appreciated!
 

neochilds

Member
Thank you for the assist several email strings deep. Doing some modifications to make the premium work in my game.
It took some tinkering but with the updates you made it works without a hitch. I was also able to port the premium code over to 1.4 with some slight changes but over all the code you created is solid as a rock when I have it working as intended I will write my review for the premium.
- It was brutal trying to figure out why it did not work prior to your update
Damn Gm updates.
 

Attachments

AetherBones

Member
Pretty cool project man, very similar to the first build of gmscoreboard.com I've since rebuilt gmscoreboard using nodejs but yeah. This was a fun read for me!
 

cadobr

Member
Olá pessoal. Primeiramente parabéns pelo tutorial. Segundo, gostaria de saber se vocês recomendam outro servidor. O Altervista está dando erro e não me deixa criar conta, esse problema vem ocorrendo desde o final do ano passado. Pode me ajudar. Eu adoraria criar um banco de dados para meus jogos seguindo este tutorial. Sou residente aqui no Brasil, e não sei se está acontecendo apenas aqui no meu país. Obrigado antecipadamente pela ajuda.

Captura de tela 2022-03-23 163051-01.jpg
 

Attachments

Last edited:

Appsurd

Member
Hello everybody. First congratulations for the tutorial. Second, I would like to know if you guys recommend another server. Altervista is giving an error and won't let me create an account, this problem has been occurring since the end of last year. Can you help me. I would love to create a database for my games by following this tutorial. I'm a resident here in Brazil, and I don't know if it's happening only here in my country. Thanks in advance for the help.

My first advice; Never share your personal name and email ;)

On my end (Europe) I am able to create an account as usual. Are you using a VPN? Have you tried disabling it and then creating an account? Otherwise, have you tried to use a VPN to create your account?
Otherwise I don't really know what would work... You can look around for other services like these, but I don't know any that are as good/flexible as Altervista.
Another possibility is to buy your own domain and hosting, so you can go from there. But obviously that costs money...
 
Last edited:

cadobr

Member
Hello.
Thank you very much for your reply but contacting the Altavista team, they do not have support in Brazil so I would have to look for another service.
Thanks a lot for the tip and answer.
 
Top