This is a read only copy without any forum functionality of the old Modcraft forum.
If there is anything that you would like to have removed, message me on Discord via Kaev#5208.
Big thanks to Alastor for making this copy!

Menu

Author Topic: [QUESTION] Account Messaging  (Read 4540 times)

stoneharry

  • Contributors
  • Creator of Worlds
  • *****
  • Posts: 617
    • View Profile
[QUESTION] Account Messaging
« on: July 30, 2012, 03:47:23 pm »

Account Messaging


[paragraph:1dmoi0by]In this post I will be explaining the processes I took to achieve this desired implementation, but at the end I will draw up some questions that I am asking the community so that we can solve some flaws with the system together.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]This entire thread is designed for the 3.3.5a WoW client, but it should work on any other client as well.[/paragraph:1dmoi0by]

Explanation


[paragraph:1dmoi0by]The World of Warcraft client has had account messaging implemented for a very long time, but never actually used by Blizzard.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]Account messaging is the process of being able to send unique messages to certain accounts upon successfully authentication with the server. Messages can be clicked away or marked as read, and when marked as read they will no longer show up on login. Multiple messages can be queued at once.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]This is demonstrated in the following video: [/paragraph:1dmoi0by]

Implementation


[paragraph:1dmoi0by]The main logical steps of account messages are as follows:
  • Player logs in
  • Server challenges player
  • Player challenges server
  • Server response has a flag that needs to be marked, to show that the client should check for account messages
  • Client goes to URL defined in strings to retrieve XML code to be displayed as message
  • Client displays message if retrieved successfully, other buttons can call different URLs to mark it read (etc)
  • When client visits URL, it parses a hash of its session key, the account name, and the message ID. The servers and clients session keys should match in theory.
[/paragraph:1dmoi0by]

The Client Changes


[paragraph:1dmoi0by]There is only one file that needs to be changed to get the client working with account messages.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]Find the gluestrings.lua file in the GlueXML folder of the interface. Note that changes to the glueXML files will not work unless the WoW executable is 'cracked'.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]There are some strings that need to be changed:[/paragraph:1dmoi0by]
Code: [Select]
ACCOUNT_MESSAGE_BODY_NO_READ_URL = "http://www.blizzard.com/getMessageBodyUnread.xml";
ACCOUNT_MESSAGE_BODY_URL = "http://www.blizzard.com/getMessageBody.xml";
ACCOUNT_MESSAGE_HEADERS_URL = "http://www.blizzard.com/getMessageHeaders.xml";
ACCOUNT_MESSAGE_READ_URL = "http://www.blizzard.com/markMessageAsRead.xml";
[paragraph:1dmoi0by]Note that the file extension makes no difference, as long as the text is formatted correctly.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]These need to be changed to the URL of your website so that it executes your scripts and not Blizzard's. I found that changes to the gluestrings.lua here was not always successful since apparently there is another file somewhere else that overwrites these strings (I forget where). To counter this, I set these strings again just before the client checks for account messages. This can be found on line 241 of GlueParent.lua.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]I set ACCOUNT_MESSAGE_BODY_URL to nil as I never see it used, and no error is ever spat it saying that it is nil when something tries to reference it, so I assume it was never implemented.[/paragraph:1dmoi0by]

The Emulator Changes


