overall improvements + support STATUS requests

This commit is contained in:
fusion32 2025-11-18 15:35:22 -03:00
parent b101f3809e
commit 6036d4f37a
14 changed files with 1896 additions and 1198 deletions

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
.vscode .vscode
.cache
bin bin
build build
local local
*.log config.cfg
compile_commands.json

View File

@ -3,39 +3,37 @@ BUILDDIR = build
OUTPUTEXE = login OUTPUTEXE = login
CXX = g++ CXX = g++
CXXFLAGS = -m64 -fno-strict-aliasing -pedantic -Wno-deprecated-declarations -Wno-unused-parameter -Wall -Wextra -pthread --std=c++11 CXXFLAGS = -m64 -fno-strict-aliasing -Wno-deprecated-declarations -pedantic -Wall -Wextra -pthread --std=c++11
LFLAGS = -Wl,-t -lcrypto LFLAGS = -Wl,-t -lcrypto
DEBUG ?= 0 DEBUG ?= 0
ifneq ($(DEBUG), 0) ifneq ($(DEBUG), 0)
CXXFLAGS += -g -Og CXXFLAGS += -g -Og -DENABLE_ASSERTIONS=1
else else
CXXFLAGS += -O2 CXXFLAGS += -O2
endif endif
HEADERS = $(SRCDIR)/common.hh $(SRCDIR)/login.hh $(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/crypto.obj $(BUILDDIR)/connections.obj $(BUILDDIR)/main.obj $(BUILDDIR)/query.obj $(BUILDDIR)/status.obj
$(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/common.obj $(BUILDDIR)/connections.obj $(BUILDDIR)/crypto.obj $(BUILDDIR)/login.obj $(BUILDDIR)/query.obj
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -o $@ $^ $(LFLAGS) $(CXX) $(CXXFLAGS) -o $@ $^ $(LFLAGS)
$(BUILDDIR)/common.obj: $(SRCDIR)/common.cc $(HEADERS) $(BUILDDIR)/crypto.obj: $(SRCDIR)/crypto.cc $(SRCDIR)/common.hh
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $< $(CXX) -c $(CXXFLAGS) -o $@ $<
$(BUILDDIR)/connections.obj: $(SRCDIR)/connections.cc $(HEADERS) $(BUILDDIR)/connections.obj: $(SRCDIR)/connections.cc $(SRCDIR)/common.hh
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $< $(CXX) -c $(CXXFLAGS) -o $@ $<
$(BUILDDIR)/crypto.obj: $(SRCDIR)/crypto.cc $(HEADERS) $(BUILDDIR)/main.obj: $(SRCDIR)/main.cc $(SRCDIR)/common.hh
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $< $(CXX) -c $(CXXFLAGS) -o $@ $<
$(BUILDDIR)/login.obj: $(SRCDIR)/login.cc $(HEADERS) $(BUILDDIR)/status.obj: $(SRCDIR)/status.cc $(SRCDIR)/common.hh
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $< $(CXX) -c $(CXXFLAGS) -o $@ $<
$(BUILDDIR)/query.obj: $(SRCDIR)/query.cc $(HEADERS) $(BUILDDIR)/query.obj: $(SRCDIR)/query.cc $(SRCDIR)/common.hh
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $< $(CXX) -c $(CXXFLAGS) -o $@ $<

View File

@ -1,13 +1,13 @@
# Tibia 7.7 Login Server # Tibia 7.7 Login Server
This is a simple login server designed to support [Tibia Game Server](https://github.com/fusion32/tibia-game). This is a simple login server designed to support [Tibia Game Server](https://github.com/fusion32/tibia-game). It also serves OpenTibia XML STATUS requests, although the response may not conform to server list demands of filtering the player number by IP address. Doing so is possible but would require additional data such as idle time and IP address to be included in the online characters table, which then requires changes to the original protocol, which would break compatibility.
## Compiling ## Compiling
Even though there are no Linux specific features being used, it will currently only compile on Linux. It should be simple enough to support compiling on Windows but I don't think it would add any value, considering the querymanager will be running on Linux and that they need to be both on the same machine. The makefile is very simple and should work as long as OpenSSL's libcrypto, which is the only dependency, is installed. You can add the `-j N` switch to make it compile across N processes. Even though there are no Linux specific features being used, it will currently only compile on Linux. It should be simple enough to support compiling on Windows but I don't think it would add any value, considering the querymanager will be running on Linux and that they need to be both on the same machine. The makefile is very simple and should work as long as OpenSSL's libcrypto, which is the only dependency, is installed.
``` ```
make # build in release mode make -B DEBUG=0 # rebuild in release mode
make DEBUG=1 # build in debug mode make -B DEBUG=1 # rebuild in debug mode
make clean # remove `build` directory make clean # remove `build` directory
``` ```
## Running ## Running
Similar to the game server, the login server won't boot up if it's not able to connect to the [Query Manager](https://github.com/fusion32/tibia-querymanager). That said, running it is straighforward, requiring only the RSA private key `tibia.pem` and `config.cfg` files to be in the working directory. It is always recommended that the server is setup as a service. There is a *systemd* configuration file (`tibia-login.service`) in the repository that may be used for that purpose. The process is very similar to the one described in the [Game Server](https://github.com/fusion32/tibia-game) so I won't repeat myself here. Similar to the game server, the login server won't boot up if it's not able to connect to the [Query Manager](https://github.com/fusion32/tibia-querymanager). That said, running it is straighforward, requiring only the RSA private key `tibia.pem` and `config.cfg` files to be in the working directory. For testing purposes you could simply compile and launch the application from the shell, but if you plan to run the game server on a dedicated machine, it is recommended that it is setup as a service. There is a *systemd* configuration file (`tibia-login.service`) in the repository that may be used for that purpose. The process is very similar to the one described in the [Game Server](https://github.com/fusion32/tibia-game) so I won't repeat myself here.

View File

@ -1,8 +1,20 @@
MOTD = "Welcome to Tibia!" # Service Config
UpdateRate = 20 LoginPort = 7171
LoginPort = 7171 ConnectionTimeout = 5s
MaxConnections = 10 MaxConnections = 5
LoginTimeout = 10s MaxStatusRecords = 1024
QueryManagerHost = "127.0.0.1" MinStatusInterval = 5m
QueryManagerPort = 7173 QueryManagerHost = "127.0.0.1"
QueryManagerPassword = "a6glaf0c" QueryManagerPort = 7173
QueryManagerPassword = "a6glaf0c"
# Service Info
StatusWorld = ""
URL = ""
Location = ""
Owner = ""
ServerType = "Tibia"
ServerVersion = "7.7"
ClientVersion = "7.7"
MOTD = "Welcome to Tibia!"

19
config.cfg.dist Normal file
View File

@ -0,0 +1,19 @@
# Service Config
LoginPort = 7171
ConnectionTimeout = 5s
MaxConnections = 10
MaxStatusRecords = 1024
MinStatusInterval = 5m
QueryManagerHost = "127.0.0.1"
QueryManagerPort = 7173
QueryManagerPassword = "a6glaf0c"
# Service Info
StatusWorld = ""
URL = ""
Location = ""
ServerType = "Tibia"
ServerVersion = "7.7"
ClientVersion = "7.7"
MOTD = "Welcome to Tibia!"

View File

@ -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;
}

View File

@ -1,31 +1,25 @@
#ifndef TIBIA_LOGIN_COMMON_HH_ #ifndef TIBIA_COMMON_HH_
#define TIBIA_LOGIN_COMMON_HH_ 1 #define TIBIA_COMMON_HH_ 1
#include <ctype.h> #include <ctype.h>
#include <limits.h>
#include <stdarg.h> #include <stdarg.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <algorithm>
typedef uint8_t uint8; typedef uint8_t uint8;
typedef uint16_t uint16; typedef uint16_t uint16;
typedef uint32_t uint32; typedef uint32_t uint32;
typedef int64_t int64; typedef int64_t int64;
typedef uint64_t uint64; typedef uint64_t uint64;
typedef size_t usize;
#define STATIC_ASSERT(expr) static_assert((expr), "static assertion failed: " #expr) #define STATIC_ASSERT(expr) static_assert((expr), "static assertion failed: " #expr)
#define NARRAY(arr) (int)(sizeof(arr) / sizeof(arr[0])) #define NARRAY(arr) (int)(sizeof(arr) / sizeof(arr[0]))
#define ISPOW2(x) ((x) != 0 && ((x) & ((x) - 1)) == 0) #define ISPOW2(x) ((x) != 0 && ((x) & ((x) - 1)) == 0)
#define KB(x) ((usize)(x) << 10) #define KB(x) ((x) * 1024)
#define MB(x) ((usize)(x) << 20)
#define GB(x) ((usize)(x) << 30)
#if defined(_WIN32) #if defined(_WIN32)
# define OS_WINDOWS 1 # define OS_WINDOWS 1
@ -60,7 +54,7 @@ typedef size_t usize;
#endif #endif
#define ASSERT_ALWAYS(expr) if(!(expr)) { TRAP(); } #define ASSERT_ALWAYS(expr) if(!(expr)) { TRAP(); }
#if BUILD_DEBUG #if ENABLE_ASSERTIONS
# define ASSERT(expr) ASSERT_ALWAYS(expr) # define ASSERT(expr) ASSERT_ALWAYS(expr)
#else #else
# define ASSERT(expr) ((void)(expr)) # define ASSERT(expr) ((void)(expr))
@ -75,29 +69,517 @@ typedef size_t usize;
TRAP(); \ TRAP(); \
}while(0) }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 LogAdd(const char *Prefix, const char *Format, ...) ATTR_PRINTF(2, 3);
void LogAddVerbose(const char *Prefix, const char *Function, void LogAddVerbose(const char *Prefix, const char *Function,
const char *File, int Line, const char *Format, ...) ATTR_PRINTF(5, 6); const char *File, int Line, const char *Format, ...) ATTR_PRINTF(5, 6);
struct tm GetLocalTime(time_t t); struct tm GetLocalTime(time_t t);
struct tm GetGMTime(time_t t);
int64 GetClockMonotonicMS(void); int64 GetClockMonotonicMS(void);
void SleepMS(int64 DurationMS); int GetMonotonicUptime(void);
bool StringEmpty(const char *String);
bool StringEq(const char *A, const char *B); bool StringEq(const char *A, const char *B);
bool StringEqCI(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 StringCopy(char *Dest, int DestCapacity, const char *Src);
bool EscapeString(char *Dest, int DestCapacity, const char *Src); bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength);
uint32 HashString(const char *String); 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 ParseBoolean(bool *Dest, const char *String);
bool ParseInteger(int *Dest, const char *String); bool ParseInteger(int *Dest, const char *String);
bool ParseDuration(int *Dest, const char *String); bool ParseDuration(int *Dest, const char *String);
bool ParseSize(int *Dest, const char *String); bool ParseSize(int *Dest, const char *String);
bool ParseString(char *Dest, int DestCapacity, 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);
typedef void ConfigKVCallback(const char *Key, const char *Val); // IMPORTANT(fusion): These macros should only be used when `Dest` is a char array
bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback); // 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)
#endif //TIBIA_LOGIN_COMMON_HH_ // 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;
};
void ProcessConnections(void);
bool InitConnections(void);
void ExitConnections(void);
void ProcessLoginRequest(TConnection *Connection);
void ProcessStatusRequest(TConnection *Connection);
#endif //TIBIA_COMMON_H_

