diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-07-18 18:04:54 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-07-18 18:04:54 -0300 |
| commit | baaeac349dc5da2cb4892662c69566a98a66a124 (patch) | |
| tree | 634b3d4be3f659290f0271402f3c68d061c686d1 /src | |
| download | login-baaeac349dc5da2cb4892662c69566a98a66a124.tar.gz login-baaeac349dc5da2cb4892662c69566a98a66a124.zip | |
initial version
Diffstat (limited to 'src')
| -rw-r--r-- | src/common.cc | 373 | ||||
| -rw-r--r-- | src/common.hh | 103 | ||||
| -rw-r--r-- | src/connections.cc | 554 | ||||
| -rw-r--r-- | src/crypto.cc | 102 | ||||
| -rw-r--r-- | src/login.cc | 130 | ||||
| -rw-r--r-- | src/login.hh | 401 | ||||
| -rw-r--r-- | src/query.cc | 285 |
7 files changed, 1948 insertions, 0 deletions
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; + } +} |