[paragraph:1dmoi0by]In this document I am describing the changes relevant to ArcEmu. But obviously, TrinityCore uses exactly the same packets. It should be a simple matter to implement it in TrinityCore as well, or any other emulator for that matter.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]The function to take note of is in AuthSocket.cpp in the logonserver:[/paragraph:1dmoi0by]
Code: [Select]
void AuthSocket::SendProofError(uint8 Error, uint8 * M2)
{
  uint8 buffer[32];
  memset(buffer, 0, 32);

  buffer[0] = 1;
  buffer[1] = Error;
  if(M2 == 0)
  {
    *(uint32*)&buffer[2] = 3;

    Send(buffer, 6);
    return;
  }

  memcpy(&buffer[2], M2, 20);
    buffer[22]= 0x01; //<-- ARENA TOURNAMENT ACC FLAG!
  buffer[30]= 1; // Unread messages
  Send(buffer, 32);
}
[paragraph:1dmoi0by]You can see that I flag that there are unread messages here:[/paragraph:1dmoi0by]
Code: [Select]
buffer[30]= 1; // Unread messages[paragraph:1dmoi0by]Since the session key is generated upon login, account messages need to be updated with the most up to date session key so that when the client uploads their hashed session key, it is the same as the servers.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]The following algorithm is wrong. This is the hash that Schlumpf thought was correct, however upon testing we found it generated a completely different result to the client. The correct hash needs to be figured out.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]However, in:
Code: [Select]
void AuthSocket::HandleProof()Of the AuthSocket.cpp, near the end:
Code: [Select]
m_account->SetSessionKey(m_sessionkey.AsByteArray());[/paragraph:1dmoi0by]

[paragraph:1dmoi0by]This sets the account to have that session key. Immediately after, I do the following to get a hashed session key and store it:[/paragraph:1dmoi0by]
Code: [Select]
 // Disabled because the server algorithm seems to be unable to generate the same result as the client.

  // Hash the session key so that the logon DB can know
  static const unsigned short hash_constant (0x7a0b);
  Sha1Hash newone;
  newone.UpdateBigNumbers (&m_sessionkey, NULL);
  newone.UpdateData ((uint8*)&hash_constant, sizeof (hash_constant));
  newone.Finalize();
  BigNumber hashed_session_key;
  hashed_session_key.SetBinary (newone.GetDigest(), newone.GetLength());

  // Let the DB know
  sLogonSQL->Execute("UPDATE `account_messages` SET `hash` = '%s' WHERE `account` = '%s';", hashed_session_key.AsDecStr(), m_account->UsernamePtr);
[paragraph:1dmoi0by]This is the first flaw with the current implementation.[/paragraph:1dmoi0by]

Website Additions


[paragraph:1dmoi0by]Schlumpf has provided the following information that shows the data flow and operations in the processes:[/paragraph:1dmoi0by]
Code: [Select]
$ curl ACCOUNT_MESSAGE_HEADERS_URL?accountName=$accountname&sessionKeyHash=$keyhash
<xs:element name="headers">
  <xs:complexType>
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="header_entry">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="subject" type="string"/>
          </xs:sequence>
          <xs:attribute name="id" type="integer"/>
          <xs:attribute name="priority" type="integer"/>
          <xs:attribute name="opened" type="integer"/>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>
</xs:element>
<!--
  <headers>
    <header_entry id="0" priority="0" opened="0">
      <subject>HeaderSubject 1</subject>
    </header_entry>
    <header_entry id="1" priority="0" opened="1">
      <subject>HeaderSubject 2</subject>
    </header_entry>
  </headers>
-->

$ curl ACCOUNT_MESSAGE_BODY_NO_READ_URL?accountName=$accountname&sessionKeyHash=$keyhash&messageId=$id
<xs:element name="body" type="xs:string"/>
<!--
  <body>Body</body>
-->

$ curl ACCOUNT_MESSAGE_READ_URL?accountName=$accountname&sessionKeyHash=$keyhash&messageId=$id

sessionKeyHash:
static const short hash_constant = 0x7A0B;
SHA1_Init(&sha_context);
SHA1_Update(&sha_context, ClientServices::Connection()->GetSessionKey(), 40);
SHA1_Update(&sha_context, &hash_constant, sizeof (hash_constant));
SHA1_Final(keyhash, &sha_context);
[paragraph:1dmoi0by]This allowed me to create three PHP scripts to handle each event. I am by no means an expert at PHP, so I am sure my scripts could easily be improved.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]First I have, getMessageHeaders.php:[/paragraph:1dmoi0by]
Code: [Select]
<?php
$con 
mysql_connect('host''user''pass');

$account mysql_real_escape_string($_GET["accountName"]);
$hash mysql_real_escape_string($_GET["sessionKeyHash"]);

