diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-11-18 15:35:22 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-11-18 15:35:22 -0300 |
| commit | 6036d4f37ad97ee1a9a830b36fb2edf9b82e3a3b (patch) | |
| tree | ce91fb0a9acd8d95fbbe58a9d19664162a30a7cf /src | |
| parent | b101f3809e509df16c71e1415e9ebb0c687601b8 (diff) | |
| download | login-6036d4f37ad97ee1a9a830b36fb2edf9b82e3a3b.tar.gz login-6036d4f37ad97ee1a9a830b36fb2edf9b82e3a3b.zip | |
overall improvements + support STATUS requests
Diffstat (limited to 'src')
| -rw-r--r-- | src/common.cc | 375 | ||||
| -rw-r--r-- | src/common.hh | 520 | ||||
| -rw-r--r-- | src/connections.cc | 420 | ||||
| -rw-r--r-- | src/crypto.cc | 6 | ||||
| -rw-r--r-- | src/login.cc | 130 | ||||
| -rw-r--r-- | src/login.hh | 401 | ||||
| -rw-r--r-- | src/main.cc | 709 | ||||
| -rw-r--r-- | src/query.cc | 180 | ||||
| -rw-r--r-- | src/status.cc | 222 |
9 files changed, 1815 insertions, 1148 deletions
diff --git a/src/common.cc b/src/common.cc deleted file mode 100644 index c4786e2..0000000 --- a/src/common.cc +++ /dev/null @@ -1,375 +0,0 @@ -#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); - fflush(stdout); - } -} - -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); - fflush(stdout); - } -} - -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 index b4e88bc..fd87888 100644 --- a/src/common.hh +++ b/src/common.hh @@ -1,31 +1,25 @@ -#ifndef TIBIA_LOGIN_COMMON_HH_ -#define TIBIA_LOGIN_COMMON_HH_ 1 +#ifndef TIBIA_COMMON_HH_ +#define TIBIA_COMMON_HH_ 1 #include <ctype.h> -#include <limits.h> #include <stdarg.h> #include <stddef.h> -#include <stdint.h> #include <stdio.h> +#include <stdint.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) +#define KB(x) ((x) * 1024) #if defined(_WIN32) # define OS_WINDOWS 1 @@ -60,7 +54,7 @@ typedef size_t usize; #endif #define ASSERT_ALWAYS(expr) if(!(expr)) { TRAP(); } -#if BUILD_DEBUG +#if ENABLE_ASSERTIONS # define ASSERT(expr) ASSERT_ALWAYS(expr) #else # define ASSERT(expr) ((void)(expr)) @@ -75,29 +69,517 @@ typedef size_t usize; TRAP(); \ }while(0) +struct TConfig { + // Service Config + int LoginPort; + int ConnectionTimeout; + int MaxConnections; + int MaxStatusRecords; + int MinStatusInterval; + char QueryManagerHost[100]; + int QueryManagerPort; + char QueryManagerPassword[30]; + + // Service Info + char StatusWorld[30]; + char Url[100]; + char Location[30]; + char ServerType[30]; + char ServerVersion[30]; + char ClientVersion[30]; + char Motd[256]; +}; + +extern TConfig g_Config; + 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); +struct tm GetGMTime(time_t t); int64 GetClockMonotonicMS(void); -void SleepMS(int64 DurationMS); +int GetMonotonicUptime(void); +bool StringEmpty(const char *String); 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 StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength); +bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...) ATTR_PRINTF(3, 4); +bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp); +void StringClear(char *Dest, int DestCapacity); +uint32 StringHash(const char *String); +bool StringEscape(char *Dest, int DestCapacity, const char *Src); + +int UTF8SequenceSize(uint8 LeadingByte); +bool UTF8IsTrailingByte(uint8 Byte); +int UTF8EncodedSize(int Codepoint); +int UTF8FindNextLeadingByte(const char *Src, int SrcLength); +int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint); +int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint); +int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength); +int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength); -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); +void ParseMotd(char *Dest, int DestCapacity, const char *String); +bool ReadConfig(const char *FileName, TConfig *Config); + +// IMPORTANT(fusion): These macros should only be used when `Dest` is a char array +// to simplify the call to `StringCopy` where we'd use `sizeof(Dest)` to determine +// the size of the destination anyways. +#define StringBufCopy(Dest, Src) StringCopy(Dest, sizeof(Dest), Src) +#define StringBufCopyN(Dest, Src, SrcLength) StringCopyN(Dest, sizeof(Dest), Src, SrcLength) +#define StringBufFormat(Dest, ...) StringFormat(Dest, sizeof(Dest), __VA_ARGS__) +#define StringBufFormatTime(Dest, Format, Timestamp) \ + StringFormatTime(Dest, sizeof(Dest), Format, Timestamp) +#define StringBufClear(Dest) StringClear(Dest, sizeof(Dest)); +#define StringBufEscape(Dest, Src) StringEscape(Dest, sizeof(Dest), Src) +#define ParseStringBuf(Dest, String) ParseString(Dest, sizeof(Dest), String) + +// 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; + } + +#if CLIENT_ENCODING_UTF8 + void ReadString(char *Dest, int DestCapacity){ + int Length = (int)this->Read16(); + if(Length == 0xFFFF){ + Length = (int)this->Read32(); + } + + if(Dest != NULL && DestCapacity > 0){ + int Written = 0; + if(this->CanRead(Length) && Length < DestCapacity){ + memcpy(Dest, this->Buffer + this->Position, Length); + Written = Length; + } + memset((Dest + Written), 0, (DestCapacity - Written)); + } + + this->Position += Length; + } +#else + void ReadString(char *Dest, int DestCapacity){ + int Length = (int)this->Read16(); + if(Length == 0xFFFF){ + Length = (int)this->Read32(); + } + + if(Dest != NULL && DestCapacity > 0){ + int Written = 0; + if(this->CanRead(Length)){ + const char *Src = (const char*)(this->Buffer + this->Position); + Written = Latin1ToUTF8(Dest, DestCapacity, Src, Length); + if(Written >= DestCapacity){ + Written = 0; + } + } + + memset((Dest + Written), 0, (DestCapacity - Written)); + } + + this->Position += Length; + } +#endif + + 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; + } + +#if CLIENT_ENCODING_UTF8 + 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; + } +#else + void WriteString(const char *String){ + int StringLength = 0; + int OutputLength = 0; + if(String != NULL){ + StringLength = (int)strlen(String); + OutputLength = UTF8ToLatin1(NULL, 0, String, (int)strlen(String)); + } + + if(OutputLength < 0xFFFF){ + this->Write16((uint16)OutputLength); + }else{ + this->Write16(0xFFFF); + this->Write32((uint32)OutputLength); + } + + if(OutputLength > 0 && this->CanWrite(OutputLength)){ + int Written = UTF8ToLatin1((char*)(this->Buffer + this->Position), + (this->Size - this->Position), String, StringLength); + ASSERT(Written == OutputLength); + } + + this->Position += OutputLength; + } +#endif + + void Rewrite16(int Position, uint16 Value){ + if((Position + 2) <= this->Position && !this->Overflowed()){ + 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 { + APPLICATION_TYPE_GAME = 1, + APPLICATION_TYPE_LOGIN = 2, + APPLICATION_TYPE_WEB = 3, +}; + +enum { + QUERY_STATUS_OK = 0, + QUERY_STATUS_ERROR = 1, + QUERY_STATUS_FAILED = 3, +}; + +enum { + QUERY_LOGIN = 0, + QUERY_LOGIN_ACCOUNT = 11, + QUERY_GET_WORLDS = 150, +}; + +struct TQueryManagerConnection{ + int Socket; +}; + +struct TCharacterLoginData{ + char Name[30]; + char WorldName[30]; + int WorldAddress; + int WorldPort; +}; + +struct TWorld { + char Name[30]; + int Type; + int NumPlayers; + int MaxPlayers; + int OnlinePeak; + int OnlinePeakTimestamp; + int LastStartup; + int LastShutdown; +}; + +bool Connect(TQueryManagerConnection *Connection); +void Disconnect(TQueryManagerConnection *Connection); +bool IsConnected(TQueryManagerConnection *Connection); +TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize); +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); +int GetWorld(const char *WorldName, TWorld *OutWorld); +bool InitQuery(void); +void ExitQuery(void); + +// status.cc +//============================================================================== +const char *GetStatusString(void); + +// connections.cc +//============================================================================== +enum ConnectionState { + CONNECTION_FREE = 0, + CONNECTION_READING = 1, + CONNECTION_PROCESSING = 2, + CONNECTION_WRITING = 3, +}; + +struct TConnection { + ConnectionState State; + int Socket; + int IPAddress; + int StartTime; + int RWSize; + int RWPosition; + uint32 RandomSeed; + uint32 XTEA[4]; + char RemoteAddress[32]; + uint8 Buffer[KB(2)]; +}; + +struct TStatusRecord { + int IPAddress; + int Timestamp; +}; -typedef void ConfigKVCallback(const char *Key, const char *Val); -bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback); +void ProcessConnections(void); +bool InitConnections(void); +void ExitConnections(void); +void ProcessLoginRequest(TConnection *Connection); +void ProcessStatusRequest(TConnection *Connection); -#endif //TIBIA_LOGIN_COMMON_HH_ +#endif //TIBIA_COMMON_H_ diff --git a/src/connections.cc b/src/connections.cc index ef89f66..774a21d 100644 --- a/src/connections.cc +++ b/src/connections.cc @@ -1,33 +1,30 @@ -#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 +#include "common.hh" + +#include <errno.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <poll.h> +#include <sys/socket.h> +#include <unistd.h> #if TIBIA772 -static const int TERMINALVERSION[] = {772, 772, 772}; +static const int TERMINALVERSION[] = {772, 772, 772}; #else -static const int TERMINALVERSION[] = {770, 770, 770}; +static const int TERMINALVERSION[] = {770, 770, 770}; #endif -static RSAKey *g_PrivateKey = NULL; -static int g_Listener = -1; -static TConnection *g_Connections = NULL; +static RSAKey *g_PrivateKey; + +static int g_Listener = -1; +static TConnection *g_Connections; +static int g_MaxConnections; + +static TStatusRecord *g_StatusRecords; +static int g_MaxStatusRecords; // 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){ +static 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)); @@ -73,7 +70,7 @@ int ListenerBind(uint16 Port){ return Socket; } -int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){ +static int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){ while(true){ sockaddr_in SocketAddr = {}; socklen_t SocketAddrLen = sizeof(SocketAddr); @@ -110,16 +107,16 @@ int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){ } } -void CloseConnection(TConnection *Connection){ +static void CloseConnection(TConnection *Connection){ if(Connection->Socket != -1){ close(Connection->Socket); Connection->Socket = -1; } } -TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){ +static TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){ int ConnectionIndex = -1; - for(int i = 0; i < g_MaxConnections; i += 1){ + for(int i = 0; i < g_Config.MaxConnections; i += 1){ if(g_Connections[i].State == CONNECTION_FREE){ ConnectionIndex = i; break; @@ -129,22 +126,25 @@ TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){ TConnection *Connection = NULL; if(ConnectionIndex != -1){ Connection = &g_Connections[ConnectionIndex]; - Connection->State = CONNECTION_HANDSHAKE; + Connection->State = CONNECTION_READING; Connection->Socket = Socket; - Connection->StartTime = g_MonotonicTimeMS; + Connection->IPAddress = (int)Addr; + Connection->StartTime = GetMonotonicUptime(); 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); + StringBufFormat(Connection->RemoteAddress, + "%d.%d.%d.%d:%d", + ((Connection->IPAddress >> 24) & 0xFF), + ((Connection->IPAddress >> 16) & 0xFF), + ((Connection->IPAddress >> 8) & 0xFF), + ((Connection->IPAddress >> 0) & 0xFF), + (int)Port); LOG("Connection %s assigned to slot %d", Connection->RemoteAddress, ConnectionIndex); } return Connection; } -void ReleaseConnection(TConnection *Connection){ +static void ReleaseConnection(TConnection *Connection){ if(Connection->State != CONNECTION_FREE){ LOG("Connection %s released", Connection->RemoteAddress); CloseConnection(Connection); @@ -153,12 +153,12 @@ void ReleaseConnection(TConnection *Connection){ } } -void CheckConnectionInput(TConnection *Connection, int Events){ - if((Events & POLLIN) == 0 || Connection->Socket == -1){ +static void CheckConnectionInput(TConnection *Connection, int Events){ + if(Connection->Socket == -1 || (Events & POLLIN) == 0){ return; } - if(Connection->State != CONNECTION_HANDSHAKE){ + if(Connection->State != CONNECTION_READING){ LOG_ERR("Connection %s (State: %d) sending out-of-order data", Connection->RemoteAddress, Connection->State); CloseConnection(Connection); @@ -166,13 +166,8 @@ void CheckConnectionInput(TConnection *Connection, int Events){ } while(true){ - int ReadSize = Connection->RWSize; - if(ReadSize == 0){ - ReadSize = 2 - Connection->RWPosition; - ASSERT(ReadSize > 0); - } - - int BytesRead = read(Connection->Socket, + int ReadSize = (Connection->RWSize > 0 ? Connection->RWSize : 2); + int BytesRead = (int)read(Connection->Socket, (Connection->Buffer + Connection->RWPosition), (ReadSize - Connection->RWPosition)); if(BytesRead == -1){ @@ -189,9 +184,12 @@ void CheckConnectionInput(TConnection *Connection, int Events){ Connection->RWPosition += BytesRead; if(Connection->RWPosition >= ReadSize){ - if(Connection->RWSize == 0){ - int PayloadSize = BufferRead16LE(Connection->Buffer); - if(PayloadSize <= 0 || PayloadSize > NARRAY(Connection->Buffer)){ + if(Connection->RWSize != 0){ + Connection->State = CONNECTION_PROCESSING; + break; + }else if(Connection->RWPosition == 2){ + int PayloadSize = (int)BufferRead16LE(Connection->Buffer); + if(PayloadSize <= 0 || PayloadSize > (int)sizeof(Connection->Buffer)){ CloseConnection(Connection); break; } @@ -199,28 +197,54 @@ void CheckConnectionInput(TConnection *Connection, int Events){ Connection->RWSize = PayloadSize; Connection->RWPosition = 0; }else{ - Connection->State = CONNECTION_LOGIN; - break; + PANIC("Invalid input state (State: %d, RWSize: %d, RWPosition: %d)", + Connection->State, Connection->RWSize, Connection->RWPosition); } } } +} - if(Connection->State == CONNECTION_LOGIN){ +static void CheckConnectionRequest(TConnection *Connection){ + if(Connection->Socket == -1){ + return; + } + + if(Connection->State != CONNECTION_PROCESSING){ + return; + } + + // PARANOID(fusion): A non-empty payload is guaranteed, due to how we parse + // input in `CheckConnectionInput` just above. + ASSERT(Connection->RWSize > 0); + + int Command = Connection->Buffer[0]; + if(Command == 1){ ProcessLoginRequest(Connection); + }else if(Command == 255){ + ProcessStatusRequest(Connection); + }else{ + LOG_ERR("Invalid command %d from %s (expected 1 or 255)", + Command, Connection->RemoteAddress); + CloseConnection(Connection); } } -void CheckConnectionOutput(TConnection *Connection, int Events){ - if((Events & POLLOUT) == 0 || Connection->Socket == -1){ +static void CheckConnectionOutput(TConnection *Connection, int Events){ + // NOTE(fusion): We're only polling `POLLOUT` when the connection is WRITING, + // but we want to allow requests to complete in a single cycle, so we always + // check for output if the connection is WRITING. + (void)Events; + + if(Connection->Socket == -1){ return; } - if(Connection->State != CONNECTION_WRITE){ + if(Connection->State != CONNECTION_WRITING){ return; } while(true){ - int BytesWritten = write(Connection->Socket, + int BytesWritten = (int)write(Connection->Socket, (Connection->Buffer + Connection->RWPosition), (Connection->RWSize - Connection->RWPosition)); if(BytesWritten == -1){ @@ -238,18 +262,18 @@ void CheckConnectionOutput(TConnection *Connection, int Events){ } } -void CheckConnection(TConnection *Connection, int Events){ +static 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); + if(g_Config.ConnectionTimeout > 0){ + int ElapsedTime = GetMonotonicUptime() - Connection->StartTime; + if(ElapsedTime >= g_Config.ConnectionTimeout){ + LOG_WARN("Connection %s TIMEDOUT (ElapsedTime: %ds, Timeout: %ds)", + Connection->RemoteAddress, ElapsedTime, g_Config.ConnectionTimeout); CloseConnection(Connection); } } @@ -259,8 +283,12 @@ void CheckConnection(TConnection *Connection, int Events){ } } -void ProcessConnections(void){ - // NOTE(fusion): Accept new connections. +static void AcceptConnections(int Events){ + ASSERT(g_Listener != -1); + if((Events & POLLIN) == 0){ + return; + } + while(true){ uint32 Addr; uint16 Port; @@ -270,56 +298,78 @@ void ProcessConnections(void){ } if(AssignConnection(Socket, Addr, Port) == NULL){ - LOG_ERR("Rejecting connection from %08X:%d due to max number of" - " connections being reached (%d)", Addr, Port, g_MaxConnections); + LOG_ERR("Rejecting connection %08X:%d:" + " max number of connections reached (%d)", + Addr, Port, g_Config.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; - } +void ProcessConnections(void){ + int NumFds = 0; + int MaxFds = g_MaxConnections + 1; + pollfd *Fds = (pollfd*)alloca(MaxFds * sizeof(pollfd)); + int *ConnectionIndices = (int*)alloca(MaxFds * sizeof(int)); - ConnectionIndices[NumConnections] = i; - ConnectionFds[NumConnections].fd = g_Connections[i].Socket; - ConnectionFds[NumConnections].events = POLLIN | POLLOUT; - ConnectionFds[NumConnections].revents = 0; - NumConnections += 1; + if(g_Listener != -1){ + Fds[NumFds].fd = g_Listener; + Fds[NumFds].events = POLLIN; + Fds[NumFds].revents = 0; + ConnectionIndices[NumFds] = -1; + NumFds += 1; } - if(NumConnections <= 0){ - return; + for(int i = 0; i < g_Config.MaxConnections; i += 1){ + if(g_Connections[i].State == CONNECTION_FREE){ + continue; + } + + Fds[NumFds].fd = g_Connections[i].Socket; + Fds[NumFds].events = POLLIN; + if(g_Connections[i].State == CONNECTION_WRITING){ + Fds[NumFds].events |= POLLOUT; + } + Fds[NumFds].revents = 0; + ConnectionIndices[NumFds] = i; + NumFds += 1; } - // NOTE(fusion): Poll connections. - int NumEvents = poll(ConnectionFds, NumConnections, 0); + // NOTE(fusion): Block for 1 second at most, so we can properly timeout + // idle connections. + ASSERT(NumFds > 0); + int NumEvents = poll(Fds, NumFds, 1); if(NumEvents == -1){ - LOG_ERR("Failed to poll connections: (%d) %s", errno, strerrordesc_np(errno)); + if(errno != ETIMEDOUT && errno != EINTR){ + 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); + for(int i = 0; i < NumFds; i += 1){ + int Index = ConnectionIndices[i]; + int Events = (int)Fds[i].revents; + if(Index >= 0 && Index < g_Config.MaxConnections){ + TConnection *Connection = &g_Connections[Index]; + CheckConnectionInput(Connection, Events); + CheckConnectionRequest(Connection); + CheckConnectionOutput(Connection, Events); + CheckConnection(Connection, Events); + }else if(Index == -1 && Fds[i].fd == g_Listener){ + AcceptConnections(Events); + }else{ + LOG_ERR("Unknown connection index %d", Index); + } } } bool InitConnections(void){ + ASSERT(g_PrivateKey == NULL); 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); + ASSERT(g_StatusRecords == NULL); g_PrivateKey = RSALoadPEM("tibia.pem"); if(g_PrivateKey == NULL){ @@ -327,17 +377,23 @@ bool InitConnections(void){ return false; } - g_Listener = ListenerBind(g_LoginPort); + g_Listener = ListenerBind((uint16)g_Config.LoginPort); if(g_Listener == -1){ - LOG_ERR("Failed to bind listener"); + LOG_ERR("Failed to bind listener to port %d", g_Config.LoginPort); return false; } - g_Connections = (TConnection*)calloc(g_MaxConnections, sizeof(TConnection)); + g_MaxConnections = g_Config.MaxConnections; + g_Connections = (TConnection*)calloc( + g_MaxConnections, sizeof(TConnection)); for(int i = 0; i < g_MaxConnections; i += 1){ g_Connections[i].State = CONNECTION_FREE; } + g_MaxStatusRecords = g_Config.MaxStatusRecords; + g_StatusRecords = (TStatusRecord*)calloc( + g_MaxStatusRecords, sizeof(TStatusRecord)); + return true; } @@ -360,13 +416,18 @@ void ExitConnections(void){ free(g_Connections); g_Connections = NULL; } + + if(g_StatusRecords != NULL){ + free(g_StatusRecords); + g_StatusRecords = NULL; + } } -// Connection Requests +// Login Request //============================================================================== -TWriteBuffer PrepareResponse(TConnection *Connection){ - if(Connection->State != CONNECTION_LOGIN){ - LOG_ERR("Connection %s is not processing login (State: %d)", +static TWriteBuffer PrepareXTEAResponse(TConnection *Connection){ + if(Connection->State != CONNECTION_PROCESSING){ + LOG_ERR("Connection %s is not PROCESSING (State: %d)", Connection->RemoteAddress, Connection->State); CloseConnection(Connection); return TWriteBuffer(NULL, 0); @@ -378,9 +439,9 @@ TWriteBuffer PrepareResponse(TConnection *Connection){ return WriteBuffer; } -void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){ - if(Connection->State != CONNECTION_LOGIN){ - LOG_ERR("Connection %s is not processing login (State: %d)", +static void SendXTEAResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){ + if(Connection->State != CONNECTION_PROCESSING){ + LOG_ERR("Connection %s is not PROCESSING (State: %d)", Connection->RemoteAddress, Connection->State); CloseConnection(Connection); return; @@ -410,25 +471,25 @@ void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){ XTEAEncrypt(Connection->XTEA, WriteBuffer->Buffer + 2, WriteBuffer->Position - 2); - Connection->State = CONNECTION_WRITE; + Connection->State = CONNECTION_WRITING; Connection->RWSize = WriteBuffer->Position; Connection->RWPosition = 0; } -void SendLoginError(TConnection *Connection, const char *Message){ - TWriteBuffer WriteBuffer = PrepareResponse(Connection); +static void SendLoginError(TConnection *Connection, const char *Message){ + TWriteBuffer WriteBuffer = PrepareXTEAResponse(Connection); WriteBuffer.Write8(10); // LOGIN_ERROR WriteBuffer.WriteString(Message); - SendResponse(Connection, &WriteBuffer); + SendXTEAResponse(Connection, &WriteBuffer); } -void SendCharacterList(TConnection *Connection, int NumCharacters, +static void SendCharacterList(TConnection *Connection, int NumCharacters, TCharacterLoginData *Characters, int PremiumDays){ - TWriteBuffer WriteBuffer = PrepareResponse(Connection); + TWriteBuffer WriteBuffer = PrepareXTEAResponse(Connection); - if(g_Motd[0] != 0){ + if(g_Config.Motd[0] != 0){ WriteBuffer.Write8(20); // MOTD - WriteBuffer.WriteString(g_Motd); + WriteBuffer.WriteString(g_Config.Motd); } WriteBuffer.Write8(100); // CHARACTER_LIST @@ -444,35 +505,28 @@ void SendCharacterList(TConnection *Connection, int NumCharacters, } WriteBuffer.Write16((uint16)PremiumDays); - SendResponse(Connection, &WriteBuffer); + SendXTEAResponse(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); + LOG_ERR("Invalid login request size from %s (expected 145, got %d)", + Connection->RemoteAddress, Connection->RWSize); 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 + TReadBuffer ReadBuffer(Connection->Buffer, Connection->RWSize); + ReadBuffer.Read8(); // always 1 for a login request + int TerminalType = ReadBuffer.Read16(); + int TerminalVersion = ReadBuffer.Read16(); + ReadBuffer.Read32(); // DATSIGNATURE + ReadBuffer.Read32(); // SPRSIGNATURE + ReadBuffer.Read32(); // PICSIGNATURE uint8 AsymmetricData[128]; - InputBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData)); - if(InputBuffer.Overflowed()){ + ReadBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData)); + if(ReadBuffer.Overflowed()){ LOG_ERR("Input buffer overflowed while reading login command from %s", Connection->RemoteAddress); CloseConnection(Connection); @@ -489,17 +543,17 @@ void ProcessLoginRequest(TConnection *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(); + ReadBuffer = TReadBuffer(AsymmetricData, sizeof(AsymmetricData)); + ReadBuffer.Read8(); // always zero + Connection->XTEA[0] = ReadBuffer.Read32(); + Connection->XTEA[1] = ReadBuffer.Read32(); + Connection->XTEA[2] = ReadBuffer.Read32(); + Connection->XTEA[3] = ReadBuffer.Read32(); char Password[30]; - int AccountID = Buffer.Read32(); - Buffer.ReadString(Password, sizeof(Password)); - if(Buffer.Overflowed()){ + int AccountID = ReadBuffer.Read32(); + ReadBuffer.ReadString(Password, sizeof(Password)); + if(ReadBuffer.Overflowed()){ LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress); CloseConnection(Connection); return; @@ -519,10 +573,17 @@ void ProcessLoginRequest(TConnection *Connection){ return; } + char IPString[16]; + StringBufFormat(IPString, "%d.%d.%d.%d", + ((Connection->IPAddress >> 24) & 0xFF), + ((Connection->IPAddress >> 16) & 0xFF), + ((Connection->IPAddress >> 8) & 0xFF), + ((Connection->IPAddress >> 0) & 0xFF)); + int NumCharacters = 0; int PremiumDays = 0; TCharacterLoginData Characters[50]; - int LoginCode = LoginAccount(AccountID, Password, Connection->IPAddress, + int LoginCode = LoginAccount(AccountID, Password, IPString, NARRAY(Characters), &NumCharacters, Characters, &PremiumDays); switch(LoginCode){ case 0:{ @@ -565,3 +626,84 @@ void ProcessLoginRequest(TConnection *Connection){ } } } + +// Status Request +//============================================================================== +static bool AllowStatusRequest(int IPAddress){ + TStatusRecord *Record = NULL; + int LeastRecentlyUsedIndex = 0; + int LeastRecentlyUsedTime = g_StatusRecords[0].Timestamp; + int TimeNow = GetMonotonicUptime(); + for(int i = 0; i < g_MaxStatusRecords; i += 1){ + if(g_StatusRecords[i].Timestamp < LeastRecentlyUsedTime){ + LeastRecentlyUsedIndex = i; + LeastRecentlyUsedTime = g_StatusRecords[i].Timestamp; + } + + if(g_StatusRecords[i].IPAddress == IPAddress){ + Record = &g_StatusRecords[i]; + break; + } + } + + bool Result = false; + if(Record == NULL){ + Record = &g_StatusRecords[LeastRecentlyUsedIndex]; + Record->IPAddress = IPAddress; + Record->Timestamp = TimeNow; + Result = true; + }else if((TimeNow - Record->Timestamp) >= g_Config.MinStatusInterval){ + Record->Timestamp = TimeNow; + Result = true; + } + + return Result; +} + +static void SendStatusString(TConnection *Connection, const char *StatusString){ + if(Connection->State != CONNECTION_PROCESSING){ + LOG_ERR("Connection %s is not PROCESSING (State: %d)", + Connection->RemoteAddress, Connection->State); + CloseConnection(Connection); + return; + } + + int Length = (int)strlen(StatusString); + if(Length > (int)sizeof(Connection->Buffer)){ + Length = (int)sizeof(Connection->Buffer); + } + + Connection->RWSize = Length; + Connection->RWPosition = 0; + memcpy(Connection->Buffer, StatusString, Length); + Connection->State = CONNECTION_WRITING; +} + +void ProcessStatusRequest(TConnection *Connection){ + if(!AllowStatusRequest(Connection->IPAddress)){ + LOG_ERR("Too many status requests from %s", Connection->RemoteAddress); + CloseConnection(Connection); + return; + } + + TReadBuffer ReadBuffer(Connection->Buffer, Connection->RWSize); + ReadBuffer.Read8(); // always 255 for a status request + int Format = (int)ReadBuffer.Read8(); + if(Format == 255){ // XML + char Request[5] = {}; + ReadBuffer.ReadBytes((uint8*)Request, 4); + if(StringEqCI(Request, "info")){ + const char *StatusString = GetStatusString(); + SendStatusString(Connection, StatusString); + }else{ + LOG_WARN("Invalid status request \"%s\" from %s", + Request, Connection->RemoteAddress); + CloseConnection(Connection); + } + }else{ + LOG_WARN("Invalid status format %d from %s", + Format, Connection->RemoteAddress); + CloseConnection(Connection); + } +} + diff --git a/src/crypto.cc b/src/crypto.cc index 8be90fd..6e50b33 100644 --- a/src/crypto.cc +++ b/src/crypto.cc @@ -1,4 +1,4 @@ -#include "login.hh" +#include "common.hh" #include <openssl/err.h> #include <openssl/rsa.h> @@ -7,7 +7,9 @@ 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 { + [](const char *str, size_t len, void *u) -> int { + (void)u; + // NOTE(fusion): These error strings already have trailing newlines, // for whatever reason. if(len > 0 && str[len - 1] == '\n'){ diff --git a/src/login.cc b/src/login.cc deleted file mode 100644 index a4fa5b6..0000000 --- a/src/login.cc +++ /dev/null @@ -1,130 +0,0 @@ -#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 deleted file mode 100644 index aa391a1..0000000 --- a/src/login.hh +++ /dev/null @@ -1,401 +0,0 @@ -#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 && !this->Overflowed()){ - 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/main.cc b/src/main.cc new file mode 100644 index 0000000..637da2f --- /dev/null +++ b/src/main.cc @@ -0,0 +1,709 @@ +#include "common.hh" + +#include <errno.h> +#include <signal.h> + +int64 g_StartTimeMS = 0; +int g_ShutdownSignal = 0; +TConfig g_Config = {}; + +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); + + // NOTE(fusion): Trim trailing whitespace. + int Length = (int)strlen(Entry); + while(Length > 0 && isspace(Entry[Length - 1])){ + Entry[Length - 1] = 0; + Length -= 1; + } + + if(Length > 0){ + char TimeString[128]; + StringBufFormatTime(TimeString, "%Y-%m-%d %H:%M:%S", (int)time(NULL)); + fprintf(stdout, "%s [%s] %s\n", TimeString, Prefix, Entry); + fflush(stdout); + } +} + +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); + + // NOTE(fusion): Trim trailing whitespace. + int Length = (int)strlen(Entry); + while(Length > 0 && isspace(Entry[Length - 1])){ + Entry[Length - 1] = 0; + Length -= 1; + } + + if(Length > 0){ + (void)File; + (void)Line; + char TimeString[128]; + StringBufFormatTime(TimeString, "%Y-%m-%d %H:%M:%S", (int)time(NULL)); + fprintf(stdout, "%s [%s] %s: %s\n", TimeString, Prefix, Function, Entry); + fflush(stdout); + } +} + +struct tm GetLocalTime(time_t t){ + struct tm result; +#if COMPILER_MSVC + localtime_s(&result, &t); +#else + localtime_r(&t, &result); +#endif + return result; +} + +struct tm GetGMTime(time_t t){ + struct tm result; +#if COMPILER_MSVC + gmtime_s(&result, &t); +#else + gmtime_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 + // NOTE(fusion): The coarse monotonic clock has a larger resolution but is + // supposed to be faster, even avoiding system calls in some cases. It should + // be fine for millisecond precision which is what we're using. + struct timespec Time; + clock_gettime(CLOCK_MONOTONIC_COARSE, &Time); + return ((int64)Time.tv_sec * 1000) + + ((int64)Time.tv_nsec / 1000000); +#endif +} + +int GetMonotonicUptime(void){ + return (int)((GetClockMonotonicMS() - g_StartTimeMS) / 1000); +} + +bool StringEmpty(const char *String){ + return String[0] == 0; +} + +bool StringEq(const char *A, const char *B){ + int Index = 0; + while(A[Index] != 0 && A[Index] == B[Index]){ + Index += 1; + } + return A[Index] == B[Index]; +} + +bool StringEqCI(const char *A, const char *B){ + int Index = 0; + while(A[Index] != 0 && tolower(A[Index]) == tolower(B[Index])){ + Index += 1; + } + return tolower(A[Index]) == tolower(B[Index]); +} + +bool StringCopy(char *Dest, int DestCapacity, const char *Src){ + int SrcLength = (Src != NULL ? (int)strlen(Src) : 0); + return StringCopyN(Dest, DestCapacity, Src, SrcLength); +} + +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 StringFormat(char *Dest, int DestCapacity, const char *Format, ...){ + va_list ap; + va_start(ap, Format); + int Written = vsnprintf(Dest, DestCapacity, Format, ap); + va_end(ap); + return Written >= 0 && Written < DestCapacity; +} + +bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp){ + struct tm tm = GetLocalTime((time_t)Timestamp); + int Result = (int)strftime(Dest, DestCapacity, Format, &tm); + + // NOTE(fusion): `strftime` will return ZERO if it's unable to fit the result + // in the supplied buffer, which is annoying because ZERO may not represent a + // failure if the result is an empty string. + ASSERT(Result >= 0 && Result < DestCapacity); + if(Result == 0){ + memset(Dest, 0, DestCapacity); + } + + return Result != 0; +} + +void StringClear(char *Dest, int DestCapacity){ + ASSERT(DestCapacity > 0); + memset(Dest, 0, DestCapacity); +} + +uint32 StringHash(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 StringEscape(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; + } +} + +int UTF8SequenceSize(uint8 LeadingByte){ + if((LeadingByte & 0x80) == 0){ + return 1; + }else if((LeadingByte & 0xE0) == 0xC0){ + return 2; + }else if((LeadingByte & 0xF0) == 0xE0){ + return 3; + }else if((LeadingByte & 0xF8) == 0xF0){ + return 4; + }else{ + return 0; + } +} + +bool UTF8IsTrailingByte(uint8 Byte){ + return (Byte & 0xC0) == 0x80; +} + +int UTF8EncodedSize(int Codepoint){ + if(Codepoint < 0){ + return 0; + }else if(Codepoint <= 0x7F){ + return 1; + }else if(Codepoint <= 0x07FF){ + return 2; + }else if(Codepoint <= 0xFFFF){ + return 3; + }else if(Codepoint <= 0x10FFFF){ + return 4; + }else{ + return 0; + } +} + +int UTF8FindNextLeadingByte(const char *Src, int SrcLength){ + int Offset = 0; + while(Offset < SrcLength){ + // NOTE(fusion): Allow the first byte to be a leading byte, in case we + // just want to advance from one leading byte to another. + if(Offset > 0 && !UTF8IsTrailingByte(Src[Offset])){ + break; + } + Offset += 1; + } + return Offset; +} + +int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint){ + if(SrcLength <= 0){ + return 0; + } + + int Size = UTF8SequenceSize(Src[0]); + if(Size <= 0 || Size > SrcLength){ + return 0; + } + + for(int i = 1; i < Size; i += 1){ + if(!UTF8IsTrailingByte(Src[i])){ + return 0; + } + } + + int Codepoint = 0; + switch(Size){ + case 1:{ + Codepoint = (int)Src[0]; + break; + } + + case 2:{ + Codepoint = ((int)(Src[0] & 0x1F) << 6) + | ((int)(Src[1] & 0x3F) << 0); + break; + } + + case 3:{ + Codepoint = ((int)(Src[0] & 0x0F) << 12) + | ((int)(Src[1] & 0x3F) << 6) + | ((int)(Src[2] & 0x3F) << 0); + break; + } + + case 4:{ + Codepoint = ((int)(Src[0] & 0x07) << 18) + | ((int)(Src[1] & 0x3F) << 12) + | ((int)(Src[2] & 0x3F) << 6) + | ((int)(Src[3] & 0x3F) << 0); + break; + } + } + + if(OutCodepoint){ + *OutCodepoint = Codepoint; + } + + return Size; +} + +int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint){ + int Size = UTF8EncodedSize(Codepoint); + if(Size > 0 && Size <= DestCapacity){ + switch(Size){ + case 1:{ + Dest[0] = (uint8)Codepoint; + break; + } + + case 2:{ + Dest[0] = (uint8)(0xC0 | (0x1F & (Codepoint >> 6))); + Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 0))); + break; + } + + case 3:{ + Dest[0] = (uint8)(0xE0 | (0x0F & (Codepoint >> 12))); + Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 6))); + Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 0))); + break; + } + + case 4:{ + Dest[0] = (uint8)(0xF0 | (0x07 & (Codepoint >> 18))); + Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 12))); + Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 6))); + Dest[3] = (uint8)(0x80 | (0x3F & (Codepoint >> 0))); + break; + } + } + } + + return Size; +} + +// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather +// convert any characters, INCLUDING the null-terminator, contained in the src +// string. Invalid or NON-LATIN1 codepoints are translated into '?'. +int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength){ + int ReadPos = 0; + int WritePos = 0; + while(ReadPos < SrcLength){ + int Codepoint = -1; + int Size = UTF8DecodeOne((uint8*)(Src + ReadPos), (SrcLength - ReadPos), &Codepoint); + if(Size > 0){ + ReadPos += Size; + }else{ + ReadPos += UTF8FindNextLeadingByte((Src + ReadPos), (SrcLength - ReadPos)); + } + + if(WritePos < DestCapacity){ + if(Codepoint >= 0 && Codepoint <= 0xFF){ + Dest[WritePos] = (char)Codepoint; + }else{ + Dest[WritePos] = '?'; + } + } + WritePos += 1; + } + + return WritePos; +} + +// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather +// convert any characters, INCLUDING the null-terminator, contained in the src +// string. Note that LATIN1 characters translates directly into UNICODE codepoints. +int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength){ + int WritePos = 0; + for(int ReadPos = 0; ReadPos < SrcLength; ReadPos += 1){ + WritePos += UTF8EncodeOne((uint8*)(Dest + WritePos), + (DestCapacity - WritePos), (uint8)Src[ReadPos]); + } + return WritePos; +} + +bool ParseBoolean(bool *Dest, const char *String){ + ASSERT(Dest && String); + *Dest = StringEqCI(String, "true") + || StringEqCI(String, "on") + || StringEqCI(String, "yes"); + return *Dest + || StringEqCI(String, "false") + || StringEqCI(String, "off") + || StringEqCI(String, "no"); +} + +bool ParseInteger(int *Dest, const char *String){ + ASSERT(Dest && String); + const char *StringEnd; + *Dest = (int)strtol(String, (char**)&StringEnd, 0); + return StringEnd > String; +} + +bool ParseDuration(int *Dest, const char *String){ + ASSERT(Dest && String); + 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 *= (1); + }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){ + *Dest *= (60); + }else if(Suffix[0] == 'H' || Suffix[0] == 'h'){ + *Dest *= (60 * 60); + } + + return true; +} + +bool ParseSize(int *Dest, const char *String){ + ASSERT(Dest && String); + 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 && DestCapacity > 0 && String); + 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)); +} + +void ParseMotd(char *Dest, int DestCapacity, const char *String){ + char *Motd = (char*)alloca(DestCapacity); + ParseString(Motd, DestCapacity, String); + if(Motd[0] != 0){ + StringFormat(Dest, DestCapacity, "%u\n%s", StringHash(Motd), Motd); + } +} + +bool ReadConfig(const char *FileName, TConfig *Config){ + 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){ + const int MaxLineSize = 1024; + char Line[MaxLineSize]; + 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(!StringBufCopyN(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(!StringBufCopyN(Val, &Line[ValStart], (ValEnd - ValStart))){ + LOG_WARN("%s:%d: Exceeded value size limit of %d characters", + FileName, LineNumber, (int)(sizeof(Val) - 1)); + continue; + } + + if(StringEqCI(Key, "LoginPort")){ + ParseInteger(&Config->LoginPort, Val); + }else if(StringEqCI(Key, "ConnectionTimeout")){ + ParseDuration(&Config->ConnectionTimeout, Val); + }else if(StringEqCI(Key, "MaxConnections")){ + ParseInteger(&Config->MaxConnections, Val); + }else if(StringEqCI(Key, "MaxStatusRecords")){ + ParseInteger(&Config->MaxStatusRecords, Val); + }else if(StringEqCI(Key, "MinStatusInterval")){ + ParseDuration(&Config->MinStatusInterval, Val); + }else if(StringEqCI(Key, "QueryManagerHost")){ + ParseStringBuf(Config->QueryManagerHost, Val); + }else if(StringEqCI(Key, "QueryManagerPort")){ + ParseInteger(&Config->QueryManagerPort, Val); + }else if(StringEqCI(Key, "QueryManagerPassword")){ + ParseStringBuf(Config->QueryManagerPassword, Val); + }else if(StringEqCI(Key, "StatusWorld")){ + ParseStringBuf(Config->StatusWorld, Val); + }else if(StringEqCI(Key, "URL")){ + ParseStringBuf(Config->Url, Val); + }else if(StringEqCI(Key, "Location")){ + ParseStringBuf(Config->Location, Val); + }else if(StringEqCI(Key, "ServerType")){ + ParseStringBuf(Config->ServerType, Val); + }else if(StringEqCI(Key, "ServerVersion")){ + ParseStringBuf(Config->ServerVersion, Val); + }else if(StringEqCI(Key, "ClientVersion")){ + ParseStringBuf(Config->ClientVersion, Val); + }else if(StringEqCI(Key, "MOTD")){ + ParseMotd(Config->Motd, sizeof(Config->Motd), Val); + }else{ + LOG_WARN("Unknown config \"%s\"", Key); + } + } + + fclose(File); + return true; +} + +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; + //WakeConnections? +} + +int main(int argc, const char **argv){ + (void)argc; + (void)argv; + + g_StartTimeMS = GetClockMonotonicMS(); + g_ShutdownSignal = 0; + if(!SigHandler(SIGPIPE, SIG_IGN) + || !SigHandler(SIGINT, ShutdownHandler) + || !SigHandler(SIGTERM, ShutdownHandler)){ + return EXIT_FAILURE; + } + + // Service Config + g_Config.LoginPort = 7171; + g_Config.ConnectionTimeout = 5; // seconds + g_Config.MaxConnections = 10; + g_Config.MaxStatusRecords = 1024; + g_Config.MinStatusInterval = 300; // seconds + StringBufCopy(g_Config.QueryManagerHost, "127.0.0.1"); + g_Config.QueryManagerPort = 7173; + StringBufCopy(g_Config.QueryManagerPassword, ""); + + // Service Info + StringBufCopy(g_Config.StatusWorld, ""); + StringBufCopy(g_Config.Url, ""); + StringBufCopy(g_Config.Location, ""); + StringBufCopy(g_Config.ServerType, ""); + StringBufCopy(g_Config.ServerVersion, ""); + StringBufCopy(g_Config.ClientVersion, ""); + StringBufCopy(g_Config.Motd, ""); + + LOG("Tibia Login v0.2"); + if(!ReadConfig("config.cfg", &g_Config)){ + return EXIT_FAILURE; + } + + LOG("Login port: %d", g_Config.LoginPort); + LOG("Connection timeout: %ds", g_Config.ConnectionTimeout); + LOG("Max connections: %d", g_Config.MaxConnections); + LOG("Max status records: %d", g_Config.MaxStatusRecords); + LOG("Min status interval: %ds", g_Config.MinStatusInterval); + LOG("Query manager host: \"%s\"", g_Config.QueryManagerHost); + LOG("Query manager port: %d", g_Config.QueryManagerPort); + LOG("Status world: \"%s\"", g_Config.StatusWorld); + LOG("URL: \"%s\"", g_Config.Url); + LOG("Location: \"%s\"", g_Config.Location); + LOG("Server type: \"%s\"", g_Config.ServerType); + LOG("Server version: \"%s\"", g_Config.ServerVersion); + LOG("Client version: \"%s\"", g_Config.ClientVersion); + LOG("MOTD: \"%s\"", g_Config.Motd); + + + { // NOTE(fusion): Print MOTD preview with escape codes. + char MotdPreview[30]; + if(StringBufEscape(MotdPreview, g_Config.Motd)){ + LOG("MOTD: \"%s\"", MotdPreview); + }else{ + LOG("MOTD: \"%s...\"", MotdPreview); + } + } + + atexit(ExitQuery); + atexit(ExitConnections); + if(!InitQuery() || !InitConnections()){ + return EXIT_FAILURE; + } + + LOG("Running..."); + while(g_ShutdownSignal == 0){ + ProcessConnections(); + } + + LOG("Received signal %d (%s), shutting down...", + g_ShutdownSignal, sigdescr_np(g_ShutdownSignal)); + return EXIT_SUCCESS; +} + diff --git a/src/query.cc b/src/query.cc index 21d44ec..1722265 100644 --- a/src/query.cc +++ b/src/query.cc @@ -1,18 +1,13 @@ -#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 +#include "common.hh" + +#include <errno.h> +#include <netdb.h> +#include <sys/socket.h> +#include <unistd.h> static TQueryManagerConnection *g_QueryManagerConnection; -bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){ +static bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){ ASSERT(HostName != NULL && OutAddr != NULL); addrinfo *Result = NULL; addrinfo Hints = {}; @@ -39,6 +34,34 @@ bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){ return Resolved; } +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; +} + bool Connect(TQueryManagerConnection *Connection){ if(Connection->Socket != -1){ LOG_ERR("Already connected"); @@ -46,8 +69,8 @@ bool Connect(TQueryManagerConnection *Connection){ } in_addr_t Addr; - if(!ResolveHostName(g_QueryManagerHost, &Addr)){ - LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_QueryManagerHost); + if(!ResolveHostName(g_Config.QueryManagerHost, &Addr)){ + LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_Config.QueryManagerHost); return false; } @@ -59,7 +82,7 @@ bool Connect(TQueryManagerConnection *Connection){ sockaddr_in QueryManagerAddress = {}; QueryManagerAddress.sin_family = AF_INET; - QueryManagerAddress.sin_port = htons((uint16)g_QueryManagerPort); + QueryManagerAddress.sin_port = htons((uint16)g_Config.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)); @@ -67,9 +90,10 @@ bool Connect(TQueryManagerConnection *Connection){ return false; } - TWriteBuffer WriteBuffer = PrepareQuery(Connection, 0); + uint8 LoginBuffer[1024]; + TWriteBuffer WriteBuffer = PrepareQuery(QUERY_LOGIN, LoginBuffer, sizeof(LoginBuffer)); WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN); - WriteBuffer.WriteString(g_QueryManagerPassword); + WriteBuffer.WriteString(g_Config.QueryManagerPassword); int Status = ExecuteQuery(Connection, false, &WriteBuffer, NULL); if(Status != QUERY_STATUS_OK){ LOG_ERR("Failed to login to query manager (%d)", Status); @@ -91,47 +115,20 @@ bool IsConnected(TQueryManagerConnection *Connection){ return Connection->Socket != -1; } -TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType){ - TWriteBuffer WriteBuffer(Connection->Buffer, sizeof(Connection->Buffer)); +TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize){ + TWriteBuffer WriteBuffer(Buffer, BufferSize); 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); + // IMPORTANT(fusion): This is similar to the Go version where there is no + // connection buffer, and the response is read into the same buffer used + // by `WriteBuffer. This helps prevent allocating and moving data around + // when reconnecting in the middle of a query. + ASSERT(WriteBuffer != NULL && WriteBuffer->Position > 2); int RequestSize = WriteBuffer->Position - 2; if(RequestSize < 0xFFFF){ @@ -147,31 +144,15 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect, } const int MaxAttempts = 2; + uint8 *Buffer = WriteBuffer->Buffer; + int BufferSize = WriteBuffer->Size; + int WriteSize = WriteBuffer->Position; for(int Attempt = 1; true; Attempt += 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(!IsConnected(Connection) && (!AutoReconnect || !Connect(Connection))){ + return QUERY_STATUS_FAILED; } - if(!WriteExact(Connection->Socket, Connection->Buffer, WriteSize)){ + if(!WriteExact(Connection->Socket, Buffer, WriteSize)){ Disconnect(Connection); if(Attempt >= MaxAttempts){ LOG_ERR("Failed to write request"); @@ -200,20 +181,20 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect, ResponseSize = BufferRead32LE(Help); } - if(ResponseSize <= 0 || ResponseSize > (int)sizeof(Connection->Buffer)){ + if(ResponseSize <= 0 || ResponseSize > BufferSize){ Disconnect(Connection); LOG_ERR("Invalid response size %d (BufferSize: %d)", - ResponseSize, (int)sizeof(Connection->Buffer)); + ResponseSize, BufferSize); return QUERY_STATUS_FAILED; } - if(!ReadExact(Connection->Socket, Connection->Buffer, ResponseSize)){ + if(!ReadExact(Connection->Socket, Buffer, ResponseSize)){ Disconnect(Connection); LOG_ERR("Failed to read response"); return QUERY_STATUS_FAILED; } - TReadBuffer ReadBuffer(Connection->Buffer, ResponseSize); + TReadBuffer ReadBuffer(Buffer, ResponseSize); int Status = ReadBuffer.Read8(); if(OutReadBuffer){ *OutReadBuffer = ReadBuffer; @@ -225,7 +206,8 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect, int LoginAccount(int AccountID, const char *Password, const char *IPAddress, int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters, int *PremiumDays){ - TWriteBuffer WriteBuffer = PrepareQuery(g_QueryManagerConnection, 11); + uint8 Buffer[KB(4)]; + TWriteBuffer WriteBuffer = PrepareQuery(QUERY_LOGIN_ACCOUNT, Buffer, sizeof(Buffer)); WriteBuffer.Write32((uint32)AccountID); WriteBuffer.WriteString(Password); WriteBuffer.WriteString(IPAddress); @@ -261,12 +243,45 @@ int LoginAccount(int AccountID, const char *Password, const char *IPAddress, return Result; } +int GetWorld(const char *WorldName, TWorld *OutWorld){ + ASSERT(WorldName && OutWorld); + uint8 Buffer[4096]; + TReadBuffer ReadBuffer; + TWriteBuffer WriteBuffer = PrepareQuery(QUERY_GET_WORLDS, Buffer, sizeof(Buffer)); + int Status = ExecuteQuery(g_QueryManagerConnection, true, &WriteBuffer, &ReadBuffer); + int Result = (Status == QUERY_STATUS_OK ? 0 : -1); + memset(OutWorld, 0, sizeof(TWorld)); + if(Status == QUERY_STATUS_OK){ + int NumWorlds = (int)ReadBuffer.Read8(); + for(int i = 0; i < NumWorlds; i += 1){ + TWorld World = {}; + ReadBuffer.ReadString(World.Name, sizeof(World.Name)); + World.Type = (int)ReadBuffer.Read8(); + World.NumPlayers = (int)ReadBuffer.Read16(); + World.MaxPlayers = (int)ReadBuffer.Read16(); + World.OnlinePeak = (int)ReadBuffer.Read16(); + World.OnlinePeakTimestamp = (int)ReadBuffer.Read32(); + World.LastStartup = (int)ReadBuffer.Read32(); + World.LastShutdown = (int)ReadBuffer.Read32(); + + if(StringEmpty(WorldName)){ + // NOTE(fusion): Pick the world with the most players. + if(i == 0 || World.NumPlayers > OutWorld->NumPlayers){ + *OutWorld = World; + } + }else if(StringEqCI(WorldName, World.Name)){ + *OutWorld = World; + break; + } + } + }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)){ @@ -283,3 +298,4 @@ void ExitQuery(void){ g_QueryManagerConnection = NULL; } } + diff --git a/src/status.cc b/src/status.cc new file mode 100644 index 0000000..cb960b8 --- /dev/null +++ b/src/status.cc @@ -0,0 +1,222 @@ +#include "common.hh" + +// IMPORTANT(fusion): We want connections to use the status string directly for +// output, but because it could be refreshed while connections are still using +// it, we need to have them buffered, to allow old versions to remain valid, at +// least for a couple refreshes. Note that ` + +static int g_LastStatusRefresh = 0; +static int g_StatusStringIndex = 0; +static char g_StatusString[3][KB(2)]; + +struct XMLBuffer{ + char *Data; + int Size; + int Position; +}; + +static void XMLNullTerminate(XMLBuffer *Buffer){ + ASSERT(Buffer->Size > 0); + if(Buffer->Position < Buffer->Size){ + Buffer->Data[Buffer->Position] = 0; + }else{ + Buffer->Data[Buffer->Size - 1] = 0; + } +} + +static void XMLAppendChar(XMLBuffer *Buffer, char Ch){ + if(Buffer->Position < Buffer->Size){ + Buffer->Data[Buffer->Position] = Ch; + } + Buffer->Position += 1; +} + +static void XMLAppendNumber(XMLBuffer *Buffer, int64 Num){ + if(Num == 0){ + XMLAppendChar(Buffer, '0'); + return; + } + + if(Num < 0){ + XMLAppendChar(Buffer, '-'); + Num = -Num; + } + + char String[64] = {}; + int StringLen = 0; + while(Num > 0){ + ASSERT(StringLen < (int)sizeof(String)); + String[StringLen] = (Num % 10) + '0'; + Num = (Num / 10); + StringLen += 1; + } + + for(int i = 0; i < StringLen; i += 1){ + XMLAppendChar(Buffer, String[(StringLen - 1) - i]); + } +} + +static void XMLAppendString(XMLBuffer *Buffer, const char *String){ + const char *P = String; + while(P[0]){ + XMLAppendChar(Buffer, P[0]); + P += 1; + } +} + +static void XMLAppendStringEscaped(XMLBuffer *Buffer, const char *String){ + const char *P = String; + while(P[0]){ + switch(P[0]){ + case '\t': XMLAppendString(Buffer, "	"); break; + case '\n': XMLAppendString(Buffer, " "); break; + case '"': XMLAppendString(Buffer, """); break; + case '&': XMLAppendString(Buffer, "&"); break; + case '\'': XMLAppendString(Buffer, "'"); break; + case '<': XMLAppendString(Buffer, "<"); break; + case '>': XMLAppendString(Buffer, ">"); break; + default: XMLAppendChar(Buffer, P[0]); break; + } + P += 1; + } +} + +static void XMLAppendStringFV(XMLBuffer *Buffer, const char *Format, va_list Args){ + const char *P = Format; + while(P[0]){ + // IMPORTANT(fusion): This function only implements a small subset of + // the printf syntax. + if(P[0] == '%'){ + P += 1; + + // IMPORTANT(fusion): Small integral parameters such as char and short + // are promoted to int, similar to how they're promoted in arithmetic + // operations. This means there are only really 4 and 8 bytes integers. + int Length = 4; + if(P[0] == 'h' && P[1] == 'h'){ + P += 2; + }else if(P[0] == 'l' && P[1] == 'l'){ + Length = 8; + P += 2; + }else if(P[0] == 'h'){ + P += 1; + }else if(P[0] == 'l'){ + Length = 8; + P += 1; + } + + switch(P[0]){ + case '%':{ + XMLAppendChar(Buffer, '%'); + break; + } + + case 'c':{ + int Ch = va_arg(Args, int); + XMLAppendChar(Buffer, (char)Ch); + break; + } + + case 'd':{ + int64 Num = (Length == 4 ? va_arg(Args, int) : va_arg(Args, int64)); + XMLAppendNumber(Buffer, Num); + break; + } + + case 's':{ + const char *String = va_arg(Args, const char*); + XMLAppendStringEscaped(Buffer, String); + break; + } + + default:{ + LOG_ERR("Invalid XML format specifier \"%c\"", P[0]); + break; + } + } + + if(P[0]){ + P += 1; + } + + }else{ + XMLAppendChar(Buffer, P[0]); + P += 1; + } + } +} + +static void XMLAppendStringF(XMLBuffer *Buffer, const char *Format, ...){ + va_list Args; + va_start(Args, Format); + XMLAppendStringFV(Buffer, Format, Args); + va_end(Args); +} + +const char *GetStatusString(void){ + int TimeNow = (int)time(NULL); + if((TimeNow - g_LastStatusRefresh) >= g_Config.MinStatusInterval){ + const char *WorldName = ""; + int Uptime = 0; + int NumPlayers = 0; + int MaxPlayers = 0; + int OnlinePeak = 0; + + TWorld World = {}; + if(GetWorld(g_Config.StatusWorld, &World) == 0){ + WorldName = World.Name; + if(World.LastStartup != 0 && World.LastStartup > World.LastShutdown){ + Uptime = (int)time(NULL) - World.LastStartup; + } + NumPlayers = World.NumPlayers; + MaxPlayers = World.MaxPlayers; + OnlinePeak = World.OnlinePeak; + + // IMPORTANT(fusion): This could be a common behaviour but, on OTSERVLIST, + // the server will show as OFFLINE if the the online peak is less than + // the number of online players. This shouldn't usually be a problem since + // the online character list and online peak are updated together in the + // same CREATE_PLAYERLIST query, but is something to keep in mind. + if(OnlinePeak < NumPlayers){ + OnlinePeak = NumPlayers; + } + }else{ + LOG_ERR("Failed to query world data..."); + } + + // NOTE(fusion): Skip line with MOTD hash. + const char *Motd = g_Config.Motd; + while(Motd[0]){ + if(Motd[0] == '\n'){ + Motd += 1; + break; + } + Motd += 1; + } + + g_StatusStringIndex = (g_StatusStringIndex + 1) % NARRAY(g_StatusString); + XMLBuffer Buffer = {}; + Buffer.Data = g_StatusString[g_StatusStringIndex]; + Buffer.Size = sizeof(g_StatusString[g_StatusStringIndex]); + XMLAppendString(&Buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + XMLAppendString(&Buffer, "<tsqp version=\"1.0\">"); + XMLAppendStringF(&Buffer, + "<serverinfo servername=\"%s\" uptime=\"%d\" url=\"%s\"" + " location=\"%s\" server=\"%s\" version=\"%s\"" + " client=\"%s\"/>", + WorldName, Uptime, g_Config.Url, g_Config.Location, + g_Config.ServerType, g_Config.ServerVersion, + g_Config.ClientVersion); + XMLAppendStringF(&Buffer, + "<players online=\"%d\" max=\"%d\" peak=\"%d\"/>", + NumPlayers, MaxPlayers, OnlinePeak); + XMLAppendStringF(&Buffer, "<motd>%s</motd>", Motd); + XMLAppendString(&Buffer, "</tsqp>"); + XMLNullTerminate(&Buffer); + + g_LastStatusRefresh = TimeNow; + } + + return g_StatusString[g_StatusStringIndex]; +} + |