View File

@ -1,33 +1,30 @@
#include "login.hh" #include "common.hh"
// TODO(fusion): Support windows eventually? #include <errno.h>
#if OS_LINUX #include <fcntl.h>
# include <errno.h> #include <netinet/in.h>
# include <fcntl.h> #include <poll.h>
# include <netinet/in.h> #include <sys/socket.h>
# include <poll.h> #include <unistd.h>
# include <sys/socket.h>
# include <unistd.h>
# include <time.h>
#else
# error "Operating system not currently supported."
#endif
#if TIBIA772 #if TIBIA772
static const int TERMINALVERSION[] = {772, 772, 772}; static const int TERMINALVERSION[] = {772, 772, 772};
#else #else
static const int TERMINALVERSION[] = {770, 770, 770}; static const int TERMINALVERSION[] = {770, 770, 770};
#endif #endif
static RSAKey *g_PrivateKey = NULL; static RSAKey *g_PrivateKey;
static int g_Listener = -1;
static TConnection *g_Connections = NULL; static int g_Listener = -1;
static TConnection *g_Connections;
static int g_MaxConnections;
static TStatusRecord *g_StatusRecords;
static int g_MaxStatusRecords;
// Connection Handling // Connection Handling
//============================================================================== //==============================================================================
// NOTE(fusion): This is very similar to the connection handling code used by the static int ListenerBind(uint16 Port){
// query manager with a few subtle differences including the encryption scheme.
int ListenerBind(uint16 Port){
int Socket = socket(AF_INET, SOCK_STREAM, 0); int Socket = socket(AF_INET, SOCK_STREAM, 0);
if(Socket == -1){ if(Socket == -1){
LOG_ERR("Failed to create listener socket: (%d) %s", errno, strerrordesc_np(errno)); LOG_ERR("Failed to create listener socket: (%d) %s", errno, strerrordesc_np(errno));
@ -73,7 +70,7 @@ int ListenerBind(uint16 Port){
return Socket; return Socket;
} }
int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){ static int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){
while(true){ while(true){
sockaddr_in SocketAddr = {}; sockaddr_in SocketAddr = {};
socklen_t SocketAddrLen = sizeof(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){ if(Connection->Socket != -1){
close(Connection->Socket); close(Connection->Socket);
Connection->Socket = -1; Connection->Socket = -1;
} }
} }
TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){ static TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
int ConnectionIndex = -1; 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){ if(g_Connections[i].State == CONNECTION_FREE){
ConnectionIndex = i; ConnectionIndex = i;
break; break;
@ -129,22 +126,25 @@ TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
TConnection *Connection = NULL; TConnection *Connection = NULL;
if(ConnectionIndex != -1){ if(ConnectionIndex != -1){
Connection = &g_Connections[ConnectionIndex]; Connection = &g_Connections[ConnectionIndex];
Connection->State = CONNECTION_HANDSHAKE; Connection->State = CONNECTION_READING;
Connection->Socket = Socket; Connection->Socket = Socket;
Connection->StartTime = g_MonotonicTimeMS; Connection->IPAddress = (int)Addr;
Connection->StartTime = GetMonotonicUptime();
Connection->RandomSeed = (uint32)rand(); Connection->RandomSeed = (uint32)rand();
snprintf(Connection->IPAddress, sizeof(Connection->IPAddress), StringBufFormat(Connection->RemoteAddress,
"%d.%d.%d.%d", ((int)(Addr >> 24) & 0xFF), ((int)(Addr >> 16) & 0xFF), "%d.%d.%d.%d:%d",
((int)(Addr >> 8) & 0xFF), ((int)(Addr >> 0) & 0xFF)); ((Connection->IPAddress >> 24) & 0xFF),
snprintf(Connection->RemoteAddress, sizeof(Connection->RemoteAddress), ((Connection->IPAddress >> 16) & 0xFF),
"%s:%d", Connection->IPAddress, (int)Port); ((Connection->IPAddress >> 8) & 0xFF),
((Connection->IPAddress >> 0) & 0xFF),
(int)Port);
LOG("Connection %s assigned to slot %d", LOG("Connection %s assigned to slot %d",
Connection->RemoteAddress, ConnectionIndex); Connection->RemoteAddress, ConnectionIndex);
} }
return Connection; return Connection;
} }
void ReleaseConnection(TConnection *Connection){ static void ReleaseConnection(TConnection *Connection){
if(Connection->State != CONNECTION_FREE){ if(Connection->State != CONNECTION_FREE){
LOG("Connection %s released", Connection->RemoteAddress); LOG("Connection %s released", Connection->RemoteAddress);
CloseConnection(Connection); CloseConnection(Connection);
@ -153,12 +153,12 @@ void ReleaseConnection(TConnection *Connection){
} }
} }
void CheckConnectionInput(TConnection *Connection, int Events){ static void CheckConnectionInput(TConnection *Connection, int Events){
if((Events & POLLIN) == 0 || Connection->Socket == -1){ if(Connection->Socket == -1 || (Events & POLLIN) == 0){
return; return;
} }
if(Connection->State != CONNECTION_HANDSHAKE){ if(Connection->State != CONNECTION_READING){
LOG_ERR("Connection %s (State: %d) sending out-of-order data", LOG_ERR("Connection %s (State: %d) sending out-of-order data",
Connection->RemoteAddress, Connection->State); Connection->RemoteAddress, Connection->State);
CloseConnection(Connection); CloseConnection(Connection);
@ -166,13 +166,8 @@ void CheckConnectionInput(TConnection *Connection, int Events){
} }
while(true){ while(true){
int ReadSize = Connection->RWSize; int ReadSize = (Connection->RWSize > 0 ? Connection->RWSize : 2);
if(ReadSize == 0){ int BytesRead = (int)read(Connection->Socket,
ReadSize = 2 - Connection->RWPosition;
ASSERT(ReadSize > 0);
}
int BytesRead = read(Connection->Socket,
(Connection->Buffer + Connection->RWPosition), (Connection->Buffer + Connection->RWPosition),
(ReadSize - Connection->RWPosition)); (ReadSize - Connection->RWPosition));
if(BytesRead == -1){ if(BytesRead == -1){
@ -189,9 +184,12 @@ void CheckConnectionInput(TConnection *Connection, int Events){
Connection->RWPosition += BytesRead; Connection->RWPosition += BytesRead;
if(Connection->RWPosition >= ReadSize){ if(Connection->RWPosition >= ReadSize){
if(Connection->RWSize == 0){ if(Connection->RWSize != 0){
int PayloadSize = BufferRead16LE(Connection->Buffer); Connection->State = CONNECTION_PROCESSING;
if(PayloadSize <= 0 || PayloadSize > NARRAY(Connection->Buffer)){ break;
}else if(Connection->RWPosition == 2){
int PayloadSize = (int)BufferRead16LE(Connection->Buffer);
if(PayloadSize <= 0 || PayloadSize > (int)sizeof(Connection->Buffer)){
CloseConnection(Connection); CloseConnection(Connection);
break; break;
} }
@ -199,28 +197,54 @@ void CheckConnectionInput(TConnection *Connection, int Events){
Connection->RWSize = PayloadSize; Connection->RWSize = PayloadSize;
Connection->RWPosition = 0; Connection->RWPosition = 0;
}else{ }else{
Connection->State = CONNECTION_LOGIN; PANIC("Invalid input state (State: %d, RWSize: %d, RWPosition: %d)",
break; Connection->State, Connection->RWSize, Connection->RWPosition);
} }
} }
} }
if(Connection->State == CONNECTION_LOGIN){
ProcessLoginRequest(Connection);
}
} }
void CheckConnectionOutput(TConnection *Connection, int Events){ static void CheckConnectionRequest(TConnection *Connection){
if((Events & POLLOUT) == 0 || Connection->Socket == -1){ if(Connection->Socket == -1){
return; return;
} }
if(Connection->State != CONNECTION_WRITE){ 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);
}
}
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_WRITING){
return; return;
} }
while(true){ while(true){
int BytesWritten = write(Connection->Socket, int BytesWritten = (int)write(Connection->Socket,
(Connection->Buffer + Connection->RWPosition), (Connection->Buffer + Connection->RWPosition),
(Connection->RWSize - Connection->RWPosition)); (Connection->RWSize - Connection->RWPosition));
if(BytesWritten == -1){ 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); ASSERT((Events & POLLNVAL) == 0);
if((Events & (POLLERR | POLLHUP)) != 0){ if((Events & (POLLERR | POLLHUP)) != 0){
CloseConnection(Connection); CloseConnection(Connection);
} }
if(g_LoginTimeout > 0){ if(g_Config.ConnectionTimeout > 0){
int LoginTime = (g_MonotonicTimeMS - Connection->StartTime); int ElapsedTime = GetMonotonicUptime() - Connection->StartTime;
if(LoginTime >= g_LoginTimeout){ if(ElapsedTime >= g_Config.ConnectionTimeout){
LOG_WARN("Connection %s TIMEDOUT (LoginTime: %dms, Timeout: %dms)", LOG_WARN("Connection %s TIMEDOUT (ElapsedTime: %ds, Timeout: %ds)",
Connection->RemoteAddress, LoginTime, g_LoginTimeout); Connection->RemoteAddress, ElapsedTime, g_Config.ConnectionTimeout);
CloseConnection(Connection); CloseConnection(Connection);
} }
} }
@ -259,8 +283,12 @@ void CheckConnection(TConnection *Connection, int Events){
} }
} }
void ProcessConnections(void){ static void AcceptConnections(int Events){
// NOTE(fusion): Accept new connections. ASSERT(g_Listener != -1);
if((Events & POLLIN) == 0){
return;
}
while(true){ while(true){
uint32 Addr; uint32 Addr;
uint16 Port; uint16 Port;
@ -270,56 +298,78 @@ void ProcessConnections(void){
} }
if(AssignConnection(Socket, Addr, Port) == NULL){ if(AssignConnection(Socket, Addr, Port) == NULL){
LOG_ERR("Rejecting connection from %08X:%d due to max number of" LOG_ERR("Rejecting connection %08X:%d:"
" connections being reached (%d)", Addr, Port, g_MaxConnections); " max number of connections reached (%d)",
Addr, Port, g_Config.MaxConnections);
close(Socket); close(Socket);
} }
} }
}
// NOTE(fusion): Gather active connections. void ProcessConnections(void){
int NumConnections = 0; int NumFds = 0;
int *ConnectionIndices = (int*)alloca(g_MaxConnections * sizeof(int)); int MaxFds = g_MaxConnections + 1;
pollfd *ConnectionFds = (pollfd*)alloca(g_MaxConnections * sizeof(pollfd)); pollfd *Fds = (pollfd*)alloca(MaxFds * sizeof(pollfd));
for(int i = 0; i < g_MaxConnections; i += 1){ int *ConnectionIndices = (int*)alloca(MaxFds * sizeof(int));
if(g_Connections[i].State == CONNECTION_FREE || g_Connections[i].Socket == -1){
if(g_Listener != -1){
Fds[NumFds].fd = g_Listener;
Fds[NumFds].events = POLLIN;
Fds[NumFds].revents = 0;
ConnectionIndices[NumFds] = -1;
NumFds += 1;
}
for(int i = 0; i < g_Config.MaxConnections; i += 1){
if(g_Connections[i].State == CONNECTION_FREE){
continue; continue;
} }
ConnectionIndices[NumConnections] = i; Fds[NumFds].fd = g_Connections[i].Socket;
ConnectionFds[NumConnections].fd = g_Connections[i].Socket; Fds[NumFds].events = POLLIN;
ConnectionFds[NumConnections].events = POLLIN | POLLOUT; if(g_Connections[i].State == CONNECTION_WRITING){
ConnectionFds[NumConnections].revents = 0; Fds[NumFds].events |= POLLOUT;
NumConnections += 1; }
Fds[NumFds].revents = 0;
ConnectionIndices[NumFds] = i;
NumFds += 1;
} }
if(NumConnections <= 0){ // NOTE(fusion): Block for 1 second at most, so we can properly timeout
return; // idle connections.
} ASSERT(NumFds > 0);
int NumEvents = poll(Fds, NumFds, 1);
// NOTE(fusion): Poll connections.
int NumEvents = poll(ConnectionFds, NumConnections, 0);
if(NumEvents == -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; return;
} }
// NOTE(fusion): Process connections. // NOTE(fusion): Process connections.
for(int i = 0; i < NumConnections; i += 1){ for(int i = 0; i < NumFds; i += 1){
TConnection *Connection = &g_Connections[ConnectionIndices[i]]; int Index = ConnectionIndices[i];
int Events = (int)ConnectionFds[i].revents; int Events = (int)Fds[i].revents;
CheckConnectionInput(Connection, Events); if(Index >= 0 && Index < g_Config.MaxConnections){
CheckConnectionOutput(Connection, Events); TConnection *Connection = &g_Connections[Index];
CheckConnection(Connection, Events); 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){ bool InitConnections(void){
ASSERT(g_PrivateKey == NULL);
ASSERT(g_Listener == -1); ASSERT(g_Listener == -1);
ASSERT(g_Connections == NULL); ASSERT(g_Connections == NULL);
ASSERT(g_StatusRecords == NULL);
LOG("Login port: %d", g_LoginPort);
LOG("Max connections: %d", g_MaxConnections);
LOG("Login timeout: %dms", g_LoginTimeout);
g_PrivateKey = RSALoadPEM("tibia.pem"); g_PrivateKey = RSALoadPEM("tibia.pem");
if(g_PrivateKey == NULL){ if(g_PrivateKey == NULL){
@ -327,17 +377,23 @@ bool InitConnections(void){
return false; return false;
} }
g_Listener = ListenerBind(g_LoginPort); g_Listener = ListenerBind((uint16)g_Config.LoginPort);
if(g_Listener == -1){ if(g_Listener == -1){
LOG_ERR("Failed to bind listener"); LOG_ERR("Failed to bind listener to port %d", g_Config.LoginPort);
return false; 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){ for(int i = 0; i < g_MaxConnections; i += 1){
g_Connections[i].State = CONNECTION_FREE; g_Connections[i].State = CONNECTION_FREE;
} }
g_MaxStatusRecords = g_Config.MaxStatusRecords;
g_StatusRecords = (TStatusRecord*)calloc(
g_MaxStatusRecords, sizeof(TStatusRecord));
return true; return true;
} }
@ -360,13 +416,18 @@ void ExitConnections(void){
free(g_Connections); free(g_Connections);
g_Connections = NULL; g_Connections = NULL;
} }
if(g_StatusRecords != NULL){
free(g_StatusRecords);
g_StatusRecords = NULL;
}
} }
// Connection Requests // Login Request
//============================================================================== //==============================================================================
TWriteBuffer PrepareResponse(TConnection *Connection){ static TWriteBuffer PrepareXTEAResponse(TConnection *Connection){
if(Connection->State != CONNECTION_LOGIN){ if(Connection->State != CONNECTION_PROCESSING){
LOG_ERR("Connection %s is not processing login (State: %d)", LOG_ERR("Connection %s is not PROCESSING (State: %d)",
Connection->RemoteAddress, Connection->State); Connection->RemoteAddress, Connection->State);
CloseConnection(Connection); CloseConnection(Connection);
return TWriteBuffer(NULL, 0); return TWriteBuffer(NULL, 0);
@ -378,9 +439,9 @@ TWriteBuffer PrepareResponse(TConnection *Connection){
return WriteBuffer; return WriteBuffer;
} }
void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){ static void SendXTEAResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
if(Connection->State != CONNECTION_LOGIN){ if(Connection->State != CONNECTION_PROCESSING){
LOG_ERR("Connection %s is not processing login (State: %d)", LOG_ERR("Connection %s is not PROCESSING (State: %d)",
Connection->RemoteAddress, Connection->State); Connection->RemoteAddress, Connection->State);
CloseConnection(Connection); CloseConnection(Connection);
return; return;
@ -410,25 +471,25 @@ void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
XTEAEncrypt(Connection->XTEA, XTEAEncrypt(Connection->XTEA,
WriteBuffer->Buffer + 2, WriteBuffer->Buffer + 2,
WriteBuffer->Position - 2); WriteBuffer->Position - 2);
Connection->State = CONNECTION_WRITE; Connection->State = CONNECTION_WRITING;
Connection->RWSize = WriteBuffer->Position; Connection->RWSize = WriteBuffer->Position;
Connection->RWPosition = 0; Connection->RWPosition = 0;
} }
void SendLoginError(TConnection *Connection, const char *Message){ static void SendLoginError(TConnection *Connection, const char *Message){
TWriteBuffer WriteBuffer = PrepareResponse(Connection); TWriteBuffer WriteBuffer = PrepareXTEAResponse(Connection);
WriteBuffer.Write8(10); // LOGIN_ERROR WriteBuffer.Write8(10); // LOGIN_ERROR
WriteBuffer.WriteString(Message); 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){ 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.Write8(20); // MOTD
WriteBuffer.WriteString(g_Motd); WriteBuffer.WriteString(g_Config.Motd);
} }
WriteBuffer.Write8(100); // CHARACTER_LIST WriteBuffer.Write8(100); // CHARACTER_LIST
@ -444,35 +505,28 @@ void SendCharacterList(TConnection *Connection, int NumCharacters,
} }
WriteBuffer.Write16((uint16)PremiumDays); WriteBuffer.Write16((uint16)PremiumDays);
SendResponse(Connection, &WriteBuffer); SendXTEAResponse(Connection, &WriteBuffer);
} }
void ProcessLoginRequest(TConnection *Connection){ void ProcessLoginRequest(TConnection *Connection){
if(Connection->RWSize != 145){ if(Connection->RWSize != 145){
LOG_ERR("Invalid login request size %d from %s (expected 145)", LOG_ERR("Invalid login request size from %s (expected 145, got %d)",
Connection->RWSize, Connection->RemoteAddress); Connection->RemoteAddress, Connection->RWSize);
CloseConnection(Connection); CloseConnection(Connection);
return; return;
} }
TReadBuffer InputBuffer(Connection->Buffer, Connection->RWSize); TReadBuffer ReadBuffer(Connection->Buffer, Connection->RWSize);
int Command = InputBuffer.Read8(); ReadBuffer.Read8(); // always 1 for a login request
if(Command != 1){ int TerminalType = ReadBuffer.Read16();
LOG_ERR("Invalid login command %d from %s (expected 1)", int TerminalVersion = ReadBuffer.Read16();
Command, Connection->RemoteAddress); ReadBuffer.Read32(); // DATSIGNATURE
CloseConnection(Connection); ReadBuffer.Read32(); // SPRSIGNATURE
return; ReadBuffer.Read32(); // PICSIGNATURE
}
int TerminalType = InputBuffer.Read16();
int TerminalVersion = InputBuffer.Read16();
InputBuffer.Read32(); // DATSIGNATURE
InputBuffer.Read32(); // SPRSIGNATURE
InputBuffer.Read32(); // PICSIGNATURE
uint8 AsymmetricData[128]; uint8 AsymmetricData[128];
InputBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData)); ReadBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData));
if(InputBuffer.Overflowed()){ if(ReadBuffer.Overflowed()){
LOG_ERR("Input buffer overflowed while reading login command from %s", LOG_ERR("Input buffer overflowed while reading login command from %s",
Connection->RemoteAddress); Connection->RemoteAddress);
CloseConnection(Connection); CloseConnection(Connection);
@ -489,17 +543,17 @@ void ProcessLoginRequest(TConnection *Connection){
return; return;
} }
TReadBuffer Buffer(AsymmetricData, sizeof(AsymmetricData)); ReadBuffer = TReadBuffer(AsymmetricData, sizeof(AsymmetricData));
Buffer.Read8(); // always zero ReadBuffer.Read8(); // always zero
Connection->XTEA[0] = Buffer.Read32(); Connection->XTEA[0] = ReadBuffer.Read32();
Connection->XTEA[1] = Buffer.Read32(); Connection->XTEA[1] = ReadBuffer.Read32();
Connection->XTEA[2] = Buffer.Read32(); Connection->XTEA[2] = ReadBuffer.Read32();
Connection->XTEA[3] = Buffer.Read32(); Connection->XTEA[3] = ReadBuffer.Read32();
char Password[30]; char Password[30];
int AccountID = Buffer.Read32(); int AccountID = ReadBuffer.Read32();
Buffer.ReadString(Password, sizeof(Password)); ReadBuffer.ReadString(Password, sizeof(Password));
if(Buffer.Overflowed()){ if(ReadBuffer.Overflowed()){
LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress); LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress);
CloseConnection(Connection); CloseConnection(Connection);
return; return;
@ -519,10 +573,17 @@ void ProcessLoginRequest(TConnection *Connection){
return; 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 NumCharacters = 0;
int PremiumDays = 0; int PremiumDays = 0;
TCharacterLoginData Characters[50]; TCharacterLoginData Characters[50];
int LoginCode = LoginAccount(AccountID, Password, Connection->IPAddress, int LoginCode = LoginAccount(AccountID, Password, IPString,
NARRAY(Characters), &NumCharacters, Characters, &PremiumDays); NARRAY(Characters), &NumCharacters, Characters, &PremiumDays);
switch(LoginCode){ switch(LoginCode){
case 0:{ 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);
}
}

View File

@ -1,4 +1,4 @@
#include "login.hh" #include "common.hh"
#include <openssl/err.h> #include <openssl/err.h>
#include <openssl/rsa.h> #include <openssl/rsa.h>
@ -7,7 +7,9 @@
static void DumpOpenSSLErrors(const char *Where, const char *What){ static void DumpOpenSSLErrors(const char *Where, const char *What){
LOG_ERR("OpenSSL error(s) while executing %s at %s:", What, Where); LOG_ERR("OpenSSL error(s) while executing %s at %s:", What, Where);
ERR_print_errors_cb( 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, // NOTE(fusion): These error strings already have trailing newlines,
// for whatever reason. // for whatever reason.
if(len > 0 && str[len - 1] == '\n'){ if(len > 0 && str[len - 1] == '\n'){

View File

@ -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;
}

View File

@ -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_

709
src/main.cc Normal file
View File

@ -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;
}

View File

@ -1,18 +1,13 @@
#include "login.hh" #include "common.hh"
// TODO(fusion): Support windows eventually? #include <errno.h>
#if OS_LINUX #include <netdb.h>
# include <errno.h> #include <sys/socket.h>
# include <netdb.h> #include <unistd.h>
# include <sys/socket.h>
# include <unistd.h>
#else
# error "Operating system not currently supported."
#endif
static TQueryManagerConnection *g_QueryManagerConnection; 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); ASSERT(HostName != NULL && OutAddr != NULL);
addrinfo *Result = NULL; addrinfo *Result = NULL;
addrinfo Hints = {}; addrinfo Hints = {};
@ -39,65 +34,6 @@ bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
return Resolved; return Resolved;
} }
bool Connect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
LOG_ERR("Already connected");
return false;
}
in_addr_t Addr;
if(!ResolveHostName(g_QueryManagerHost, &Addr)){
LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_QueryManagerHost);
return false;
}
Connection->Socket = socket(AF_INET, SOCK_STREAM, 0);
if(Connection->Socket == -1){
LOG_ERR("Failed to create socket: (%d) %s", errno, strerrordesc_np(errno));
return false;
}
sockaddr_in QueryManagerAddress = {};
QueryManagerAddress.sin_family = AF_INET;
QueryManagerAddress.sin_port = htons((uint16)g_QueryManagerPort);
QueryManagerAddress.sin_addr.s_addr = Addr;
if(connect(Connection->Socket, (sockaddr*)&QueryManagerAddress, sizeof(QueryManagerAddress)) == -1){
LOG_ERR("Failed to connect: (%d) %s", errno, strerrordesc_np(errno));
Disconnect(Connection);
return false;
}
TWriteBuffer WriteBuffer = PrepareQuery(Connection, 0);
WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN);
WriteBuffer.WriteString(g_QueryManagerPassword);
int Status = ExecuteQuery(Connection, false, &WriteBuffer, NULL);
if(Status != QUERY_STATUS_OK){
LOG_ERR("Failed to login to query manager (%d)", Status);
Disconnect(Connection);
return false;
}
return true;
}
void Disconnect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
close(Connection->Socket);
Connection->Socket = -1;
}
}
bool IsConnected(TQueryManagerConnection *Connection){
return Connection->Socket != -1;
}
TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType){
TWriteBuffer WriteBuffer(Connection->Buffer, sizeof(Connection->Buffer));
WriteBuffer.Write16(0); // Request Size
WriteBuffer.Write8((uint8)QueryType);
return WriteBuffer;
}
static bool WriteExact(int Fd, const uint8 *Buffer, int Size){ static bool WriteExact(int Fd, const uint8 *Buffer, int Size){
int BytesToWrite = Size; int BytesToWrite = Size;
const uint8 *WritePtr = Buffer; const uint8 *WritePtr = Buffer;
@ -126,12 +62,73 @@ static bool ReadExact(int Fd, uint8 *Buffer, int Size){
return true; return true;
} }
bool Connect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
LOG_ERR("Already connected");
return false;
}
in_addr_t Addr;
if(!ResolveHostName(g_Config.QueryManagerHost, &Addr)){
LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_Config.QueryManagerHost);
return false;
}
Connection->Socket = socket(AF_INET, SOCK_STREAM, 0);
if(Connection->Socket == -1){
LOG_ERR("Failed to create socket: (%d) %s", errno, strerrordesc_np(errno));
return false;
}
sockaddr_in QueryManagerAddress = {};
QueryManagerAddress.sin_family = AF_INET;
QueryManagerAddress.sin_port = htons((uint16)g_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));
Disconnect(Connection);
return false;
}
uint8 LoginBuffer[1024];
TWriteBuffer WriteBuffer = PrepareQuery(QUERY_LOGIN, LoginBuffer, sizeof(LoginBuffer));
WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN);
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);
Disconnect(Connection);
return false;
}
return true;
}
void Disconnect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
close(Connection->Socket);
Connection->Socket = -1;
}
}
bool IsConnected(TQueryManagerConnection *Connection){
return Connection->Socket != -1;
}
TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize){
TWriteBuffer WriteBuffer(Buffer, BufferSize);
WriteBuffer.Write16(0); // Request Size
WriteBuffer.Write8((uint8)QueryType);
return WriteBuffer;
}
int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect, int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer){ TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer){
ASSERT(WriteBuffer != NULL // IMPORTANT(fusion): This is similar to the Go version where there is no
&& WriteBuffer->Buffer == Connection->Buffer // connection buffer, and the response is read into the same buffer used
&& WriteBuffer->Size == sizeof(Connection->Buffer) // by `WriteBuffer. This helps prevent allocating and moving data around
&& WriteBuffer->Position > 2); // when reconnecting in the middle of a query.
ASSERT(WriteBuffer != NULL && WriteBuffer->Position > 2);
int RequestSize = WriteBuffer->Position - 2; int RequestSize = WriteBuffer->Position - 2;
if(RequestSize < 0xFFFF){ if(RequestSize < 0xFFFF){
@ -147,31 +144,15 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
} }
const int MaxAttempts = 2; const int MaxAttempts = 2;
uint8 *Buffer = WriteBuffer->Buffer;
int BufferSize = WriteBuffer->Size;
int WriteSize = WriteBuffer->Position;
for(int Attempt = 1; true; Attempt += 1){ for(int Attempt = 1; true; Attempt += 1){
int WriteSize = WriteBuffer->Position; if(!IsConnected(Connection) && (!AutoReconnect || !Connect(Connection))){
if(!IsConnected(Connection)){ return QUERY_STATUS_FAILED;
if(!AutoReconnect){
return QUERY_STATUS_FAILED;
}
// IMPORTANT(fusion): There is no way around this. `Connect` will
// use the connection buffer to send the login query so we need to
// save and restore it to not lose any data. One improvement here
// would be to use a stack or statically allocated buffer, although
// using malloc/free should have no real impact on performance as
// we're already doing BLOCKING I/O here.
uint8 *TempBuffer = (uint8*)malloc(WriteSize);
memcpy(TempBuffer, Connection->Buffer, WriteSize);
bool Reconnected = Connect(Connection);
memcpy(Connection->Buffer, TempBuffer, WriteSize);
free(TempBuffer);
if(!Reconnected){
return QUERY_STATUS_FAILED;
}
} }
if(!WriteExact(Connection->Socket, Connection->Buffer, WriteSize)){ if(!WriteExact(Connection->Socket, Buffer, WriteSize)){
Disconnect(Connection); Disconnect(Connection);
if(Attempt >= MaxAttempts){ if(Attempt >= MaxAttempts){
LOG_ERR("Failed to write request"); LOG_ERR("Failed to write request");
@ -200,20 +181,20 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
ResponseSize = BufferRead32LE(Help); ResponseSize = BufferRead32LE(Help);
} }
if(ResponseSize <= 0 || ResponseSize > (int)sizeof(Connection->Buffer)){ if(ResponseSize <= 0 || ResponseSize > BufferSize){
Disconnect(Connection); Disconnect(Connection);
LOG_ERR("Invalid response size %d (BufferSize: %d)", LOG_ERR("Invalid response size %d (BufferSize: %d)",
ResponseSize, (int)sizeof(Connection->Buffer)); ResponseSize, BufferSize);
return QUERY_STATUS_FAILED; return QUERY_STATUS_FAILED;
} }
if(!ReadExact(Connection->Socket, Connection->Buffer, ResponseSize)){ if(!ReadExact(Connection->Socket, Buffer, ResponseSize)){
Disconnect(Connection); Disconnect(Connection);
LOG_ERR("Failed to read response"); LOG_ERR("Failed to read response");
return QUERY_STATUS_FAILED; return QUERY_STATUS_FAILED;
} }
TReadBuffer ReadBuffer(Connection->Buffer, ResponseSize); TReadBuffer ReadBuffer(Buffer, ResponseSize);
int Status = ReadBuffer.Read8(); int Status = ReadBuffer.Read8();
if(OutReadBuffer){ if(OutReadBuffer){
*OutReadBuffer = ReadBuffer; *OutReadBuffer = ReadBuffer;
@ -225,7 +206,8 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
int LoginAccount(int AccountID, const char *Password, const char *IPAddress, int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters, int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
int *PremiumDays){ 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.Write32((uint32)AccountID);
WriteBuffer.WriteString(Password); WriteBuffer.WriteString(Password);
WriteBuffer.WriteString(IPAddress); WriteBuffer.WriteString(IPAddress);
@ -261,12 +243,45 @@ int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
return Result; 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){ bool InitQuery(void){
ASSERT(g_QueryManagerConnection == NULL); ASSERT(g_QueryManagerConnection == NULL);
LOG("QueryManagerHost: %s", g_QueryManagerHost);
LOG("QueryManagerPort: %d", g_QueryManagerPort);
g_QueryManagerConnection = (TQueryManagerConnection*)calloc(1, sizeof(TQueryManagerConnection)); g_QueryManagerConnection = (TQueryManagerConnection*)calloc(1, sizeof(TQueryManagerConnection));
g_QueryManagerConnection->Socket = -1; g_QueryManagerConnection->Socket = -1;
if(!Connect(g_QueryManagerConnection)){ if(!Connect(g_QueryManagerConnection)){
@ -283,3 +298,4 @@ void ExitQuery(void){
g_QueryManagerConnection = NULL; g_QueryManagerConnection = NULL;
} }
} }

222
src/status.cc Normal file
View File

@ -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];
}