$count 0;

mysql_select_db('logon'$con);

$q mysql_query("SELECT `heading`,`read`,`hash` FROM `account_messages` WHERE account = '{$account}'") or die(mysql_error());

if (
mysql_num_rows($q) != 0)
{
  echo 
"<headers>";
  
  while(
$row mysql_fetch_array($q))
  {
    
//if ($hash == $row['hash'])
    //{
      
if ($row['read'] == 0)
      {
        
$head $row['heading'];
        
        echo 
"<header_entry id="{$count}" priority="0" opened="0">";
        echo  
"<subject>{$head}</subject>";
        echo 
"</header_entry>";
        
        
$count $count 1;
      }
    
//}
  
}
  
  echo 
"</headers>";
}

mysql_close($con);

/*
echo  "<headers>";
echo    "<header_entry id="0" priority="0" opened="0">";
echo      "<subject>{$account}! {$hash}</subject>";
echo    "</header_entry>";
echo    "<header_entry id="1" priority="0" opened="0">";
echo      "<subject>Message 2</subject>";
echo    "</header_entry>";
echo    "<header_entry id="2" priority="0" opened="0">";
echo      "<subject>Message 3</subject>";
echo    "</header_entry>";
echo  "</headers>";
*/
?>
[paragraph:1dmoi0by]Things to take note of here is that the account name and hashed session key are passed in as parameters to the script. I store account messages in a database, hence why I select the necessary data from this database. I loop through each unread message and display XML code as I go along. The structure of the XML code can be seen commented out at the bottom.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]I have also commented out the comparison to see if the database hash is the same as the client hash, because of the incorrect hash I was generating server side. This makes it insecure.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]The next file is getMessageBody.php, which follows a similar structure:[/paragraph:1dmoi0by]
Code: [Select]
<?php
$con 
mysql_connect('host''user''pass');

$account mysql_real_escape_string($_GET["accountName"]);
$hash mysql_real_escape_string($_GET["sessionKeyHash"]);
$id mysql_real_escape_string($_GET["messageId"]);

$count 0;

mysql_select_db('logon'$con);

$q mysql_query("SELECT `id`,`message`,`read`,`hash` FROM `account_messages` WHERE account = '{$account}' ORDER BY `id`") or die(mysql_error());

if (
mysql_num_rows($q) != 0)
{
  while(
$row mysql_fetch_array($q))
  {
    
//if ($hash == $row['hash'])
    //{
      
if ($row['read'] == 0)
      {
        if (
$count == $id)
        {
          
$body $row['message'];
          echo 
"<body>{$body}</body>";
          break;
        }
        
        
$count $count 1;
      }
    
//}
  
}
}

mysql_close($con);
?>
[paragraph:1dmoi0by]This just gets the text of the message depending on it's ID.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]The next script is markRead.php, which is used to mark the message as read to stop it showing up:[/paragraph:1dmoi0by]
Code: [Select]
<?php
$con 
mysql_connect('host''user''pass');

$account mysql_real_escape_string($_GET["accountName"]);
$hash mysql_real_escape_string($_GET["sessionKeyHash"]);
$id mysql_real_escape_string($_GET["messageId"]);

$count 0;
$id_found 0;

mysql_select_db('zzlogon'$con);

$q mysql_query("SELECT `id`,`hash`,`read` FROM `account_messages` WHERE account = '{$account}' ORDER BY `id`") or die(mysql_error());

if (
mysql_num_rows($q) != 0)
{
  while(
$row mysql_fetch_array($q))
  {
    
//if ($hash == $row['hash'])
    //{
      
if ($row['read'] == 0)
      {
        if (
$count == $id)
        {
          
$id_found $row['id'];
          break;
        }
        
        
$count $count 1;
      }
    
//}
  
}
}

if (
$id_found != 0)
{
  
mysql_query("UPDATE `account_messages` SET `read` = '1' WHERE `id` = '{$id_found}'") or die(mysql_error());
}

