aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfusion32 <marcopuzziello@gmail.com>2025-07-18 18:04:54 -0300
committerfusion32 <marcopuzziello@gmail.com>2025-07-18 18:04:54 -0300
commitbaaeac349dc5da2cb4892662c69566a98a66a124 (patch)
tree634b3d4be3f659290f0271402f3c68d061c686d1
downloadlogin-baaeac349dc5da2cb4892662c69566a98a66a124.tar.gz
login-baaeac349dc5da2cb4892662c69566a98a66a124.zip
initial version
-rw-r--r--.gitignore5
-rw-r--r--Makefile46
-rw-r--r--config.cfg8
-rw-r--r--src/common.cc373
-rw-r--r--src/common.hh103
-rw-r--r--src/connections.cc554
-rw-r--r--src/crypto.cc102
-rw-r--r--src/login.cc130
-rw-r--r--src/login.hh401
-rw-r--r--src/query.cc285
-rw-r--r--tibia.pem15
11 files changed, 2022 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d680a30
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.vscode
+bin
+build
+local
+*.log
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c456577
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,46 @@
+SRCDIR = src
+BUILDDIR = build
+OUTPUTEXE = login
+
+CXX = g++
+CXXFLAGS = -m64 -fno-strict-aliasing -pedantic -Wno-unused-parameter -Wall -Wextra -pthread --std=c++11
+LFLAGS = -Wl,-t -lcrypto
+
+DEBUG ?= 0
+ifneq ($(DEBUG), 0)
+ CXXFLAGS += -g -O0
+else
+ CXXFLAGS += -O2
+endif
+
+HEADERS = $(SRCDIR)/common.hh $(SRCDIR)/login.hh
+
+$(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/common.obj $(BUILDDIR)/connections.obj $(BUILDDIR)/crypto.obj $(BUILDDIR)/login.obj $(BUILDDIR)/query.obj
+ @mkdir -p $(@D)
+ $(CXX) $(CXXFLAGS) -o $@ $^ $(LFLAGS)
+
+$(BUILDDIR)/common.obj: $(SRCDIR)/common.cc $(HEADERS)
+ @mkdir -p $(@D)
+ $(CXX) -c $(CXXFLAGS) -o $@ $<
+
+$(BUILDDIR)/connections.obj: $(SRCDIR)/connections.cc $(HEADERS)
+ @mkdir -p $(@D)
+ $(CXX) -c $(CXXFLAGS) -o $@ $<
+
+$(BUILDDIR)/crypto.obj: $(SRCDIR)/crypto.cc $(HEADERS)
+ @mkdir -p $(@D)
+ $(CXX) -c $(CXXFLAGS) -o $@ $<
+
+$(BUILDDIR)/login.obj: $(SRCDIR)/login.cc $(HEADERS)
+ @mkdir -p $(@D)
+ $(CXX) -c $(CXXFLAGS) -o $@ $<
+
+$(BUILDDIR)/query.obj: $(SRCDIR)/query.cc $(HEADERS)
+ @mkdir -p $(@D)
+ $(CXX) -c $(CXXFLAGS) -o $@ $<
+
+.PHONY: clean
+
+clean:
+ @rm -rf $(BUILDDIR)
+
diff --git a/config.cfg b/config.cfg
new file mode 100644
index 0000000..458a0c0
--- /dev/null
+++ b/config.cfg
@@ -0,0 +1,8 @@
+MOTD = "Welcome to Tibia!"
+UpdateRate = 20
+LoginPort = 7171
+MaxConnections = 10
+LoginTimeout = 10s
+QueryManagerHost = "127.0.0.1"
+QueryManagerPort = 7174
+QueryManagerPassword = "a6glaf0c"
diff --git a/src/common.cc b/src/common.cc
new file mode 100644
index 0000000..2a40df2
--- /dev/null
+++ b/src/common.cc
@@ -0,0 +1,373 @@
+#include "common.hh"
+
+void LogAdd(const char *Prefix, const char *Format, ...){
+ char Entry[4096];
+ va_list ap;
+ va_start(ap, Format);
+ vsnprintf(Entry, sizeof(Entry), Format, ap);
+ va_end(ap);
+
+ if(Entry[0] != 0){
+ struct tm LocalTime = GetLocalTime(time(NULL));
+ fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s\n",
+ LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
+ LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
+ Prefix, Entry);
+ }
+}
+
+void LogAddVerbose(const char *Prefix, const char *Function,
+ const char *File, int Line, const char *Format, ...){
+ char Entry[4096];
+ va_list ap;
+ va_start(ap, Format);
+ vsnprintf(Entry, sizeof(Entry), Format, ap);
+ va_end(ap);
+
+ if(Entry[0] != 0){
+ (void)File;
+ (void)Line;
+ struct tm LocalTime = GetLocalTime(time(NULL));
+ fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s: %s\n",
+ LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
+ LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
+ Prefix, Function, Entry);
+ }
+}
+
+struct tm GetLocalTime(time_t t){
+ struct tm result;
+#if COMPILER_MSVC
+ localtime_s(&result, &t);
+#else
+ localtime_r(&t, &result);
+#endif
+ return result;
+}
+
+int64 GetClockMonotonicMS(void){
+#if OS_WINDOWS
+ LARGE_INTEGER Counter, Frequency;
+ QueryPerformanceCounter(&Counter);
+ QueryPerformanceFrequency(&Frequency);
+ return (int64)((Counter.QuadPart * 1000) / Frequency.QuadPart);
+#else
+ struct timespec Time;
+ clock_gettime(CLOCK_MONOTONIC, &Time);
+ return ((int64)Time.tv_sec * 1000)
+ + ((int64)Time.tv_nsec / 1000000);
+#endif
+}
+
+void SleepMS(int64 DurationMS){
+#if OS_WINDOWS
+ Sleep((DWORD)DurationMS);
+#else
+ struct timespec Duration;
+ Duration.tv_sec = (time_t)(DurationMS / 1000);
+ Duration.tv_nsec = (long)((DurationMS % 1000) * 1000000);
+ nanosleep(&Duration, NULL);
+#endif
+}
+
+bool StringEq(const char *A, const char *B){
+ int Index = 0;
+ while(true){
+ if(A[Index] != B[Index]){
+ return false;
+ }else if(A[Index] == 0){
+ return true;
+ }
+ Index += 1;
+ }
+}
+
+bool StringEqCI(const char *A, const char *B){
+ int Index = 0;
+ while(true){
+ if(tolower(A[Index]) != tolower(B[Index])){
+ return false;
+ }else if(A[Index] == 0){
+ return true;
+ }
+ Index += 1;
+ }
+}
+
+bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ ASSERT(DestCapacity > 0);
+ bool Result = (SrcLength < DestCapacity);
+ if(Result && SrcLength > 0){
+ memcpy(Dest, Src, SrcLength);
+ Dest[SrcLength] = 0;
+ }else{
+ Dest[0] = 0;
+ }
+ return Result;
+}
+
+bool StringCopy(char *Dest, int DestCapacity, const char *Src){
+ // IMPORTANT(fusion): `sqlite3_column_text` may return NULL if the column is
+ // also NULL so we have an incentive to properly handle the case where `Src`
+ // is NULL.
+ int SrcLength = (Src != NULL ? (int)strlen(Src) : 0);
+ return StringCopyN(Dest, DestCapacity, Src, SrcLength);
+}
+
+bool EscapeString(char *Dest, int DestCapacity, const char *Src){
+ int WritePos = 0;
+ for(int ReadPos = 0; Src[ReadPos] != 0 && WritePos < DestCapacity; ReadPos += 1){
+ int EscapeCh = -1;
+ switch(Src[ReadPos]){
+ case '\a': EscapeCh = 'a'; break;
+ case '\b': EscapeCh = 'b'; break;
+ case '\t': EscapeCh = 't'; break;
+ case '\n': EscapeCh = 'n'; break;
+ case '\v': EscapeCh = 'v'; break;
+ case '\f': EscapeCh = 'f'; break;
+ case '\r': EscapeCh = 'r'; break;
+ case '\"': EscapeCh = '\"'; break;
+ case '\'': EscapeCh = '\''; break;
+ case '\\': EscapeCh = '\\'; break;
+ }
+
+ if(EscapeCh != -1){
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = '\\';
+ WritePos += 1;
+ }
+
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = EscapeCh;
+ WritePos += 1;
+ }
+ }else{
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = Src[ReadPos];
+ WritePos += 1;
+ }
+ }
+ }
+
+ if(WritePos < DestCapacity){
+ Dest[WritePos] = 0;
+ return true;
+ }else{
+ Dest[DestCapacity - 1] = 0;
+ return false;
+ }
+}
+
+uint32 HashString(const char *String){
+ // FNV1a 32-bits
+ uint32 Hash = 0x811C9DC5U;
+ for(int i = 0; String[i] != 0; i += 1){
+ Hash ^= (uint32)String[i];
+ Hash *= 0x01000193U;
+ }
+ return Hash;
+}
+
+bool ParseIPAddress(int *Dest, const char *String){
+ ASSERT(Dest != NULL && String != NULL);
+ int Addr[4];
+ if(sscanf(String, "%d.%d.%d.%d", &Addr[0], &Addr[1], &Addr[2], &Addr[3]) != 4){
+ LOG_ERR("Invalid IP Address format \"%s\"", String);
+ return false;
+ }
+
+ if(Addr[0] < 0 || Addr[0] > 0xFF
+ || Addr[1] < 0 || Addr[1] > 0xFF
+ || Addr[2] < 0 || Addr[2] > 0xFF
+ || Addr[3] < 0 || Addr[3] > 0xFF){
+ LOG_ERR("Invalid IP Address \"%s\"", String);
+ return false;
+ }
+
+ *Dest = (Addr[0] << 24)
+ | (Addr[1] << 16)
+ | (Addr[2] << 8)
+ | (Addr[3] << 0);
+ return true;
+}
+
+bool ParseBoolean(bool *Dest, const char *String){
+ ASSERT(Dest != NULL && String != NULL);
+ *Dest = StringEqCI(String, "true");
+ return *Dest || StringEqCI(String, "false");
+}
+
+bool ParseInteger(int *Dest, const char *String){
+ ASSERT(Dest != NULL && String != NULL);
+ const char *StringEnd;
+ *Dest = (int)strtol(String, (char**)&StringEnd, 0);
+ return StringEnd > String;
+}
+
+bool ParseDuration(int *Dest, const char *String){
+ ASSERT(Dest != NULL && String != NULL);
+ const char *Suffix;
+ *Dest = (int)strtol(String, (char**)&Suffix, 0);
+ if(Suffix == String){
+ return false;
+ }
+
+ while(Suffix[0] != 0 && isspace(Suffix[0])){
+ Suffix += 1;
+ }
+
+ if(Suffix[0] == 'S' || Suffix[0] == 's'){
+ *Dest *= (1000);
+ }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
+ *Dest *= (60 * 1000);
+ }else if(Suffix[0] == 'H' || Suffix[0] == 'h'){
+ *Dest *= (60 * 60 * 1000);
+ }
+
+ return true;
+}
+
+bool ParseSize(int *Dest, const char *String){
+ ASSERT(Dest != NULL && String != NULL);
+ const char *Suffix;
+ *Dest = (int)strtol(String, (char**)&Suffix, 0);
+ if(Suffix == String){
+ return false;
+ }
+
+ while(Suffix[0] != 0 && isspace(Suffix[0])){
+ Suffix += 1;
+ }
+
+ if(Suffix[0] == 'K' || Suffix[0] == 'k'){
+ *Dest *= (1024);
+ }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
+ *Dest *= (1024 * 1024);
+ }
+
+ return true;
+}
+
+bool ParseString(char *Dest, int DestCapacity, const char *String){
+ ASSERT(Dest != NULL && DestCapacity > 0 && String != NULL);
+ int StringStart = 0;
+ int StringEnd = (int)strlen(String);
+ if(StringEnd >= 2){
+ if((String[0] == '"' && String[StringEnd - 1] == '"')
+ || (String[0] == '\'' && String[StringEnd - 1] == '\'')
+ || (String[0] == '`' && String[StringEnd - 1] == '`')){
+ StringStart += 1;
+ StringEnd -= 1;
+ }
+ }
+
+ return StringCopyN(Dest, DestCapacity,
+ &String[StringStart], (StringEnd - StringStart));
+}
+
+bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback){
+ ASSERT(FileName != NULL && KVCallback != NULL);
+ FILE *File = fopen(FileName, "rb");
+ if(File == NULL){
+ LOG_ERR("Failed to open config file \"%s\"", FileName);
+ return false;
+ }
+
+ bool EndOfFile = false;
+ for(int LineNumber = 1; !EndOfFile; LineNumber += 1){
+ char Line[1024];
+ int MaxLineSize = (int)sizeof(Line);
+ int LineSize = 0;
+ int KeyStart = -1;
+ int EqualPos = -1;
+ while(true){
+ int ch = fgetc(File);
+ if(ch == EOF || ch == '\n'){
+ if(ch == EOF){
+ EndOfFile = true;
+ }
+ break;
+ }
+
+ if(LineSize < MaxLineSize){
+ Line[LineSize] = (char)ch;
+ }
+
+ if(KeyStart == -1 && !isspace(ch)){
+ KeyStart = LineSize;
+ }
+
+ if(EqualPos == -1 && ch == '='){
+ EqualPos = LineSize;
+ }
+
+ LineSize += 1;
+ }
+
+ // NOTE(fusion): Check line size limit.
+ if(LineSize > MaxLineSize){
+ LOG_WARN("%s:%d: Exceeded line size limit of %d characters",
+ FileName, LineNumber, MaxLineSize);
+ continue;
+ }
+
+ // NOTE(fusion): Check empty line or comment.
+ if(KeyStart == -1 || Line[KeyStart] == '#'){
+ continue;
+ }
+
+ // NOTE(fusion): Check assignment.
+ if(EqualPos == -1){
+ LOG_WARN("%s:%d: No assignment found on non empty line",
+ FileName, LineNumber);
+ continue;
+ }
+
+ // NOTE(fusion): Check empty key.
+ int KeyEnd = EqualPos;
+ while(KeyEnd > KeyStart && isspace(Line[KeyEnd - 1])){
+ KeyEnd -= 1;
+ }
+
+ if(KeyStart == KeyEnd){
+ LOG_WARN("%s:%d: Empty key", FileName, LineNumber);
+ continue;
+ }
+
+ // NOTE(fusion): Check empty value.
+ int ValStart = EqualPos + 1;
+ int ValEnd = LineSize;
+ while(ValStart < ValEnd && isspace(Line[ValStart])){
+ ValStart += 1;
+ }
+
+ while(ValEnd > ValStart && isspace(Line[ValEnd - 1])){
+ ValEnd -= 1;
+ }
+
+ if(ValStart == ValEnd){
+ LOG_WARN("%s:%d: Empty value", FileName, LineNumber);
+ continue;
+ }
+
+ // NOTE(fusion): Parse KV pair.
+ char Key[256];
+ if(!StringCopyN(Key, (int)sizeof(Key), &Line[KeyStart], (KeyEnd - KeyStart))){
+ LOG_WARN("%s:%d: Exceeded key size limit of %d characters",
+ FileName, LineNumber, (int)(sizeof(Key) - 1));
+ continue;
+ }
+
+ char Val[256];
+ if(!StringCopyN(Val, (int)sizeof(Val), &Line[ValStart], (ValEnd - ValStart))){
+ LOG_WARN("%s:%d: Exceeded value size limit of %d characters",
+ FileName, LineNumber, (int)(sizeof(Val) - 1));
+ continue;
+ }
+
+ KVCallback(Key, Val);
+ }
+
+ fclose(File);
+ return true;
+}
diff --git a/src/common.hh b/src/common.hh
new file mode 100644
index 0000000..b4e88bc
--- /dev/null
+++ b/src/common.hh
@@ -0,0 +1,103 @@
+#ifndef TIBIA_LOGIN_COMMON_HH_
+#define TIBIA_LOGIN_COMMON_HH_ 1
+
+#include <ctype.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <algorithm>
+
+typedef uint8_t uint8;
+typedef uint16_t uint16;
+typedef uint32_t uint32;
+typedef int64_t int64;
+typedef uint64_t uint64;
+typedef size_t usize;
+
+#define STATIC_ASSERT(expr) static_assert((expr), "static assertion failed: " #expr)
+#define NARRAY(arr) (int)(sizeof(arr) / sizeof(arr[0]))
+#define ISPOW2(x) ((x) != 0 && ((x) & ((x) - 1)) == 0)
+#define KB(x) ((usize)(x) << 10)
+#define MB(x) ((usize)(x) << 20)
+#define GB(x) ((usize)(x) << 30)
+
+#if defined(_WIN32)
+# define OS_WINDOWS 1
+#elif defined(__linux__) || defined(__gnu_linux__)
+# define OS_LINUX 1
+#else
+# error "Operating system not supported."
+#endif
+
+#if defined(_MSC_VER)
+# define COMPILER_MSVC 1
+#elif defined(__GNUC__)
+# define COMPILER_GCC 1
+#elif defined(__clang__)
+# define COMPILER_CLANG 1
+#endif
+
+#if COMPILER_GCC || COMPILER_CLANG
+# define ATTR_FALLTHROUGH __attribute__((fallthrough))
+# define ATTR_PRINTF(x, y) __attribute__((format(printf, x, y)))
+#else
+# define ATTR_FALLTHROUGH
+# define ATTR_PRINTF(x, y)
+#endif
+
+#if COMPILER_MSVC
+# define TRAP() __debugbreak()
+#elif COMPILER_GCC || COMPILER_CLANG
+# define TRAP() __builtin_trap()
+#else
+# define TRAP() abort()
+#endif
+
+#define ASSERT_ALWAYS(expr) if(!(expr)) { TRAP(); }
+#if BUILD_DEBUG
+# define ASSERT(expr) ASSERT_ALWAYS(expr)
+#else
+# define ASSERT(expr) ((void)(expr))
+#endif
+
+#define LOG(...) LogAdd("INFO", __VA_ARGS__)
+#define LOG_WARN(...) LogAddVerbose("WARN", __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
+#define LOG_ERR(...) LogAddVerbose("ERR", __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
+#define PANIC(...) \
+ do{ \
+ LogAddVerbose("PANIC", __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__); \
+ TRAP(); \
+ }while(0)
+
+void LogAdd(const char *Prefix, const char *Format, ...) ATTR_PRINTF(2, 3);
+void LogAddVerbose(const char *Prefix, const char *Function,
+ const char *File, int Line, const char *Format, ...) ATTR_PRINTF(5, 6);
+
+struct tm GetLocalTime(time_t t);
+int64 GetClockMonotonicMS(void);
+void SleepMS(int64 DurationMS);
+
+bool StringEq(const char *A, const char *B);
+bool StringEqCI(const char *A, const char *B);
+bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength);
+bool StringCopy(char *Dest, int DestCapacity, const char *Src);
+bool EscapeString(char *Dest, int DestCapacity, const char *Src);
+uint32 HashString(const char *String);
+
+bool ParseIPAddress(int *Dest, const char *String);
+bool ParseBoolean(bool *Dest, const char *String);
+bool ParseInteger(int *Dest, const char *String);
+bool ParseDuration(int *Dest, const char *String);
+bool ParseSize(int *Dest, const char *String);
+bool ParseString(char *Dest, int DestCapacity, const char *String);
+
+typedef void ConfigKVCallback(const char *Key, const char *Val);
+bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback);
+
+#endif //TIBIA_LOGIN_COMMON_HH_
diff --git a/src/connections.cc b/src/connections.cc
new file mode 100644
index 0000000..5fd885f
--- /dev/null
+++ b/src/connections.cc
@@ -0,0 +1,554 @@
+#include "login.hh"
+
+// TODO(fusion): Support windows eventually?
+#if OS_LINUX
+# include <errno.h>
+# include <fcntl.h>
+# include <netinet/in.h>
+# include <poll.h>
+# include <sys/socket.h>
+# include <unistd.h>
+# include <time.h>
+#else
+# error "Operating system not currently supported."
+#endif
+
+static const int TERMINALVERSION[] = {770, 770, 770};
+static RSAKey *g_PrivateKey = NULL;
+static int g_Listener = -1;
+static TConnection *g_Connections = NULL;
+
+// Connection Handling
+//==============================================================================
+// NOTE(fusion): This is very similar to the connection handling code used by the
+// query manager with a few subtle differences including the encryption scheme.
+int ListenerBind(uint16 Port){
+ int Socket = socket(AF_INET, SOCK_STREAM, 0);
+ if(Socket == -1){
+ LOG_ERR("Failed to create listener socket: (%d) %s", errno, strerrordesc_np(errno));
+ return -1;
+ }
+
+ int ReuseAddr = 1;
+ if(setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR, &ReuseAddr, sizeof(ReuseAddr)) == -1){
+ LOG_ERR("Failed to set SO_REUSADDR: (%d) %s", errno, strerrordesc_np(errno));
+ close(Socket);
+ return -1;
+ }
+
+ int Flags = fcntl(Socket, F_GETFL);
+ if(Flags == -1){
+ LOG_ERR("Failed to get socket flags: (%d) %s", errno, strerrordesc_np(errno));
+ close(Socket);
+ return -1;
+ }
+
+ if(fcntl(Socket, F_SETFL, Flags | O_NONBLOCK) == -1){
+ LOG_ERR("Failed to set socket flags: (%d) %s", errno, strerrordesc_np(errno));
+ close(Socket);
+ return -1;
+ }
+
+ sockaddr_in Addr = {};
+ Addr.sin_family = AF_INET;
+ Addr.sin_port = htons(Port);
+ Addr.sin_addr.s_addr = htonl(INADDR_ANY);
+ if(bind(Socket, (sockaddr*)&Addr, sizeof(Addr)) == -1){
+ LOG_ERR("Failed to bind socket to port %d: (%d) %s", Port, errno, strerrordesc_np(errno));
+ close(Socket);
+ return -1;
+ }
+
+ if(listen(Socket, 128) == -1){
+ LOG_ERR("Failed to listen to port %d: (%d) %s", Port, errno, strerrordesc_np(errno));
+ close(Socket);
+ return -1;
+ }
+
+ return Socket;
+}
+
+int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){
+ while(true){
+ sockaddr_in SocketAddr = {};
+ socklen_t SocketAddrLen = sizeof(SocketAddr);
+ int Socket = accept(Listener, (sockaddr*)&SocketAddr, &SocketAddrLen);
+ if(Socket == -1){
+ if(errno != EAGAIN){
+ LOG_ERR("Failed to accept connection: (%d) %s", errno, strerrordesc_np(errno));
+ }
+ return -1;
+ }
+
+ int Flags = fcntl(Socket, F_GETFL);
+ if(Flags == -1){
+ LOG_ERR("Failed to get socket flags: (%d) %s", errno, strerrordesc_np(errno));
+ close(Socket);
+ continue;
+ }
+
+ if(fcntl(Socket, F_SETFL, Flags | O_NONBLOCK) == -1){
+ LOG_ERR("Failed to set socket flags: (%d) %s", errno, strerrordesc_np(errno));
+ close(Socket);
+ continue;
+ }
+
+ if(OutAddr){
+ *OutAddr = ntohl(SocketAddr.sin_addr.s_addr);
+ }
+
+ if(OutPort){
+ *OutPort = ntohs(SocketAddr.sin_port);
+ }
+
+ return Socket;
+ }
+}
+
+void CloseConnection(TConnection *Connection){
+ if(Connection->Socket != -1){
+ close(Connection->Socket);
+ Connection->Socket = -1;
+ }
+}
+
+TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
+ int ConnectionIndex = -1;
+ for(int i = 0; i < g_MaxConnections; i += 1){
+ if(g_Connections[i].State == CONNECTION_FREE){
+ ConnectionIndex = i;
+ break;
+ }
+ }
+
+ TConnection *Connection = NULL;
+ if(ConnectionIndex != -1){
+ Connection = &g_Connections[ConnectionIndex];
+ Connection->State = CONNECTION_HANDSHAKE;
+ Connection->Socket = Socket;
+ Connection->StartTime = g_MonotonicTimeMS;
+ Connection->RandomSeed = (uint32)rand();
+ snprintf(Connection->IPAddress, sizeof(Connection->IPAddress),
+ "%d.%d.%d.%d", ((int)(Addr >> 24) & 0xFF), ((int)(Addr >> 16) & 0xFF),
+ ((int)(Addr >> 8) & 0xFF), ((int)(Addr >> 0) & 0xFF));
+ snprintf(Connection->RemoteAddress, sizeof(Connection->RemoteAddress),
+ "%s:%d", Connection->IPAddress, (int)Port);
+ LOG("Connection %s assigned to slot %d",
+ Connection->RemoteAddress, ConnectionIndex);
+ }
+ return Connection;
+}
+
+void ReleaseConnection(TConnection *Connection){
+ if(Connection->State != CONNECTION_FREE){
+ LOG("Connection %s released", Connection->RemoteAddress);
+ CloseConnection(Connection);
+ memset(Connection, 0, sizeof(TConnection));
+ Connection->State = CONNECTION_FREE;
+ }
+}
+
+void CheckConnectionInput(TConnection *Connection, int Events){
+ if((Events & POLLIN) == 0 || Connection->Socket == -1){
+ return;
+ }
+
+ if(Connection->State != CONNECTION_HANDSHAKE){
+ LOG_ERR("Connection %s (State: %d) sending out-of-order data",
+ Connection->RemoteAddress, Connection->State);
+ CloseConnection(Connection);
+ return;
+ }
+
+ while(true){
+ int ReadSize = Connection->RWSize;
+ if(ReadSize == 0){
+ ReadSize = 2 - Connection->RWPosition;
+ ASSERT(ReadSize > 0);
+ }
+
+ int BytesRead = read(Connection->Socket,
+ (Connection->Buffer + Connection->RWPosition),
+ (ReadSize - Connection->RWPosition));
+ if(BytesRead == -1){
+ if(errno != EAGAIN){
+ // NOTE(fusion): Connection error.
+ CloseConnection(Connection);
+ }
+ break;
+ }else if(BytesRead == 0){
+ // NOTE(fusion): Graceful close.
+ CloseConnection(Connection);
+ break;
+ }
+
+ Connection->RWPosition += BytesRead;
+ if(Connection->RWPosition >= ReadSize){
+ if(Connection->RWSize == 0){
+ int PayloadSize = BufferRead16LE(Connection->Buffer);
+ if(PayloadSize <= 0 || PayloadSize > NARRAY(Connection->Buffer)){
+ CloseConnection(Connection);
+ break;
+ }
+
+ Connection->RWSize = PayloadSize;
+ Connection->RWPosition = 0;
+ }else{
+ Connection->State = CONNECTION_LOGIN;
+ break;
+ }
+ }
+ }
+
+ if(Connection->State == CONNECTION_LOGIN){
+ ProcessLoginRequest(Connection);
+ }
+}
+
+void CheckConnectionOutput(TConnection *Connection, int Events){
+ if((Events & POLLOUT) == 0 || Connection->Socket == -1){
+ return;
+ }
+
+ if(Connection->State != CONNECTION_WRITE){
+ return;
+ }
+
+ while(true){
+ int BytesWritten = write(Connection->Socket,
+ (Connection->Buffer + Connection->RWPosition),
+ (Connection->RWSize - Connection->RWPosition));
+ if(BytesWritten == -1){
+ if(errno != EAGAIN){
+ CloseConnection(Connection);
+ }
+ break;
+ }
+
+ Connection->RWPosition += BytesWritten;
+ if(Connection->RWPosition >= Connection->RWSize){
+ CloseConnection(Connection);
+ break;
+ }
+ }
+}
+
+void CheckConnection(TConnection *Connection, int Events){
+ ASSERT((Events & POLLNVAL) == 0);
+
+ if((Events & (POLLERR | POLLHUP)) != 0){
+ CloseConnection(Connection);
+ }
+
+ if(g_LoginTimeout > 0){
+ int LoginTime = (g_MonotonicTimeMS - Connection->StartTime);
+ if(LoginTime >= g_LoginTimeout){
+ LOG_WARN("Connection %s TIMEDOUT (LoginTime: %dms, Timeout: %dms)",
+ Connection->RemoteAddress, LoginTime, g_LoginTimeout);
+ CloseConnection(Connection);
+ }
+ }
+
+ if(Connection->Socket == -1){
+ ReleaseConnection(Connection);
+ }
+}
+
+void ProcessConnections(void){
+ // NOTE(fusion): Accept new connections.
+ while(true){
+ uint32 Addr;
+ uint16 Port;
+ int Socket = ListenerAccept(g_Listener, &Addr, &Port);
+ if(Socket == -1){
+ break;
+ }
+
+ if(AssignConnection(Socket, Addr, Port) == NULL){
+ LOG_ERR("Rejecting connection from %08X due to max number of"
+ " connections being reached (%d)", Addr, g_MaxConnections);
+ close(Socket);
+ }
+ }
+
+ // NOTE(fusion): Gather active connections.
+ int NumConnections = 0;
+ int *ConnectionIndices = (int*)alloca(g_MaxConnections * sizeof(int));
+ pollfd *ConnectionFds = (pollfd*)alloca(g_MaxConnections * sizeof(pollfd));
+ for(int i = 0; i < g_MaxConnections; i += 1){
+ if(g_Connections[i].State == CONNECTION_FREE || g_Connections[i].Socket == -1){
+ continue;
+ }
+
+ ConnectionIndices[NumConnections] = i;
+ ConnectionFds[NumConnections].fd = g_Connections[i].Socket;
+ ConnectionFds[NumConnections].events = POLLIN | POLLOUT;
+ ConnectionFds[NumConnections].revents = 0;
+ NumConnections += 1;
+ }
+
+ if(NumConnections <= 0){
+ return;
+ }
+
+ // NOTE(fusion): Poll connections.
+ int NumEvents = poll(ConnectionFds, NumConnections, 0);
+ if(NumEvents == -1){
+ LOG_ERR("Failed to poll connections: (%d) %s", errno, strerrordesc_np(errno));
+ return;
+ }
+
+ // NOTE(fusion): Process connections.
+ for(int i = 0; i < NumConnections; i += 1){
+ TConnection *Connection = &g_Connections[ConnectionIndices[i]];
+ int Events = (int)ConnectionFds[i].revents;
+ CheckConnectionInput(Connection, Events);
+ CheckConnectionOutput(Connection, Events);
+ CheckConnection(Connection, Events);
+ }
+}
+
+bool InitConnections(void){
+ ASSERT(g_Listener == -1);
+ ASSERT(g_Connections == NULL);
+
+ LOG("Login port: %d", g_LoginPort);
+ LOG("Max connections: %d", g_MaxConnections);
+ LOG("Login timeout: %dms", g_LoginTimeout);
+
+ g_PrivateKey = RSALoadPEM("tibia.pem");
+ if(g_PrivateKey == NULL){
+ LOG_ERR("Failed to load RSA key");
+ return false;
+ }
+
+ g_Listener = ListenerBind(g_LoginPort);
+ if(g_Listener == -1){
+ LOG_ERR("Failed to bind listener");
+ return false;
+ }
+
+ g_Connections = (TConnection*)calloc(g_MaxConnections, sizeof(TConnection));
+ for(int i = 0; i < g_MaxConnections; i += 1){
+ g_Connections[i].State = CONNECTION_FREE;
+ }
+
+ return true;
+}
+
+void ExitConnections(void){
+ if(g_PrivateKey != NULL){
+ RSAFree(g_PrivateKey);
+ g_PrivateKey = NULL;
+ }
+
+ if(g_Listener != -1){
+ close(g_Listener);
+ g_Listener = -1;
+ }
+
+ if(g_Connections != NULL){
+ for(int i = 0; i < g_MaxConnections; i += 1){
+ ReleaseConnection(&g_Connections[i]);
+ }
+
+ free(g_Connections);
+ g_Connections = NULL;
+ }
+}
+
+// Connection Requests
+//==============================================================================
+TWriteBuffer PrepareResponse(TConnection *Connection){
+ if(Connection->State != CONNECTION_LOGIN){
+ LOG_ERR("Connection %s is not processing login (State: %d)",
+ Connection->RemoteAddress, Connection->State);
+ CloseConnection(Connection);
+ return TWriteBuffer(NULL, 0);
+ }
+
+ TWriteBuffer WriteBuffer(Connection->Buffer, sizeof(Connection->Buffer));
+ WriteBuffer.Write16(0); // Encrypted Size
+ WriteBuffer.Write16(0); // Data Size
+ return WriteBuffer;
+}
+
+void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
+ if(Connection->State != CONNECTION_LOGIN){
+ LOG_ERR("Connection %s is not processing login (State: %d)",
+ Connection->RemoteAddress, Connection->State);
+ CloseConnection(Connection);
+ return;
+ }
+
+ ASSERT(WriteBuffer != NULL
+ && WriteBuffer->Buffer == Connection->Buffer
+ && WriteBuffer->Size == sizeof(Connection->Buffer)
+ && WriteBuffer->Position > 4);
+
+ int DataSize = WriteBuffer->Position - 4;
+ int EncryptedSize = WriteBuffer->Position - 2;
+ while((EncryptedSize % 8) != 0){
+ WriteBuffer->Write8(rand_r(&Connection->RandomSeed));
+ EncryptedSize += 1;
+ }
+
+ if(WriteBuffer->Overflowed()){
+ LOG_ERR("Write buffer overflowed when writing response to %s",
+ Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ WriteBuffer->Rewrite16(0, EncryptedSize);
+ WriteBuffer->Rewrite16(2, DataSize);
+ XTEAEncrypt(Connection->XTEA,
+ WriteBuffer->Buffer + 2,
+ WriteBuffer->Position - 2);
+ Connection->State = CONNECTION_WRITE;
+ Connection->RWSize = WriteBuffer->Position;
+ Connection->RWPosition = 0;
+}
+
+void SendLoginError(TConnection *Connection, const char *Message){
+ TWriteBuffer WriteBuffer = PrepareResponse(Connection);
+ WriteBuffer.Write8(10); // LOGIN_ERROR
+ WriteBuffer.WriteString(Message);
+ SendResponse(Connection, &WriteBuffer);
+}
+
+void SendCharacterList(TConnection *Connection, int NumCharacters,
+ TCharacterLoginData *Characters, int PremiumDays){
+ TWriteBuffer WriteBuffer = PrepareResponse(Connection);
+
+ if(g_Motd[0] != 0){
+ WriteBuffer.Write8(20); // MOTD
+ WriteBuffer.WriteString(g_Motd);
+ }
+
+ WriteBuffer.Write8(100); // CHARACTER_LIST
+ if(NumCharacters > UINT8_MAX){
+ NumCharacters = UINT8_MAX;
+ }
+ WriteBuffer.Write8(NumCharacters);
+ for(int i = 0; i < NumCharacters; i += 1){
+ WriteBuffer.WriteString(Characters[i].Name);
+ WriteBuffer.WriteString(Characters[i].WorldName);
+ WriteBuffer.Write32BE((uint32)Characters[i].WorldAddress);
+ WriteBuffer.Write16((uint16)Characters[i].WorldPort);
+ }
+ WriteBuffer.Write16((uint16)PremiumDays);
+
+ SendResponse(Connection, &WriteBuffer);
+}
+
+void ProcessLoginRequest(TConnection *Connection){
+ if(Connection->RWSize != 145){
+ LOG_ERR("Invalid login request size %d from %s (expected 145)",
+ Connection->RWSize, Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ TReadBuffer InputBuffer(Connection->Buffer, Connection->RWSize);
+ int Command = InputBuffer.Read8();
+ if(Command != 1){
+ LOG_ERR("Invalid login command %d from %s (expected 1)",
+ Command, Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ int TerminalType = InputBuffer.Read16();
+ int TerminalVersion = InputBuffer.Read16();
+ InputBuffer.Read32(); // DATSIGNATURE
+ InputBuffer.Read32(); // SPRSIGNATURE
+ InputBuffer.Read32(); // PICSIGNATURE
+
+ uint8 AsymmetricData[128];
+ InputBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData));
+ if(InputBuffer.Overflowed()){
+ LOG_ERR("Input buffer overflowed while reading login command from %s",
+ Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ if(!RSADecrypt(g_PrivateKey, AsymmetricData, sizeof(AsymmetricData))){
+ LOG_ERR("Failed to decrypt asymmetric data from %s",
+ Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ TReadBuffer Buffer(AsymmetricData, sizeof(AsymmetricData));
+ Buffer.Read8(); // always zero
+ Connection->XTEA[0] = Buffer.Read32();
+ Connection->XTEA[1] = Buffer.Read32();
+ Connection->XTEA[2] = Buffer.Read32();
+ Connection->XTEA[3] = Buffer.Read32();
+
+ if(TerminalType < 0 || TerminalType >= NARRAY(TERMINALVERSION)
+ || TERMINALVERSION[TerminalType] != TerminalVersion){
+ SendLoginError(Connection,
+ "Your terminal version is too old.\n"
+ "Please get a new version at\n"
+ "http://www.tibia.com.");
+ return;
+ }
+
+ char Password[30];
+ int AccountID = Buffer.Read32();
+ Buffer.ReadString(Password, sizeof(Password));
+ if(Buffer.Overflowed()){
+ LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ int NumCharacters = 0;
+ int PremiumDays = 0;
+ TCharacterLoginData Characters[50];
+ int LoginCode = LoginAccount(AccountID, Password, Connection->IPAddress,
+ NARRAY(Characters), &NumCharacters, Characters, &PremiumDays);
+ switch(LoginCode){
+ case 0:{
+ SendCharacterList(Connection, NumCharacters, Characters, PremiumDays);
+ break;
+ }
+
+ case 1: // Invalid account number
+ case 2:{ // Invalid password
+ SendLoginError(Connection, "Accountnumber or password is not correct.");
+ break;
+ }
+
+ case 3:{
+ SendLoginError(Connection, "Account disabled for five minutes. Please wait.");
+ break;
+ }
+
+ case 4:{
+ SendLoginError(Connection, "IP address blocked for 30 minutes. Please wait.");
+ break;
+ }
+
+ case 5:{
+ SendLoginError(Connection, "Your account is banished.");
+ break;
+ }
+
+ case 6:{
+ SendLoginError(Connection, "Your IP address is banished.");
+ break;
+ }
+
+ default:{
+ if(LoginCode != -1){
+ LOG_ERR("Invalid login code %d", LoginCode);
+ }
+ SendLoginError(Connection, "Internal error, closing connection.");
+ break;
+ }
+ }
+}
diff --git a/src/crypto.cc b/src/crypto.cc
new file mode 100644
index 0000000..8be90fd
--- /dev/null
+++ b/src/crypto.cc
@@ -0,0 +1,102 @@
+#include "login.hh"
+
+#include <openssl/err.h>
+#include <openssl/rsa.h>
+#include <openssl/pem.h>
+
+static void DumpOpenSSLErrors(const char *Where, const char *What){
+ LOG_ERR("OpenSSL error(s) while executing %s at %s:", What, Where);
+ ERR_print_errors_cb(
+ [](const char *str, usize len, void *u) -> int {
+ // NOTE(fusion): These error strings already have trailing newlines,
+ // for whatever reason.
+ if(len > 0 && str[len - 1] == '\n'){
+ len -= 1;
+ }
+
+ if(len > 0){
+ LOG_ERR("> %*s", (int)len, str);
+ }
+
+ return 1;
+ }, NULL);
+}
+
+RSAKey *RSALoadPEM(const char *FileName){
+ FILE *File = fopen(FileName, "rb");
+ if(File == NULL){
+ LOG_ERR("Failed to open \"%s\"", FileName);
+ return NULL;
+ }
+
+ RSA *Key = PEM_read_RSAPrivateKey(File, NULL, NULL, NULL);
+ fclose(File);
+ if(Key == NULL){
+ LOG_ERR("Failed to read key from \"%s\"", FileName);
+ DumpOpenSSLErrors("RSALoadPem", "PEM_read_PrivateKey");
+ }
+
+ return (RSAKey*)Key;
+}
+
+void RSAFree(RSAKey *Key){
+ RSA_free((RSA*)Key);
+}
+
+bool RSADecrypt(RSAKey *Key, uint8 *Data, int Size){
+ ASSERT(Data != NULL && Size > 0);
+ if(Key == NULL){
+ LOG_ERR("Key not initialized");
+ return false;
+ }
+
+ if(Size != RSA_size((RSA*)Key)){
+ LOG_ERR("Invalid data size %d (expected %d)", Size, RSA_size((RSA*)Key));
+ return false;
+ }
+
+ if(RSA_private_decrypt(Size, Data, Data, (RSA*)Key, RSA_NO_PADDING) == -1){
+ DumpOpenSSLErrors("RSADecrypt", "RSA_private_decrypt");
+ return false;
+ }
+
+ return true;
+}
+
+void XTEAEncrypt(const uint32 *Key, uint8 *Data, int Size){
+ ASSERT(Key != NULL);
+ while(Size >= 8){
+ uint32 Sum = 0x00000000UL;
+ uint32 Delta = 0x9E3779B9UL;
+ uint32 V0 = BufferRead32LE(&Data[0]);
+ uint32 V1 = BufferRead32LE(&Data[4]);
+ for(int i = 0; i < 32; i += 1){
+ V0 += (((V1 << 4) ^ (V1 >> 5)) + V1) ^ (Sum + Key[Sum & 3]);
+ Sum += Delta;
+ V1 += (((V0 << 4) ^ (V0 >> 5)) + V0) ^ (Sum + Key[(Sum >> 11) & 3]);
+ }
+ BufferWrite32LE(&Data[0], V0);
+ BufferWrite32LE(&Data[4], V1);
+ Data += 8;
+ Size -= 8;
+ }
+}
+
+void XTEADecrypt(const uint32 *Key, uint8 *Data, int Size){
+ ASSERT(Key != NULL);
+ while(Size >= 8){
+ uint32 Sum = 0xC6EF3720UL;
+ uint32 Delta = 0x9E3779B9UL;
+ uint32 V0 = BufferRead32LE(&Data[0]);
+ uint32 V1 = BufferRead32LE(&Data[4]);
+ for(int i = 0; i < 32; i += 1){
+ V1 -= (((V0 << 4) ^ (V0 >> 5)) + V0) ^ (Sum + Key[(Sum >> 11) & 3]);
+ Sum -= Delta;
+ V0 -= (((V1 << 4) ^ (V1 >> 5)) + V1) ^ (Sum + Key[Sum & 3]);
+ }
+ BufferWrite32LE(&Data[0], V0);
+ BufferWrite32LE(&Data[4], V1);
+ Data += 8;
+ Size -= 8;
+ }
+}
diff --git a/src/login.cc b/src/login.cc
new file mode 100644
index 0000000..a4fa5b6
--- /dev/null
+++ b/src/login.cc
@@ -0,0 +1,130 @@
+#include "login.hh"
+
+// TODO(fusion): Support windows eventually?
+#if OS_LINUX
+# include <errno.h>
+# include <signal.h>
+#else
+# error "Operating system not currently supported."
+#endif
+
+// Shutdown Signal
+int g_ShutdownSignal = 0;
+
+// Time
+int g_MonotonicTimeMS = 0;
+
+// Config
+char g_Motd[256] = "";
+int g_UpdateRate = 20;
+int g_LoginPort = 7171;
+int g_MaxConnections = 10;
+int g_LoginTimeout = 10000;
+char g_QueryManagerHost[100] = "127.0.0.1";
+int g_QueryManagerPort = 7174;
+char g_QueryManagerPassword[30] = "";
+
+static void ParseMotd(char *Dest, int DestCapacity, const char *String){
+ char *Motd = (char*)alloca(DestCapacity);
+ ParseString(Motd, DestCapacity, String);
+ if(Motd[0] != 0){
+ snprintf(Dest, DestCapacity, "%u\n%s", HashString(Motd), Motd);
+ }
+}
+
+static void LoginKVCallback(const char *Key, const char *Val){
+ if(StringEqCI(Key, "LoginPort")){
+ ParseInteger(&g_LoginPort, Val);
+ }else if(StringEqCI(Key, "MaxConnections")){
+ ParseInteger(&g_MaxConnections, Val);
+ }else if(StringEqCI(Key, "LoginTimeout")){
+ ParseDuration(&g_LoginTimeout, Val);
+ }else if(StringEqCI(Key, "UpdateRate")){
+ ParseInteger(&g_UpdateRate, Val);
+ }else if(StringEqCI(Key, "QueryManagerHost")){
+ ParseString(g_QueryManagerHost,
+ sizeof(g_QueryManagerHost), Val);
+ }else if(StringEqCI(Key, "QueryManagerPort")){
+ ParseInteger(&g_QueryManagerPort, Val);
+ }else if(StringEqCI(Key, "QueryManagerPassword")){
+ ParseString(g_QueryManagerPassword,
+ sizeof(g_QueryManagerPassword), Val);
+ }else if(StringEqCI(Key, "MOTD")){
+ ParseMotd(g_Motd, sizeof(g_Motd), Val);
+ }else{
+ LOG_WARN("Unknown config \"%s\"", Key);
+ }
+}
+
+static bool SigHandler(int SigNr, sighandler_t Handler){
+ struct sigaction Action = {};
+ Action.sa_handler = Handler;
+ sigfillset(&Action.sa_mask);
+ if(sigaction(SigNr, &Action, NULL) == -1){
+ LOG_ERR("Failed to change handler for signal %d (%s): (%d) %s",
+ SigNr, sigdescr_np(SigNr), errno, strerrordesc_np(errno));
+ return false;
+ }
+ return true;
+}
+
+static void ShutdownHandler(int SigNr){
+ g_ShutdownSignal = SigNr;
+}
+
+int main(int argc, char **argv){
+ (void)argc;
+ (void)argv;
+
+ g_ShutdownSignal = 0;
+ if(!SigHandler(SIGPIPE, SIG_IGN)
+ || !SigHandler(SIGINT, ShutdownHandler)
+ || !SigHandler(SIGTERM, ShutdownHandler)){
+ return EXIT_FAILURE;
+ }
+
+ int64 StartTime = GetClockMonotonicMS();
+ g_MonotonicTimeMS = 0;
+
+ LOG("Tibia Login Server v0.1");
+ if(!ReadConfig("config.cfg", LoginKVCallback)){
+ return EXIT_FAILURE;
+ }
+
+ atexit(ExitConnections);
+ atexit(ExitQuery);
+
+ if(!InitConnections()){
+ return EXIT_FAILURE;
+ }
+
+ if(!InitQuery()){
+ return EXIT_FAILURE;
+ }
+
+ // NOTE(fusion): Print MOTD with escape codes.
+ char Motd[30];
+ if(EscapeString(Motd, sizeof(Motd), g_Motd)){
+ LOG("MOTD: \"%s\"", Motd);
+ }else{
+ LOG("MOTD: \"%s...\"", Motd);
+ }
+
+ LOG("Running at %d updates per second...", g_UpdateRate);
+ int64 UpdateInterval = 1000 / (int64)g_UpdateRate;
+ while(g_ShutdownSignal == 0){
+ int64 UpdateStart = GetClockMonotonicMS();
+ g_MonotonicTimeMS = (int)(UpdateStart - StartTime);
+ ProcessConnections();
+ int64 UpdateEnd = GetClockMonotonicMS();
+ int64 NextUpdate = UpdateStart + UpdateInterval;
+ if(NextUpdate > UpdateEnd){
+ SleepMS(NextUpdate - UpdateEnd);
+ }
+ }
+
+ LOG("Received signal %d (%s), shutting down...",
+ g_ShutdownSignal, sigdescr_np(g_ShutdownSignal));
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/login.hh b/src/login.hh
new file mode 100644
index 0000000..99c8ceb
--- /dev/null
+++ b/src/login.hh
@@ -0,0 +1,401 @@
+#ifndef TIBIA_LOGIN_HH_
+#define TIBIA_LOGIN_HH_ 1
+
+#include "common.hh"
+
+// Shutdown Signal
+extern int g_ShutdownSignal;
+
+// Time
+extern int g_MonotonicTimeMS;
+
+// Config
+extern char g_Motd[256];
+extern int g_UpdateRate;
+extern int g_LoginPort;
+extern int g_MaxConnections;
+extern int g_LoginTimeout;
+extern char g_QueryManagerHost[100];
+extern int g_QueryManagerPort;
+extern char g_QueryManagerPassword[30];
+
+// Buffer Utility
+//==============================================================================
+inline uint8 BufferRead8(const uint8 *Buffer){
+ return Buffer[0];
+}
+
+inline uint16 BufferRead16LE(const uint8 *Buffer){
+ return (uint16)Buffer[0]
+ | ((uint16)Buffer[1] << 8);
+}
+
+inline uint16 BufferRead16BE(const uint8 *Buffer){
+ return ((uint16)Buffer[0] << 8)
+ | (uint16)Buffer[1];
+}
+
+inline uint32 BufferRead32LE(const uint8 *Buffer){
+ return (uint32)Buffer[0]
+ | ((uint32)Buffer[1] << 8)
+ | ((uint32)Buffer[2] << 16)
+ | ((uint32)Buffer[3] << 24);
+}
+
+inline uint32 BufferRead32BE(const uint8 *Buffer){
+ return ((uint32)Buffer[0] << 24)
+ | ((uint32)Buffer[1] << 16)
+ | ((uint32)Buffer[2] << 8)
+ | (uint32)Buffer[3];
+}
+
+inline uint64 BufferRead64LE(const uint8 *Buffer){
+ return (uint64)Buffer[0]
+ | ((uint64)Buffer[1] << 8)
+ | ((uint64)Buffer[2] << 16)
+ | ((uint64)Buffer[3] << 24)
+ | ((uint64)Buffer[4] << 32)
+ | ((uint64)Buffer[5] << 40)
+ | ((uint64)Buffer[6] << 48)
+ | ((uint64)Buffer[7] << 56);
+}
+
+inline uint64 BufferRead64BE(const uint8 *Buffer){
+ return ((uint64)Buffer[0] << 56)
+ | ((uint64)Buffer[1] << 48)
+ | ((uint64)Buffer[2] << 40)
+ | ((uint64)Buffer[3] << 32)
+ | ((uint64)Buffer[4] << 24)
+ | ((uint64)Buffer[5] << 16)
+ | ((uint64)Buffer[6] << 8)
+ | (uint64)Buffer[7];
+}
+
+inline void BufferWrite8(uint8 *Buffer, uint8 Value){
+ Buffer[0] = Value;
+}
+
+inline void BufferWrite16LE(uint8 *Buffer, uint16 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+}
+
+inline void BufferWrite16BE(uint8 *Buffer, uint16 Value){
+ Buffer[0] = (uint8)(Value >> 8);
+ Buffer[1] = (uint8)(Value >> 0);
+}
+
+inline void BufferWrite32LE(uint8 *Buffer, uint32 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+ Buffer[2] = (uint8)(Value >> 16);
+ Buffer[3] = (uint8)(Value >> 24);
+}
+
+inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
+ Buffer[0] = (uint8)(Value >> 24);
+ Buffer[1] = (uint8)(Value >> 16);
+ Buffer[2] = (uint8)(Value >> 8);
+ Buffer[3] = (uint8)(Value >> 0);
+}
+
+inline void BufferWrite64LE(uint8 *Buffer, uint64 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+ Buffer[2] = (uint8)(Value >> 16);
+ Buffer[3] = (uint8)(Value >> 24);
+ Buffer[4] = (uint8)(Value >> 32);
+ Buffer[5] = (uint8)(Value >> 40);
+ Buffer[6] = (uint8)(Value >> 48);
+ Buffer[7] = (uint8)(Value >> 56);
+}
+
+inline void BufferWrite64BE(uint8 *Buffer, uint64 Value){
+ Buffer[0] = (uint8)(Value >> 56);
+ Buffer[1] = (uint8)(Value >> 48);
+ Buffer[2] = (uint8)(Value >> 40);
+ Buffer[3] = (uint8)(Value >> 32);
+ Buffer[4] = (uint8)(Value >> 24);
+ Buffer[5] = (uint8)(Value >> 16);
+ Buffer[6] = (uint8)(Value >> 8);
+ Buffer[7] = (uint8)(Value >> 0);
+}
+
+struct TReadBuffer{
+ uint8 *Buffer;
+ int Size;
+ int Position;
+
+ TReadBuffer(void) : TReadBuffer(NULL, 0) {}
+ TReadBuffer(uint8 *Buffer, int Size)
+ : Buffer(Buffer), Size(Size), Position(0) {}
+
+ bool CanRead(int Bytes){
+ return (this->Position + Bytes) <= this->Size;
+ }
+
+ bool Overflowed(void){
+ return this->Position > this->Size;
+ }
+
+ bool ReadFlag(void){
+ return this->Read8() != 0x00;
+ }
+
+ uint8 Read8(void){
+ uint8 Result = 0;
+ if(this->CanRead(1)){
+ Result = BufferRead8(this->Buffer + this->Position);
+ }
+ this->Position += 1;
+ return Result;
+ }
+
+ uint16 Read16(void){
+ uint16 Result = 0;
+ if(this->CanRead(2)){
+ Result = BufferRead16LE(this->Buffer + this->Position);
+ }
+ this->Position += 2;
+ return Result;
+ }
+
+ uint16 Read16BE(void){
+ uint16 Result = 0;
+ if(this->CanRead(2)){
+ Result = BufferRead16BE(this->Buffer + this->Position);
+ }
+ this->Position += 2;
+ return Result;
+ }
+
+ uint32 Read32(void){
+ uint32 Result = 0;
+ if(this->CanRead(4)){
+ Result = BufferRead32LE(this->Buffer + this->Position);
+ }
+ this->Position += 4;
+ return Result;
+ }
+
+ uint32 Read32BE(void){
+ uint32 Result = 0;
+ if(this->CanRead(4)){
+ Result = BufferRead32BE(this->Buffer + this->Position);
+ }
+ this->Position += 4;
+ return Result;
+ }
+
+ void ReadString(char *Dest, int DestCapacity){
+ int Length = (int)this->Read16();
+ if(Length == 0xFFFF){
+ Length = (int)this->Read32();
+ }
+
+ if(Dest != NULL && DestCapacity > 0){
+ if(Length < DestCapacity && this->CanRead(Length)){
+ memcpy(Dest, this->Buffer + this->Position, Length);
+ Dest[Length] = 0;
+ }else{
+ Dest[0] = 0;
+ }
+ }
+
+ this->Position += Length;
+ }
+
+ void ReadBytes(uint8 *Buffer, int Count){
+ if(this->CanRead(Count)){
+ memcpy(Buffer, this->Buffer + this->Position, Count);
+ }
+ this->Position += Count;
+ }
+};
+
+struct TWriteBuffer{
+ uint8 *Buffer;
+ int Size;
+ int Position;
+
+ TWriteBuffer(void) : TWriteBuffer(NULL, 0) {}
+ TWriteBuffer(uint8 *Buffer, int Size)
+ : Buffer(Buffer), Size(Size), Position(0) {}
+
+ bool CanWrite(int Bytes){
+ return (this->Position + Bytes) <= this->Size;
+ }
+
+ bool Overflowed(void){
+ return this->Position > this->Size;
+ }
+
+ void WriteFlag(bool Value){
+ this->Write8(Value ? 0x01 : 0x00);
+ }
+
+ void Write8(uint8 Value){
+ if(this->CanWrite(1)){
+ BufferWrite8(this->Buffer + this->Position, Value);
+ }
+ this->Position += 1;
+ }
+
+ void Write16(uint16 Value){
+ if(this->CanWrite(2)){
+ BufferWrite16LE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 2;
+ }
+
+ void Write16BE(uint16 Value){
+ if(this->CanWrite(2)){
+ BufferWrite16BE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 2;
+ }
+
+ void Write32(uint32 Value){
+ if(this->CanWrite(4)){
+ BufferWrite32LE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 4;
+ }
+
+ void Write32BE(uint32 Value){
+ if(this->CanWrite(4)){
+ BufferWrite32BE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 4;
+ }
+
+ void WriteString(const char *String){
+ int StringLength = 0;
+ if(String != NULL){
+ StringLength = (int)strlen(String);
+ }
+
+ if(StringLength < 0xFFFF){
+ this->Write16((uint16)StringLength);
+ }else{
+ this->Write16(0xFFFF);
+ this->Write32((uint32)StringLength);
+ }
+
+ if(StringLength > 0 && this->CanWrite(StringLength)){
+ memcpy(this->Buffer + this->Position, String, StringLength);
+ }
+
+ this->Position += StringLength;
+ }
+
+ void Rewrite16(int Position, uint16 Value){
+ if((Position + 2) <= this->Position){
+ BufferWrite16LE(this->Buffer + Position, Value);
+ }
+ }
+
+ void Insert32(int Position, uint32 Value){
+ if(Position <= this->Position){
+ if(this->CanWrite(4)){
+ memmove(this->Buffer + Position + 4,
+ this->Buffer + Position,
+ this->Position - Position);
+ BufferWrite32LE(this->Buffer + Position, Value);
+ }
+
+ this->Position += 4;
+ }
+ }
+};
+
+// crypto.cc
+//==============================================================================
+typedef void RSAKey;
+RSAKey *RSALoadPEM(const char *FileName);
+void RSAFree(RSAKey *Key);
+bool RSADecrypt(RSAKey *Key, uint8 *Data, int Size);
+void XTEAEncrypt(const uint32 *Key, uint8 *Data, int Size);
+void XTEADecrypt(const uint32 *Key, uint8 *Data, int Size);
+
+// query.cc
+//==============================================================================
+enum : int {
+ APPLICATION_TYPE_GAME = 1,
+ APPLICATION_TYPE_LOGIN = 2,
+ APPLICATION_TYPE_WEB = 3,
+};
+
+enum : int {
+ QUERY_STATUS_OK = 0,
+ QUERY_STATUS_ERROR = 1,
+ QUERY_STATUS_FAILED = 3,
+};
+
+struct TCharacterLoginData{
+ char Name[30];
+ char WorldName[30];
+ int WorldAddress;
+ int WorldPort;
+};
+
+struct TQueryManagerConnection{
+ int Socket;
+ uint8 Buffer[KB(16)];
+};
+
+bool Connect(TQueryManagerConnection *Connection);
+void Disconnect(TQueryManagerConnection *Connection);
+bool IsConnected(TQueryManagerConnection *Connection);
+TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType);
+int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
+ TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer);
+int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
+ int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
+ int *PremiumDays);
+bool InitQuery(void);
+void ExitQuery(void);
+
+// connections.cc
+//==============================================================================
+enum ConnectionState{
+ CONNECTION_FREE = 0,
+ CONNECTION_HANDSHAKE = 1,
+ CONNECTION_LOGIN = 2,
+ CONNECTION_WRITE = 3,
+};
+
+struct TConnection{
+ ConnectionState State;
+ int Socket;
+ int StartTime;
+ int RWSize;
+ int RWPosition;
+ uint32 RandomSeed;
+ uint32 XTEA[4];
+ char IPAddress[16];
+ char RemoteAddress[30];
+ uint8 Buffer[KB(2)];
+};
+
+int ListenerBind(uint16 Port);
+int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort);
+void CloseConnection(TConnection *Connection);
+TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port);
+void ReleaseConnection(TConnection *Connection);
+void ProcessLoginRequest(TConnection *Connection);
+void CheckConnectionInput(TConnection *Connection, int Events);
+void CheckConnectionOutput(TConnection *Connection, int Events);
+void CheckConnection(TConnection *Connection, int Events);
+void ProcessConnections(void);
+bool InitConnections(void);
+void ExitConnections(void);
+
+TWriteBuffer PrepareResponse(TConnection *Connection);
+void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer);
+void SendLoginError(TConnection *Connection, const char *Message);
+void SendCharacterList(TConnection *Connection, int NumCharacters,
+ TCharacterLoginData *Characters, int PremiumDays);
+void ProcessLoginRequest(TConnection *Connection);
+
+#endif //TIBIA_LOGIN_HH_
diff --git a/src/query.cc b/src/query.cc
new file mode 100644
index 0000000..1335631
--- /dev/null
+++ b/src/query.cc
@@ -0,0 +1,285 @@
+#include "login.hh"
+
+// TODO(fusion): Support windows eventually?
+#if OS_LINUX
+# include <errno.h>
+# include <netdb.h>
+# include <sys/socket.h>
+# include <unistd.h>
+#else
+# error "Operating system not currently supported."
+#endif
+
+static TQueryManagerConnection *g_QueryManagerConnection;
+
+bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
+ ASSERT(HostName != NULL && OutAddr != NULL);
+ addrinfo *Result = NULL;
+ addrinfo Hints = {};
+ Hints.ai_family = AF_INET;
+ Hints.ai_socktype = SOCK_STREAM;
+ int ErrCode = getaddrinfo(HostName, NULL, &Hints, &Result);
+ if(ErrCode != 0){
+ LOG_ERR("Failed to resolve hostname \"%s\": %s", HostName, gai_strerror(ErrCode));
+ return false;
+ }
+
+ bool Resolved = false;
+ for(addrinfo *AddrInfo = Result;
+ AddrInfo != NULL;
+ AddrInfo = AddrInfo->ai_next){
+ if(AddrInfo->ai_family == AF_INET && AddrInfo->ai_socktype == SOCK_STREAM){
+ ASSERT(AddrInfo->ai_addrlen == sizeof(sockaddr_in));
+ *OutAddr = ((sockaddr_in*)AddrInfo->ai_addr)->sin_addr.s_addr;
+ Resolved = true;
+ break;
+ }
+ }
+ freeaddrinfo(Result);
+ return Resolved;
+}
+
+bool Connect(TQueryManagerConnection *Connection){
+ if(Connection->Socket != -1){
+ LOG_ERR("Already connected");
+ return false;
+ }
+
+ in_addr_t Addr;
+ if(!ResolveHostName(g_QueryManagerHost, &Addr)){
+ LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_QueryManagerHost);
+ return false;
+ }
+
+ Connection->Socket = socket(AF_INET, SOCK_STREAM, 0);
+ if(Connection->Socket == -1){
+ LOG_ERR("Failed to create socket: (%d) %s", errno, strerrordesc_np(errno));
+ return false;
+ }
+
+ sockaddr_in QueryManagerAddress = {};
+ QueryManagerAddress.sin_family = AF_INET;
+ QueryManagerAddress.sin_port = htons((uint16)g_QueryManagerPort);
+ QueryManagerAddress.sin_addr.s_addr = Addr;
+ if(connect(Connection->Socket, (sockaddr*)&QueryManagerAddress, sizeof(QueryManagerAddress)) == -1){
+ LOG_ERR("Failed to connect: (%d) %s", errno, strerrordesc_np(errno));
+ Disconnect(Connection);
+ return false;
+ }
+
+ TWriteBuffer WriteBuffer = PrepareQuery(Connection, 0);
+ WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN);
+ WriteBuffer.WriteString(g_QueryManagerPassword);
+ int Status = ExecuteQuery(Connection, false, &WriteBuffer, NULL);
+ if(Status != QUERY_STATUS_OK){
+ LOG_ERR("Failed to login to query manager (%d)", Status);
+ Disconnect(Connection);
+ return false;
+ }
+
+ return true;
+}
+
+void Disconnect(TQueryManagerConnection *Connection){
+ if(Connection->Socket != -1){
+ close(Connection->Socket);
+ Connection->Socket = -1;
+ }
+}
+
+bool IsConnected(TQueryManagerConnection *Connection){
+ return Connection->Socket != -1;
+}
+
+TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType){
+ TWriteBuffer WriteBuffer(Connection->Buffer, sizeof(Connection->Buffer));
+ WriteBuffer.Write16(0); // Request Size
+ WriteBuffer.Write8((uint8)QueryType);
+ return WriteBuffer;
+}
+
+static bool WriteExact(int Fd, const uint8 *Buffer, int Size){
+ int BytesToWrite = Size;
+ const uint8 *WritePtr = Buffer;
+ while(BytesToWrite > 0){
+ int Ret = (int)write(Fd, WritePtr, BytesToWrite);
+ if(Ret == -1){
+ return false;
+ }
+ BytesToWrite -= Ret;
+ WritePtr += Ret;
+ }
+ return true;
+}
+
+static bool ReadExact(int Fd, uint8 *Buffer, int Size){
+ int BytesToRead = Size;
+ uint8 *ReadPtr = Buffer;
+ while(BytesToRead > 0){
+ int Ret = (int)read(Fd, ReadPtr, BytesToRead);
+ if(Ret == -1 || Ret == 0){
+ return false;
+ }
+ BytesToRead -= Ret;
+ ReadPtr += Ret;
+ }
+ return true;
+}
+
+int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
+ TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer){
+ ASSERT(WriteBuffer != NULL
+ && WriteBuffer->Buffer == Connection->Buffer
+ && WriteBuffer->Size == sizeof(Connection->Buffer)
+ && WriteBuffer->Position > 2);
+
+ int RequestSize = WriteBuffer->Position - 2;
+ if(RequestSize < 0xFFFF){
+ WriteBuffer->Rewrite16(0, (uint16)RequestSize);
+ }else{
+ WriteBuffer->Rewrite16(0, 0xFFFF);
+ WriteBuffer->Insert32(2, (uint32)RequestSize);
+ }
+
+ if(WriteBuffer->Overflowed()){
+ LOG_ERR("Write buffer overflowed when writing request");
+ return QUERY_STATUS_FAILED;
+ }
+
+ const int MaxAttempts = 2;
+ for(int Attempts = 0; true; Attempts += 1){
+ int WriteSize = WriteBuffer->Position;
+ if(!IsConnected(Connection)){
+ if(!AutoReconnect){
+ return QUERY_STATUS_FAILED;
+ }
+
+ // IMPORTANT(fusion): There is no way around this. `Connect` will
+ // use the connection buffer to send the login query so we need to
+ // save and restore it to not lose any data. One improvement here
+ // would be to use a stack or statically allocated buffer, although
+ // using malloc/free should have no real impact on performance as
+ // we're already doing BLOCKING I/O here.
+ uint8 *TempBuffer = (uint8*)malloc(WriteSize);
+ memcpy(TempBuffer, Connection->Buffer, WriteSize);
+ bool Reconnected = Connect(Connection);
+ memcpy(Connection->Buffer, TempBuffer, WriteSize);
+ free(TempBuffer);
+
+ if(!Reconnected){
+ return QUERY_STATUS_FAILED;
+ }
+ }
+
+ if(!WriteExact(Connection->Socket, Connection->Buffer, WriteSize)){
+ Disconnect(Connection);
+ if(Attempts >= MaxAttempts){
+ LOG_ERR("Failed to write request");
+ return QUERY_STATUS_FAILED;
+ }
+ continue;
+ }
+
+ uint8 Help[4];
+ if(!ReadExact(Connection->Socket, Help, 2)){
+ Disconnect(Connection);
+ if(Attempts >= MaxAttempts){
+ LOG_ERR("Failed to read response size");
+ return QUERY_STATUS_FAILED;
+ }
+ continue;
+ }
+
+ int ResponseSize = BufferRead16LE(Help);
+ if(ResponseSize == 0xFFFF){
+ if(!ReadExact(Connection->Socket, Help, 4)){
+ Disconnect(Connection);
+ LOG_ERR("Failed to read response extended size");
+ return QUERY_STATUS_FAILED;
+ }
+ ResponseSize = BufferRead32LE(Help);
+ }
+
+ if(ResponseSize <= 0 || ResponseSize > (int)sizeof(Connection->Buffer)){
+ Disconnect(Connection);
+ LOG_ERR("Invalid response size %d (BufferSize: %d)",
+ ResponseSize, (int)sizeof(Connection->Buffer));
+ return QUERY_STATUS_FAILED;
+ }
+
+ if(!ReadExact(Connection->Socket, Connection->Buffer, ResponseSize)){
+ Disconnect(Connection);
+ LOG_ERR("Failed to read response");
+ return QUERY_STATUS_FAILED;
+ }
+
+ TReadBuffer ReadBuffer(Connection->Buffer, ResponseSize);
+ int Status = ReadBuffer.Read8();
+ if(OutReadBuffer){
+ *OutReadBuffer = ReadBuffer;
+ }
+ return Status;
+ }
+}
+
+int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
+ int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
+ int *PremiumDays){
+ TWriteBuffer WriteBuffer = PrepareQuery(g_QueryManagerConnection, 11);
+ WriteBuffer.Write32((uint32)AccountID);
+ WriteBuffer.WriteString(Password);
+ WriteBuffer.WriteString(IPAddress);
+
+ TReadBuffer ReadBuffer;
+ int Status = ExecuteQuery(g_QueryManagerConnection, true, &WriteBuffer, &ReadBuffer);
+ int Result = (Status == QUERY_STATUS_OK ? 0 : -1);
+ if(Status == QUERY_STATUS_OK){
+ *NumCharacters = ReadBuffer.Read8();
+ if(*NumCharacters > MaxCharacters){
+ LOG_ERR("Too many characters");
+ return -1;
+ }
+
+ for(int i = 0; i < *NumCharacters; i += 1){
+ ReadBuffer.ReadString(Characters[i].Name, sizeof(Characters[i].Name));
+ ReadBuffer.ReadString(Characters[i].WorldName, sizeof(Characters[i].WorldName));
+ Characters[i].WorldAddress = ReadBuffer.Read32BE();
+ Characters[i].WorldPort = ReadBuffer.Read16();
+ }
+
+ *PremiumDays = ReadBuffer.Read16();
+ }else if(Status == QUERY_STATUS_ERROR){
+ int ErrorCode = ReadBuffer.Read8();
+ if(ErrorCode >= 1 && ErrorCode <= 6){
+ Result = ErrorCode;
+ }else{
+ LOG_ERR("Invalid error code %d", ErrorCode);
+ }
+ }else{
+ LOG_ERR("Request failed");
+ }
+ return Result;
+}
+
+bool InitQuery(void){
+ ASSERT(g_QueryManagerConnection == NULL);
+
+ LOG("QueryManagerHost: %s", g_QueryManagerHost);
+ LOG("QueryManagerPort: %d", g_QueryManagerPort);
+
+ g_QueryManagerConnection = (TQueryManagerConnection*)calloc(1, sizeof(TQueryManagerConnection));
+ g_QueryManagerConnection->Socket = -1;
+ if(!Connect(g_QueryManagerConnection)){
+ LOG_ERR("Failed to connect to query manager");
+ return false;
+ }
+ return true;
+}
+
+void ExitQuery(void){
+ if(g_QueryManagerConnection != NULL){
+ Disconnect(g_QueryManagerConnection);
+ free(g_QueryManagerConnection);
+ g_QueryManagerConnection = NULL;
+ }
+}
diff --git a/tibia.pem b/tibia.pem
new file mode 100644
index 0000000..83dbe83
--- /dev/null
+++ b/tibia.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDLoibNahc41DeQhZUTth3FrhMNYek5xbQrlZ0O4BFr0LukdGY1
+A3xHKAXt4d6Y539cQxpKqVPb+eSo44076c1PfL31BTDNFToN4uB3eUsOw3hROPSx
+0Jkh72FAIhl6QynId0AHyrqeAgY59ZsTyXyJCSCWpFnl0WvK3Qf6nVBH/wIDAQAB
+AoGAB9DoA19sl8JRhasS70hAuUs2sP9Ol+iWQ0wBVMZV9Nj0stnC6IsDNKn9HEXc
+qOrN0SlEM5RvQxTC6ZaeX6vYNQDSJj/aigSyiNTMvw+38TD09z6EBlI4+0piGIJd
+qY1ky0w3bdVhWiY2VFyGZ50NkfwjrpZSdLmsbhExzR4b/mkCQQDldLUeePo3RKZD
+7+CVLFkTX/Yof8RLPOh1nukgftpvgaGcD1FXSwrIyKCHVAuwFnGgPfEr/2OAj60D
+7Ya6XPeHAkEA4zC3U67ukzQxfyELrLTUBnsnKfkxejySsJ/M29SKCuHV2NOWxO8W
+s9/c0RFvp0wI4uOxH00fIPYyI2h6TpxZyQJBAKQ0ZPktskKjCilMHPgkCIro/Yv2
+A0+kgubJliP/I+rwZer8u0UxGsKdcOPnrYWSSjZWnaTS2y5Bo5tP/D6aETkCQDAc
+AtZPtumpJcob/1LlP/jXX2W+BUIzTYTlcgYjLdA8HoK527V8Q7x3bVVAcfplWYRi
+XwGX3T2npNpmp2+6IDECQBZuNbXiwHhDdsH7sSt36BGTFJNtVk3GJl/Bf50VrL6N
+6NBNU3d4jQwArvSsanFYrZw+ayxS4rcJdZasO5mm+EI=
+-----END RSA PRIVATE KEY-----