aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/common.cc375
-rw-r--r--src/common.hh520
-rw-r--r--src/connections.cc420
-rw-r--r--src/crypto.cc6
-rw-r--r--src/login.cc130
-rw-r--r--src/login.hh401
-rw-r--r--src/main.cc709
-rw-r--r--src/query.cc180
-rw-r--r--src/status.cc222
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, "&#9;"); break;
+ case '\n': XMLAppendString(Buffer, "&#10;"); break;
+ case '"': XMLAppendString(Buffer, "&quot;"); break;
+ case '&': XMLAppendString(Buffer, "&amp;"); break;
+ case '\'': XMLAppendString(Buffer, "&apos;"); break;
+ case '<': XMLAppendString(Buffer, "&lt;"); break;
+ case '>': XMLAppendString(Buffer, "&gt;"); 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];
+}
+