mysql_close($con);
?>
[paragraph:1dmoi0by]It follows a similar structure to the other two scripts. The ID of the message is a little hard to obtain, so the majority of the script is spent finding this ID. If the message ID is found, then a query is executed to update the account message and set it as read.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]This is all that is needed web side.[/paragraph:1dmoi0by]

Current Bugs


[paragraph:1dmoi0by]The first bug is that the hashed session key generated by the server is different to that of the client, making it very insecure. This needs to be fixed. This is described in more detail previously in the thread.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]The second bug is that it makes the client quite unstable for unknown reasons. If your account is flagged for account messages, I have found that upon next exiting the game a WoW error will be generated. This WoW error can also be generated at random often as well, by simple things such as pressing escape. I have been unable to make any progress on diagnosing this issue. A image of the error is shown below:[/paragraph:1dmoi0by]

[paragraph:1dmoi0by]Happy messaging, and please post if you think you can, or have, solved the issues described here.[/paragraph:1dmoi0by]
[paragraph:1dmoi0by]Thanks,
Harry & Schlumpf[/paragraph:1dmoi0by]
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

schlumpf

  • Administrator
  • Creator of Worlds
  • *****
  • Posts: 2967
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #1 on: July 30, 2012, 05:00:23 pm »
  • Rather then setting the strings again at some point, you may want to add a new file to the doc and only have the global strings there.
  • Rather than storing the session key per message, you may want to have a table with active sessions and die on count (SELECT 1 FROM sessions WHERE account = ? AND session_key = ?") != 1.
  • You may want to have the correct number of account messages sent on login, not a hardcoded value.
  • It seems like you want a table with (unique id, id per account, header, body, read), to save yourself the major part of marking as read.
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

stoneharry

  • Contributors
  • Creator of Worlds
  • *****
  • Posts: 617
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #2 on: July 31, 2012, 01:07:07 am »
Quote from: "schlumpf"
  • Rather then setting the strings again at some point, you may want to add a new file to the doc and only have the global strings there.
  • Rather than storing the session key per message, you may want to have a table with active sessions and die on count (SELECT 1 FROM sessions WHERE account = ? AND session_key = ?") != 1.
  • You may want to have the correct number of account messages sent on login, not a hardcoded value.
  • It seems like you want a table with (unique id, id per account, header, body, read), to save yourself the major part of marking as read.

I'll try and act on all of this, starting with the most important change from my eyes:

Code: [Select]
   buffer[22]= 0x01; //<-- ARENA TOURNAMENT ACC FLAG!
uint8 messages = 0;
QueryResult * result =  sLogonSQL->Query("SELECT `read` from account_messages where account = '%s'", m_account->UsernamePtr->c_str());
if(result)
{
do
{
if(!result->Fetch()[0].GetBool())
messages++;
} while (result->NextRow());
delete result;
}
buffer[30]= messages; // Unread messages
Send(buffer, 32);

Now I load how many messages for that account from the database. This means no more flagging when there are no messages (that's a large improvement). However, increasing that number makes no difference from what I can see, it has the same effect and the crashes still happen.
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

Norimar

  • Registred Member
  • Wiki Incarnate
  • *****
  • Posts: 156
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #3 on: July 31, 2012, 06:29:45 am »
The System is nice! But: Do I have to make a new core for each new "Account Message" ?
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

stoneharry

  • Contributors
  • Creator of Worlds
  • *****
  • Posts: 617
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #4 on: July 31, 2012, 10:35:44 am »
Quote from: "Norimar"
The System is nice! But: Do I have to make a new core for each new "Account Message" ?

I made it so that all account messages are loaded from the database in real time (so it will check for messages each relog)
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

Norimar

  • Registred Member
  • Wiki Incarnate
  • *****
  • Posts: 156
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #5 on: July 31, 2012, 01:03:35 pm »
Nice job :) Will test it, if it works korrekt on my server
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

Steff

  • Administrator
  • Creator of Worlds
  • *****
  • Posts: 4551
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #6 on: July 31, 2012, 02:32:21 pm »
Always try to make all the filter stuff inside the MySQL server. Much Faster and less data needed back.

Code: [Select]
SELECT heading FROM account_messages WHERE account = '".$account."' AND hash = '".$hash."' AND read = '0'
And to avoid css as long u use not SQLi and prepared statements.

Code: [Select]
$account = mysql_real_escape_string($account);
$hash      = mysql_real_escape_string($hash);
« Last Edit: January 01, 1970, 01:00:00 am by Admin »
Please mark as solved if solved.
Don't ask if you could ask a question... JUST ask the Question.
You can send me also offline messages. I will answer if I get online.
Skype: project.modcraft
Discord: steff#6954

schlumpf

  • Administrator
  • Creator of Worlds
  • *****
  • Posts: 2967
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #7 on: August 03, 2012, 01:53:09 pm »
Please add the call tree and registers from the crash log.
« Last Edit: January 01, 1970, 01:00:00 am by Admin »

stoneharry

  • Contributors
  • Creator of Worlds
  • *****
  • Posts: 617
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #8 on: August 03, 2012, 02:19:36 pm »
Quote from: "schlumpf"
Please add the call tree and registers from the crash log.

Test 1:

Code: [Select]
World of WarCraft (build 12340)

The instruction at "0x00814CE4" referenced memory at "0x0D0760A4".
The memory could not be "read".

EAX=0CBE2CE8  EBX=00000001  ECX=0D0760A4  EDX=0008E3C8  ESI=0D0760A4
EDI=7CCCFC85  EBP=001FFE58  ESP=001FFE48  EIP=00814CE4  FLG=00210202
CS =0023      DS =002B      ES =002B      SS =002B      FS =0053      GS =002B

--- Thread ID: 2904 [Current Thread] ---
00814CE4 001FFE58 0001:00413CE4 C:WoW 3.3.5wow.exe
00550B27 001FFEA0 0001:0014FB27 C:WoW 3.3.5wow.exe
0040D17D 001FFEEC 0001:0000C17D C:WoW 3.3.5wow.exe
0040D15D 001FFF88 0001:0000C15D C:WoW 3.3.5wow.exe
7637339A 001FFF94 0001:0000339A C:Windowssyswow64kernel32.dll
77579EF2 001FFFD4 0001:00029EF2 C:WindowsSysWOW64ntdll.dll
77579EC5 001FFFEC 0001:00029EC5 C:WindowsSysWOW64ntdll.dll

Test 2:

Code: [Select]
World of WarCraft (build 12340)

The instruction at "0x00814CE4" referenced memory at "0x0CCC37FC".
The memory could not be "read".

EAX=0C782CD8  EBX=00000001  ECX=0CCC37FC  EDX=0008E3C8  ESI=0CCC37FC
EDI=73F0EA14  EBP=001FFE58  ESP=001FFE48  EIP=00814CE4  FLG=00210206
CS =0023      DS =002B      ES =002B      SS =002B      FS =0053      GS =002B

--- Thread ID: 4652 [Current Thread] ---
00814CE4 001FFE58 0001:00413CE4 C:WoW 3.3.5wow.exe
00550B27 001FFEA0 0001:0014FB27 C:WoW 3.3.5wow.exe
0040D17D 001FFEEC 0001:0000C17D C:WoW 3.3.5wow.exe
0040D15D 001FFF88 0001:0000C15D C:WoW 3.3.5wow.exe
7637339A 001FFF94 0001:0000339A C:Windowssyswow64kernel32.dll
77579EF2 001FFFD4 0001:00029EF2 C:WindowsSysWOW64ntdll.dll
77579EC5 001FFFEC 0001:00029EC5 C:WindowsSysWOW64ntdll.dll
« Last Edit: August 03, 2012, 03:11:15 pm by Admin »

schlumpf

  • Administrator
  • Creator of Worlds
  • *****
  • Posts: 2967
    • View Profile
Re: [QUESTION] Account Messaging
« Reply #9 on: August 03, 2012, 02:25:00 pm »
I removed all the useless information.
« Last Edit: January 01, 1970, 01:00:00 am by Admin »