Ok so I'm really bored atm and have an hour or two to kill. This post will NOT be useful to very many people as I am NOT going to be like "paste this code in this file etcetc and BOOM you have a custom source!", I'm simply going to go through some basic steps to create a workable source. Eg: you can log in... if I bother getting even that far.
USUAL DISCLAIMER: I'm far from good at this stuff and am just learning it myself. If I make a mistake I request that you bring it to my attention and I will fix it/give credit. I have no desire to lead people down the road to bad coding (lets face it, I will) but I thought I'd do this mini project to kill an hour or two.
BEFORE YOU START:
Read and hopefully understand Null's excellent explanation of what a server is and how it works. A lot of that stuff will be repeated here but he does an awesome job explaining things. I'm simply going to back it up with sample code from various public sources.
Things we will need for our source
A C# editor (personally I use visual studio 2010 ultimate edition but that's just me)
A brain
Time
Things we need to do for our source
-
Connections
There is a ton you can do to write an efficient async socket system but for now I'm going to just go with an existing socket system that works fairly well. In this case, Impulse's.
Personally I enjoy his system because it's incredibly easy to impliment for a rapid development type situation. DO NOT LIMIT YOURSELF! If you feel comfortable enough working with async sockets (sync are not really perfect for this application but feel free to try) then you may want to write your own system. Personally I just find this simple to use.
Code:
public class Wrapper
{
public byte[] buffer;
public Socket _socket;
public object connector;
}
public class WinSocket
{
private Dictionary<string, byte> Connections;
public event Action<Wrapper> AnnounceNewConnection;
public event Action<Wrapper> AnnounceDisconnection;
public event Action<byte[], Wrapper, byte[]> AnnounceReceive;
private Socket _socket;
public WinSocket(ushort port)
{
try
{
Connections = new Dictionary<string, byte>();
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_socket.Bind(new IPEndPoint(IPAddress.Any, port));
_socket.Listen(500);
_socket.BeginAccept(AcceptConnections, new Wrapper());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private void AcceptConnections(IAsyncResult result)
{
try
{
Wrapper wr = result.AsyncState as Wrapper;
wr._socket = _socket.EndAccept(result);
#region Invisible
string IP = wr._socket.RemoteEndPoint.ToString().Split(':')[0].ToString();
if (!Connections.ContainsKey(IP))
Connections.Add(IP, 1);
else
if (Connections[IP] <= 12)
{
byte connections = Connections[IP];
Connections.Remove(IP);
Connections.Add(IP, (byte)(connections + 1));
}
else
{
wr._socket.Disconnect(false);
_socket.BeginAccept(AcceptConnections, new Wrapper());
return;
}
#endregion
wr.buffer = new byte[65535];
wr._socket.BeginReceive(wr.buffer, 0, 65535, SocketFlags.None, ReceiveData, wr);
AnnounceNewConnection.Invoke(wr);
_socket.BeginAccept(AcceptConnections, new Wrapper());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private void ReceiveData(IAsyncResult result)
{
try
{
Wrapper wr = result.AsyncState as Wrapper;
string IP = wr._socket.RemoteEndPoint.ToString().Split(':')[0].ToString();
if (Connections.ContainsKey(IP))
{
SocketError error = SocketError.Disconnecting;
int size = wr._socket.EndReceive(result, out error);
if (error == SocketError.Success && size != 0)
{
byte[] buffer = new byte[size];
Buffer.BlockCopy(wr.buffer, 0, buffer, 0, size);
byte[] question = new byte[] { 1 };
AnnounceReceive.Invoke(buffer, wr, question);
if (wr._socket.Connected && question[0] == 1)
wr._socket.BeginReceive(wr.buffer, 0, 65535, SocketFlags.None, ReceiveData, wr);
}
else
{
if (wr._socket.Connected)
{
wr._socket.Disconnect(true);
}
byte connections = Connections[IP];
Connections.Remove(IP);
Connections.Add(IP, (byte)(connections - 1));
try
{
AnnounceDisconnection.Invoke(wr);
}
catch { }
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
Note: some of the things from his original system are changed in this.. I'm not using thread per client and I may have changed other things... I personally don't remember what all I've changed atm so lets just use this as an example for now.
Setting up the listeners!
For a conquer server you will need two sockets blocked off for listening, namely game and auth ports.
If you are making a server for a recent tq patch you will be using auth port 9959 and game port 5816 wheras older patches used 9958 for auth port iirc.
So yes, lets setup our basic listeners!
WinSocket AuthSocket = new WinSocket(AuthPort);
WinSocket GameSocket = new WinSocket(GamePort);
In this case obviously I have Auth and Game ports defined as a static variable in my source which can then be read from a database, a text file... anything you want really.
Now, lets set up the actions for this system.
Simply do something along the lines of...
AuthSocket.AnnounceNewConnection += new Action<Wrapper>(ClientConnect);
Explanation! ClientConnect is the function/method to call when a client connects on the authsocket!
Code:
static void ClientConnect(Wrapper C)
{
C.connector = new ClientSocket(C._socket);
}
Now, we are creating a new instance of our ClientSocket. What is this? Well this is the class which contains all of our variables for this login client. NOTE: In this case it is only login information (login encryption, user, pass, passwordseed, etc) You could write this a number of ways. Just make sure you are doing things in the proper order.
Personally I send the password seed directly when the client connects (only needed if you are doing patch 52xx+ (forget which patch exactly) but I'll explain it slightly now.
public class ClientSocket
{
//Client variables
public ClientSocket(Socket C)
{
Client = C;
AuthType = 1;
AuthCrypt = new Encryption.AuthCrypt();
PassSeed = Program.Rand.Next(100000, 90000000);
byte[] S = Packets.PassSeed((uint)PassSeed);
Packets.Send(S, this);
}
}
Now, not all of this will be needed depending on which patch you are running. Basically I'm just initializing the values for my auth connection. This includes in this case setting the auth type (I use this to sort out which stage of authorization my client is at), encryption, password seed and then sending this.
In the case of newer patches seed MUST be sent before the client will send a login request packet.
This leads us to packet receiving/handling and encryption!
Encryption:
There are 2-3 main encryptions you will need to worry about for conquer emulation currently.
There are complete solutions to ALL of these encryption. You could write your own but personally I simply see no reason to.
1: Login Encryption. Very simple to setup and use. Requires no initializing values, you simply run login packets through the algorithm and you are good to go.
Have a note in my source that the credit for this was from immune's source. Credit to him or whoever he used it from.
Code:
public class AuthCrypt
{
class Counter
{
ushort KeyCounter = 0;
public byte FirstKey
{
get { return (byte)(KeyCounter & 0xFF); }
}
public byte SecondKey
{
get { return (byte)(KeyCounter >> 8); }
}
public ushort Increment(ushort val)
{
this.KeyCounter += val;
return (this.KeyCounter);
}
}
private Counter DecryptCounter;
private Counter EncryptCounter;
private byte[] FirstKey;
private byte[] SecondKey;
public AuthCrypt()
{
this.DecryptCounter = new Counter();
this.EncryptCounter = new Counter();
this.FirstKey = new byte[0x100];
this.SecondKey = new byte[0x100];
byte FirstByte = 0x9D;
byte SecondByte = 0x62;
for (int i = 0; i < 0x100; i++)
{
this.FirstKey[i] = FirstByte;
this.SecondKey[i] = SecondByte;
FirstByte = (byte)((0x0F + (byte)(FirstByte * 0xFA)) * FirstByte + 0x13);
SecondByte = (byte)((0x79 - (byte)(SecondByte * 0x5C)) * SecondByte + 0x6D);
}
}
public void Increment(ushort va)
{
this.EncryptCounter.Increment(va);
}
public void Encode(byte[] buffer)
{
lock (this.EncryptCounter)
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] ^= (byte)0xAB;
buffer[i] = (byte)(buffer[i] >> 4 | buffer[i] << 4);
buffer[i] ^= (byte)(this.FirstKey[this.EncryptCounter.FirstKey] ^ this.SecondKey[this.EncryptCounter.SecondKey]);
this.EncryptCounter.Increment(1);
}
}
}
public void Decode(byte[] buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] ^= (byte)0xAB;
buffer[i] = (byte)(buffer[i] >> 4 | buffer[i] << 4);
buffer[i] ^= (byte)(this.FirstKey[this.DecryptCounter.FirstKey] ^ this.SecondKey[this.DecryptCounter.SecondKey]);
this.DecryptCounter.Increment(1);
}
}
}
2: Blowfish encryption (5017+ only)
Somewhat more complex as it requires a dhkey exchange as well as a static key (the gamekey, DR654dt34trg4UI6 on new'ish patches). Loads of solutions for this already. I'll post one but I'm fairly sure they are the same in 99 pct of 5017+ sources.
Code:
public class GameCryptography
{
Blowfish _blowfish;
public GameCryptography(byte[] key)
{
_blowfish = new Blowfish(BlowfishAlgorithm.CFB64);
_blowfish.SetKey(key);
}
public void Decrypt(byte[] packet)
{
byte[] buffer = _blowfish.Decrypt(packet);
System.Buffer.BlockCopy(buffer, 0, packet, 0, buffer.Length);
}
public void Encrypt(byte[] packet)
{
byte[] buffer = _blowfish.Encrypt(packet);
System.Buffer.BlockCopy(buffer, 0, packet, 0, buffer.Length);
}
public Blowfish Blowfish
{
get { return _blowfish; }
}
public void SetKey(byte[] k)
{
_blowfish.SetKey(k);
}
public void SetIvs(byte[] i1, byte[] i2)
{
_blowfish.EncryptIV = i1;
_blowfish.DecryptIV = i2;
}
}
public enum BlowfishAlgorithm
{
ECB,
CBC,
CFB64,
OFB64,
};
public class Blowfish : IDisposable
{
[DllImport("libeay32.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void BF_set_key(IntPtr _key, int len, byte[] data);
[DllImport("libeay32.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void BF_ecb_encrypt(byte[] in_, byte[] out_, IntPtr schedule, int enc);
[DllImport("libeay32.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void BF_cbc_encrypt(byte[] in_, byte[] out_, int length, IntPtr schedule, byte[] ivec, int enc);
[DllImport("libeay32.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void BF_cfb64_encrypt(byte[] in_, byte[] out_, int length, IntPtr schedule, byte[] ivec, ref int num, int enc);
[DllImport("libeay32.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void BF_ofb64_encrypt(byte[] in_, byte[] out_, int length, IntPtr schedule, byte[] ivec, out int num);
[StructLayout(LayoutKind.Sequential)]
struct bf_key_st
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 18)]
public UInt32[] P;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)]
public UInt32[] S;
}
private BlowfishAlgorithm _algorithm;
private IntPtr _key;
private byte[] _encryptIv;
private byte[] _decryptIv;
private int _encryptNum;
private int _decryptNum;
public Blowfish(BlowfishAlgorithm algorithm)
{
_algorithm = algorithm;
_encryptIv = new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 };
_decryptIv = new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 };
bf_key_st key = new bf_key_st();
key.P = new UInt32[16 + 2];
key.S = new UInt32[4 * 256];
_key = Marshal.AllocHGlobal(key.P.Length * sizeof(UInt32) + key.S.Length * sizeof(UInt32));
Marshal.StructureToPtr(key, _key, false);
_encryptNum = 0;
_decryptNum = 0;
}
public void Dispose()
{
Marshal.FreeHGlobal(_key);
}
public void SetKey(byte[] data)
{
_encryptNum = 0;
_decryptNum = 0;
BF_set_key(_key, data.Length, data);
}
public byte[] Encrypt(byte[] buffer)
{
byte[] ret = new byte[buffer.Length];
switch (_algorithm)
{
case BlowfishAlgorithm.ECB:
BF_ecb_encrypt(buffer, ret, _key, 1);
break;
case BlowfishAlgorithm.CBC:
BF_cbc_encrypt(buffer, ret, buffer.Length, _key, _encryptIv, 1);
break;
case BlowfishAlgorithm.CFB64:
BF_cfb64_encrypt(buffer, ret, buffer.Length, _key, _encryptIv, ref _encryptNum, 1);
break;
case BlowfishAlgorithm.OFB64:
BF_ofb64_encrypt(buffer, ret, buffer.Length, _key, _encryptIv, out _encryptNum);
break;
}
return ret;
}
public byte[] Decrypt(byte[] buffer)
{
byte[] ret = new byte[buffer.Length];
switch (_algorithm)
{
case BlowfishAlgorithm.ECB:
BF_ecb_encrypt(buffer, ret, _key, 0);
break;
case BlowfishAlgorithm.CBC:
BF_cbc_encrypt(buffer, ret, buffer.Length, _key, _decryptIv, 0);
break;
case BlowfishAlgorithm.CFB64:
BF_cfb64_encrypt(buffer, ret, buffer.Length, _key, _decryptIv, ref _decryptNum, 0);
break;
case BlowfishAlgorithm.OFB64:
BF_ofb64_encrypt(buffer, ret, buffer.Length, _key, _decryptIv, out _decryptNum);
break;
}
return ret;
}
public byte[] EncryptIV
{
get { return _encryptIv; }
set { System.Buffer.BlockCopy(value, 0, _encryptIv, 0, 8); }
}
public byte[] DecryptIV
{
get { return _decryptIv; }
set { System.Buffer.BlockCopy(value, 0, _decryptIv, 0, 8); }
}
}
We also need our key exchange routine
Code:
public static class DHKeyExchange
{
public class ServerKeyExchange
{
OpenSSL.DH _keyExchange;
byte[] _serverIv;
byte[] _clientIv;
public byte[] CreateServerKeyPacket()
{
_clientIv = new byte[8];
_serverIv = new byte[8];
string P = "E7A69EBDF105F2A6BBDEAD7E798F76A209AD73FB466431E2E7352ED262F8C558F10BEFEA977DE9E21DCEE9B04D245F300ECCBBA03E72630556D011023F9E857F";
string G = "05";
_keyExchange = new OpenSSL.DH(OpenSSL.BigNumber.FromHexString(P), OpenSSL.BigNumber.FromHexString(G));
_keyExchange.GenerateKeys();
return GeneratePacket(_serverIv, _clientIv, P, G, _keyExchange.PublicKey.ToHexString());
}
public GameCryptography HandleClientKeyPacket(string PublicKey, GameCryptography cryptographer)
{
byte[] key = _keyExchange.ComputeKey(OpenSSL.BigNumber.FromHexString(PublicKey));
cryptographer.SetKey(key);
cryptographer.SetIvs(_clientIv, _serverIv);
return cryptographer;
}
public byte[] GeneratePacket(byte[] ServerIV1, byte[] ServerIV2, string P, string G, string ServerPublicKey)
{
int PAD_LEN = 11;
int _junk_len = 12;
string tqs = "TQServer";
MemoryStream ms = new MemoryStream();
byte[] pad = new byte[PAD_LEN];
Program.Rand.NextBytes(pad);
byte[] junk = new byte[_junk_len];
Program.Rand.NextBytes(junk);
int size = 47 + P.Length + G.Length + ServerPublicKey.Length + 12 + 8 + 8;
BinaryWriter bw = new BinaryWriter(ms);
bw.Write(pad);
bw.Write(size - PAD_LEN);
bw.Write((UInt32)_junk_len);
bw.Write(junk);
bw.Write((UInt32)ServerIV2.Length);
bw.Write(ServerIV2);
bw.Write((UInt32)ServerIV1.Length);
bw.Write(ServerIV1);
bw.Write((UInt32)P.ToCharArray().Length);
foreach (char fP in P.ToCharArray())
{
bw.BaseStream.WriteByte((byte)fP);
}
bw.Write((UInt32)G.ToCharArray().Length);
foreach (char fG in G.ToCharArray())
{
bw.BaseStream.WriteByte((byte)fG);
}
bw.Write((UInt32)ServerPublicKey.ToCharArray().Length);
foreach (char SPK in ServerPublicKey.ToCharArray())
{
bw.BaseStream.WriteByte((byte)SPK);
}
foreach (char tq in tqs.ToCharArray())
{
bw.BaseStream.WriteByte((byte)tq);
}
byte[] Packet = new byte[ms.Length];
Packet = ms.ToArray();
ms.Close();
return Packet;
}
}
}
Packet Receiving and Handling
Ok so the boring encryption stuff is pushed back for now so lets deal with actually receiving our packets!
Back on our authsocket we need to set up a new action. In this case AnnounceReceive
AuthSocket.AnnounceReceive += new Action<byte[], Wrapper, byte[]>(ClientReceive);
Again, ClientReceive is the function/method being called. Lets deal with that code shall we?
Code:
static void ClientReceive(byte[] arg1, Wrapper C, byte[] arg3)
{
ClientSocket Sender = C.connector as ClientSocket;
PacketHandler.Handle(arg1, Sender);
}
So in this case we are saying the ClientSocket Sender = the connector of the sender.. AS client socket. This may not make much sense but .connector is an object. This means it doesn't particularly have a type and you can ATTEMPT to interpret it.
Mini desc from msdn
"all types, predefined and user-defined, reference types and value types, inherit directly or indirectly from Object. You can assign values of any type to variables of type object. When a variable of a value type is converted to object, it is said to be boxed. When a variable of type object is converted to a value type, it is said to be unboxed"
So seeing as we assigned it to a clientsocket when we received the connection, we can interpret it as one now (unbox it)
Now, we run our client packet handler using the data received!
Code:
public static void Handle(byte[] Data, ClientSocket Client)
{
Client.AuthCrypt.Decode(Data);
}
So we need to first decode the data obviously. We can do this using the Client's encryption (which we initiated when they first connected remember?)
IMPORTANT: You MUST setup encryption and decrypt packets as they are sent. The encryption uses a counter meaning if you don't decrypt everything being received this will be off and the encryption won't work anymore (Don't ignore packets is what we're saying)
So now we have our array of bytes that makes up a packet... What do we do with it now?! Well we need a way to handle this packet we've received.
INFORMATION ON PACKETS/STRUCTURING:
So if you've read the link I just gave you, you'll know that conquer packets use little endian format. This probably makes no sense to you but basically when reading something we read the bytes from right to left (the ones on the left are the smallest... think 8 in 18...)
To do this efficiently we need a way to structure what we're reading.
Byte = 1 byte
ushort = 2 bytes
uint = 4 bytes
ulong = 8 bytes
or we can read as many as needed (usually for strings)
USEFUL NOTE: Encoding.ASCII.GetString(arrayofbytes) will give you the string for an array of bytes... obviously. Useful to know though if you just want to quickly try to view string information in a packet.
Personally I've been using Tannels system for reading packets... I'm sure there are FAR better systems out there but I have it handy and am used to is so I'll post it as an example.
Code:
public unsafe class PacketReader
{
byte[] PData;
int Position;
public PacketReader(byte[] Data, int Pos)
{
PData = Data;
Position = Pos;
}
public byte ReadByte()
{
byte Val = PData[Position];
Position += 1;
return Val;
}
public ushort ReadUInt16()
{
ushort Val = BitConverter.ToUInt16(PData, Position);
Position += 2;
return Val;
}
public uint ReadUInt32()
{
uint Val = BitConverter.ToUInt32(PData, Position);
Position += 4;
return Val;
}
public ulong ReadUInt64()
{
ulong Val = BitConverter.ToUInt64(PData, Position);
Position += 8;
return Val;
}
public byte[] ReadBytes(int Count)
{
byte[] Val = new byte[Count];
Buffer.BlockCopy(PData, Position, Val, 0, Count);
Position += Count;
return Val;
}
}
This doesn't need to make much sense to you yet... Lets go back to our packet handler and use it!
Ok so if you remember we have our Handle(Data, Client) method created and have decrypted the packet. Now what? Well we need to start reading the packet! To do this lets call our new shmancy packet reader.
PacketReader PR = new PacketReader(Data, 0);
This means we are reading the byte array known as "Data" and starting at offset 0. If you wanted to only read certain sections of the packet you may wish to skip ahead (therefor start at a higher offset).
Now. If you read the information I linked you to on how to interpret packets you would know that all packets have a length value to start and then generally a packet type.
So lets handle this!
ushort Length = PR.ReadUInt16();
ushort Type = PR.ReadUInt16();
We are calling the Packet reader we set up (PR) and reading by increments of 2 bytes. In this case we don't truly need the length but I've assigned it anyways. You could just do PR.ReadUInt16(); instead to skip it... or set the 0 in our starting offset to 2. Regardless what is important is the "Type" that we've read. Lets setup a switch statement to handle each type! (login only really uses 1... but w/e!)
switch (Type)
{
}
Now inside this we want a Case ##: for each packet we wish to handle (this only becomes an issue at all when we get to game server)
In this case we want to handle the login request. aka type 1086
case 1086:
break;
This is the client sending us the lovely login request which contains things such as...
Username, Password (remember to decrypt using password encryption if you are doing a current patch! Look for an example in immune's source, I don't care to go over it atm) and server name.
Now. I'm not going to go very far in depth with packet structure as this is something you will need to learn yourself but the link I provided earlier, trial and error as well as korv's wiki are GREAT resources!
Generally you want to look at a packet and try to figure our what it contains.
Eg: seeing a small number followed by a bunch of values in order indicates it's probably a string with a length value preceding. What you can do for this would be..
string Whatever = Encoding.ASCII.GetString(PR.ReadBytes(PR.ReadByte( )));
This would read the next byte in the stream to read the number of bytes that the string is, then you read those bytes to a byte array and convert it to string.
Now that we've received a username and password for the client (and decrypted if needed) we need to check if it is valid!
We do this vs our character database (sql, flatfile, doesn't matter! You still need to check!)
An example would be a sql function which attempts to pull a password from accounts where username = 'username'. Then check if the password read == the one from the packet and if not return false.
NOTE: User/Pass in this packet do NOT have lengths and therefor will have whitespace! You can replace these using something like...
User = User.Replace("\0", "");
DO THIS BEFORE CHECKING VS DATABASE PASSWORD OR YOU WILL ALWAYS GET IT AS BEING WRONG!
Once we've validated the user/pass we need to respond to the client using the auth response packet: Type 1055
The structure of this packet is very simple... we will want to change the values of it though depending on if the account should try to log in to the game server now or not.
This will contain things such as..
Character UID
Server IP
Server Port
and type of response (valid, invalid pass, banned, etc)
2 = valid login
0 = invalid pass
57 = banned
Forget what others their are but easy enough to try some.
Once this is done we should have the client trying to connect to the game server! Congratz!
We've already set up the listener for it so do a similar setup where you have a onreceive/onconnect type action and setup your client class (possibly inheriting the information from the old login client) and setting up encryption.
NOTE: Blowfish takes a bit more to setup. I MAY go into the game server side of things if people see a need for me to continue this "guide"
Closing:
Hopefully this will help those who are getting some decent coding knowledge and are wanting to at least attempt making their own source. Setting up something basic isn't too difficult but I thought I'd point out some of the steps you will need to go through to hopefully help in that journey.