aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--Makefile20
-rw-r--r--README.md10
-rw-r--r--config.cfg28
-rw-r--r--config.cfg.dist19
-rw-r--r--src/common.cc375
-rw-r--r--src/common.hh520
-rw-r--r--src/connections.cc420
-rw-r--r--src/crypto.cc6
-rw-r--r--src/login.cc130
-rw-r--r--src/login.hh401
-rw-r--r--src/main.cc709
-rw-r--r--src/query.cc180
-rw-r--r--src/status.cc222
14 files changed, 1871 insertions, 1173 deletions
diff --git a/.gitignore b/.gitignore
index d680a30..08ebd61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
.vscode
+.cache
bin
build
local
-*.log
+config.cfg
+compile_commands.json
diff --git a/Makefile b/Makefile
index d0c16d3..6890598 100644
--- a/Makefile
+++ b/Makefile
@@ -3,39 +3,37 @@ BUILDDIR = build
OUTPUTEXE = login
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
DEBUG ?= 0
ifneq ($(DEBUG), 0)
- CXXFLAGS += -g -Og
+ CXXFLAGS += -g -Og -DENABLE_ASSERTIONS=1
else
- CXXFLAGS += -O2
+ CXXFLAGS += -O2
endif
-HEADERS = $(SRCDIR)/common.hh $(SRCDIR)/login.hh
-
-$(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/common.obj $(BUILDDIR)/connections.obj $(BUILDDIR)/crypto.obj $(BUILDDIR)/login.obj $(BUILDDIR)/query.obj
+$(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/crypto.obj $(BUILDDIR)/connections.obj $(BUILDDIR)/main.obj $(BUILDDIR)/query.obj $(BUILDDIR)/status.obj
@mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -o $@ $^ $(LFLAGS)
-$(BUILDDIR)/common.obj: $(SRCDIR)/common.cc $(HEADERS)
+$(BUILDDIR)/crypto.obj: $(SRCDIR)/crypto.cc $(SRCDIR)/common.hh
@mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $<
-$(BUILDDIR)/connections.obj: $(SRCDIR)/connections.cc $(HEADERS)
+$(BUILDDIR)/connections.obj: $(SRCDIR)/connections.cc $(SRCDIR)/common.hh
@mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $<
-$(BUILDDIR)/crypto.obj: $(SRCDIR)/crypto.cc $(HEADERS)
+$(BUILDDIR)/main.obj: $(SRCDIR)/main.cc $(SRCDIR)/common.hh
@mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $<
-$(BUILDDIR)/login.obj: $(SRCDIR)/login.cc $(HEADERS)
+$(BUILDDIR)/status.obj: $(SRCDIR)/status.cc $(SRCDIR)/common.hh
@mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $<
-$(BUILDDIR)/query.obj: $(SRCDIR)/query.cc $(HEADERS)
+$(BUILDDIR)/query.obj: $(SRCDIR)/query.cc $(SRCDIR)/common.hh
@mkdir -p $(@D)
$(CXX) -c $(CXXFLAGS) -o $@ $<
diff --git a/README.md b/README.md
index fdd9524..328e767 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# 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
-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 DEBUG=1 # build in debug mode
+make -B DEBUG=0 # rebuild in release mode
+make -B DEBUG=1 # rebuild in debug mode
make clean # remove `build` directory
```
## 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.
diff --git a/config.cfg b/config.cfg
index 913e60c..69ed6f3 100644
--- a/config.cfg
+++ b/config.cfg
@@ -1,8 +1,20 @@
-MOTD = "Welcome to Tibia!"
-UpdateRate = 20
-LoginPort = 7171
-MaxConnections = 10
-LoginTimeout = 10s
-QueryManagerHost = "127.0.0.1"
-QueryManagerPort = 7173
-QueryManagerPassword = "a6glaf0c"
+# Service Config
+LoginPort = 7171
+ConnectionTimeout = 5s
+MaxConnections = 5
+MaxStatusRecords = 1024
+MinStatusInterval = 5m
+QueryManagerHost = "127.0.0.1"
+QueryManagerPort = 7173
+QueryManagerPassword = "a6glaf0c"
+
+# Service Info
+StatusWorld = ""
+URL = ""
+Location = ""
+Owner = ""
+ServerType = "Tibia"
+ServerVersion = "7.7"
+ClientVersion = "7.7"
+MOTD = "Welcome to Tibia!"
+
diff --git a/config.cfg.dist b/config.cfg.dist
new file mode 100644
index 0000000..bce98ce
--- /dev/null
+++ b/config.cfg.dist
@@ -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!"
+
diff --git a/src/common.cc b/src/common.cc
deleted file mode 100644
index c4786e2..0000000
--- a/src/common.cc
+++ /dev/null
@@ -1,375 +0,0 @@
-#include "common.hh"
-
-void LogAdd(const char *Prefix, const char *Format, ...){
- char Entry[4096];
- va_list ap;
- va_start(ap, Format);
- vsnprintf(Entry, sizeof(Entry), Format, ap);
- va_end(ap);
-
- if(Entry[0] != 0){
- struct tm LocalTime = GetLocalTime(time(NULL));
- fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s\n",
- LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
- LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
- Prefix, Entry);
- fflush(stdout);
- }
-}
-
-void LogAddVerbose(const char *Prefix, const char *Function,
- const char *File, int Line, const char *Format, ...){
- char Entry[4096];
- va_list ap;
- va_start(ap, Format);
- vsnprintf(Entry, sizeof(Entry), Format, ap);
- va_end(ap);
-
- if(Entry[0] != 0){
- (void)File;
- (void)Line;
- struct tm LocalTime = GetLocalTime(time(NULL));
- fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s: %s\n",
- LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
- LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
- Prefix, Function, Entry);
- fflush(stdout);
- }
-}
-
-struct tm GetLocalTime(time_t t){
- struct tm result;
-#if COMPILER_MSVC
- localtime_s(&result, &t);
-#else
- localtime_r(&t, &result);
-#endif
- return result;
-}
-
-int64 GetClockMonotonicMS(void){
-#if OS_WINDOWS
- LARGE_INTEGER Counter, Frequency;
- QueryPerformanceCounter(&Counter);
- QueryPerformanceFrequency(&Frequency);
- return (int64)((Counter.QuadPart * 1000) / Frequency.QuadPart);
-#else
- struct timespec Time;
- clock_gettime(CLOCK_MONOTONIC, &Time);
- return ((int64)Time.tv_sec * 1000)
- + ((int64)Time.tv_nsec / 1000000);
-#endif
-}
-
-void SleepMS(int64 DurationMS){
-#if OS_WINDOWS
- Sleep((DWORD)DurationMS);
-#else
- struct timespec Duration;
- Duration.tv_sec = (time_t)(DurationMS / 1000);
- Duration.tv_nsec = (long)((DurationMS % 1000) * 1000000);
- nanosleep(&Duration, NULL);
-#endif
-}
-
-bool StringEq(const char *A, const char *B){
- int Index = 0;
- while(true){
- if(A[Index] != B[Index]){
- return false;
- }else if(A[Index] == 0){
- return true;
- }
- Index += 1;
- }
-}
-
-bool StringEqCI(const char *A, const char *B){
- int Index = 0;
- while(true){
- if(tolower(A[Index]) != tolower(B[Index])){
- return false;
- }else if(A[Index] == 0){
- return true;
- }
- Index += 1;
- }
-}
-
-bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){
- ASSERT(DestCapacity > 0);
- bool Result = (SrcLength < DestCapacity);
- if(Result && SrcLength > 0){
- memcpy(Dest, Src, SrcLength);
- Dest[SrcLength] = 0;
- }else{
- Dest[0] = 0;
- }
- return Result;
-}
-
-bool StringCopy(char *Dest, int DestCapacity, const char *Src){
- // IMPORTANT(fusion): `sqlite3_column_text` may return NULL if the column is
- // also NULL so we have an incentive to properly handle the case where `Src`
- // is NULL.
- int SrcLength = (Src != NULL ? (int)strlen(Src) : 0);
- return StringCopyN(Dest, DestCapacity, Src, SrcLength);
-}
-
-bool EscapeString(char *Dest, int DestCapacity, const char *Src){
- int WritePos = 0;
- for(int ReadPos = 0; Src[ReadPos] != 0 && WritePos < DestCapacity; ReadPos += 1){
- int EscapeCh = -1;
- switch(Src[ReadPos]){
- case '\a': EscapeCh = 'a'; break;
- case '\b': EscapeCh = 'b'; break;
- case '\t': EscapeCh = 't'; break;
- case '\n': EscapeCh = 'n'; break;
- case '\v': EscapeCh = 'v'; break;
- case '\f': EscapeCh = 'f'; break;
- case '\r': EscapeCh = 'r'; break;
- case '\"': EscapeCh = '\"'; break;
- case '\'': EscapeCh = '\''; break;
- case '\\': EscapeCh = '\\'; break;
- }
-
- if(EscapeCh != -1){
- if((WritePos + 1) <= DestCapacity){
- Dest[WritePos] = '\\';
- WritePos += 1;
- }
-
- if((WritePos + 1) <= DestCapacity){
- Dest[WritePos] = EscapeCh;
- WritePos += 1;
- }
- }else{
- if((WritePos + 1) <= DestCapacity){
- Dest[WritePos] = Src[ReadPos];
- WritePos += 1;
- }
- }
- }
-
- if(WritePos < DestCapacity){
- Dest[WritePos] = 0;
- return true;
- }else{
- Dest[DestCapacity - 1] = 0;
- return false;
- }
-}
-
-uint32 HashString(const char *String){
- // FNV1a 32-bits
- uint32 Hash = 0x811C9DC5U;
- for(int i = 0; String[i] != 0; i += 1){
- Hash ^= (uint32)String[i];
- Hash *= 0x01000193U;
- }
- return Hash;
-}
-
-bool ParseIPAddress(int *Dest, const char *String){
- ASSERT(Dest != NULL && String != NULL);
- int Addr[4];
- if(sscanf(String, "%d.%d.%d.%d", &Addr[0], &Addr[1], &Addr[2], &Addr[3]) != 4){
- LOG_ERR("Invalid IP Address format \"%s\"", String);
- return false;
- }
-
- if(Addr[0] < 0 || Addr[0] > 0xFF
- || Addr[1] < 0 || Addr[1] > 0xFF
- || Addr[2] < 0 || Addr[2] > 0xFF
- || Addr[3] < 0 || Addr[3] > 0xFF){
- LOG_ERR("Invalid IP Address \"%s\"", String);
- return false;
- }
-
- *Dest = (Addr[0] << 24)
- | (Addr[1] << 16)
- | (Addr[2] << 8)
- | (Addr[3] << 0);
- return true;
-}
-
-bool ParseBoolean(bool *Dest, const char *String){
- ASSERT(Dest != NULL && String != NULL);
- *Dest = StringEqCI(String, "true");
- return *Dest || StringEqCI(String, "false");
-}
-
-bool ParseInteger(int *Dest, const char *String){
- ASSERT(Dest != NULL && String != NULL);
- const char *StringEnd;
- *Dest = (int)strtol(String, (char**)&StringEnd, 0);
- return StringEnd > String;
-}
-
-bool ParseDuration(int *Dest, const char *String){
- ASSERT(Dest != NULL && String != NULL);
- const char *Suffix;
- *Dest = (int)strtol(String, (char**)&Suffix, 0);
- if(Suffix == String){
- return false;
- }
-
- while(Suffix[0] != 0 && isspace(Suffix[0])){
- Suffix += 1;
- }
-
- if(Suffix[0] == 'S' || Suffix[0] == 's'){
- *Dest *= (1000);
- }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
- *Dest *= (60 * 1000);
- }else if(Suffix[0] == 'H' || Suffix[0] == 'h'){
- *Dest *= (60 * 60 * 1000);
- }
-
- return true;
-}
-
-bool ParseSize(int *Dest, const char *String){
- ASSERT(Dest != NULL && String != NULL);
- const char *Suffix;
- *Dest = (int)strtol(String, (char**)&Suffix, 0);
- if(Suffix == String){
- return false;
- }
-
- while(Suffix[0] != 0 && isspace(Suffix[0])){
- Suffix += 1;
- }
-
- if(Suffix[0] == 'K' || Suffix[0] == 'k'){
- *Dest *= (1024);
- }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
- *Dest *= (1024 * 1024);
- }
-
- return true;
-}
-
-bool ParseString(char *Dest, int DestCapacity, const char *String){
- ASSERT(Dest != NULL && DestCapacity > 0 && String != NULL);
- int StringStart = 0;
- int StringEnd = (int)strlen(String);
- if(StringEnd >= 2){
- if((String[0] == '"' && String[StringEnd - 1] == '"')
- || (String[0] == '\'' && String[StringEnd - 1] == '\'')
- || (String[0] == '`' && String[StringEnd - 1] == '`')){
- StringStart += 1;
- StringEnd -= 1;
- }
- }
-
- return StringCopyN(Dest, DestCapacity,
- &String[StringStart], (StringEnd - StringStart));
-}
-
-bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback){
- ASSERT(FileName != NULL && KVCallback != NULL);
- FILE *File = fopen(FileName, "rb");
- if(File == NULL){
- LOG_ERR("Failed to open config file \"%s\"", FileName);
- return false;
- }
-
- bool EndOfFile = false;
- for(int LineNumber = 1; !EndOfFile; LineNumber += 1){
- char Line[1024];
- int MaxLineSize = (int)sizeof(Line);
- int LineSize = 0;
- int KeyStart = -1;
- int EqualPos = -1;
- while(true){
- int ch = fgetc(File);
- if(ch == EOF || ch == '\n'){
- if(ch == EOF){
- EndOfFile = true;
- }
- break;
- }
-
- if(LineSize < MaxLineSize){
- Line[LineSize] = (char)ch;
- }
-
- if(KeyStart == -1 && !isspace(ch)){
- KeyStart = LineSize;
- }
-
- if(EqualPos == -1 && ch == '='){
- EqualPos = LineSize;
- }
-
- LineSize += 1;
- }
-
- // NOTE(fusion): Check line size limit.
- if(LineSize > MaxLineSize){
- LOG_WARN("%s:%d: Exceeded line size limit of %d characters",
- FileName, LineNumber, MaxLineSize);
- continue;
- }
-
- // NOTE(fusion): Check empty line or comment.
- if(KeyStart == -1 || Line[KeyStart] == '#'){
- continue;
- }
-
- // NOTE(fusion): Check assignment.
- if(EqualPos == -1){
- LOG_WARN("%s:%d: No assignment found on non empty line",
- FileName, LineNumber);
- continue;
- }
-
- // NOTE(fusion): Check empty key.
- int KeyEnd = EqualPos;
- while(KeyEnd > KeyStart && isspace(Line[KeyEnd - 1])){
- KeyEnd -= 1;
- }
-
- if(KeyStart == KeyEnd){
- LOG_WARN("%s:%d: Empty key", FileName, LineNumber);
- continue;
- }
-
- // NOTE(fusion): Check empty value.
- int ValStart = EqualPos + 1;
- int ValEnd = LineSize;
- while(ValStart < ValEnd && isspace(Line[ValStart])){
- ValStart += 1;
- }
-
- while(ValEnd > ValStart && isspace(Line[ValEnd - 1])){
- ValEnd -= 1;
- }
-
- if(ValStart == ValEnd){
- LOG_WARN("%s:%d: Empty value", FileName, LineNumber);
- continue;
- }
-
- // NOTE(fusion): Parse KV pair.
- char Key[256];
- if(!StringCopyN(Key, (int)sizeof(Key), &Line[KeyStart], (KeyEnd - KeyStart))){
- LOG_WARN("%s:%d: Exceeded key size limit of %d characters",
- FileName, LineNumber, (int)(sizeof(Key) - 1));
- continue;
- }
-
- char Val[256];
- if(!StringCopyN(Val, (int)sizeof(Val), &Line[ValStart], (ValEnd - ValStart))){
- LOG_WARN("%s:%d: Exceeded value size limit of %d characters",
- FileName, LineNumber, (int)(sizeof(Val) - 1));
- continue;
- }
-
- KVCallback(Key, Val);
- }
-
- fclose(File);
- return true;
-}
diff --git a/src/common.hh b/src/common.hh
index b4e88bc..fd87888 100644
--- a/src/common.hh
+++ b/src/common.hh
@@ -1,31 +1,25 @@
-#ifndef TIBIA_LOGIN_COMMON_HH_
-#define TIBIA_LOGIN_COMMON_HH_ 1
+#ifndef TIBIA_COMMON_HH_
+#define TIBIA_COMMON_HH_ 1
#include <ctype.h>
-#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
-#include <stdint.h>
#include <stdio.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
-#include <algorithm>
-
typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
-typedef size_t usize;
#define STATIC_ASSERT(expr) static_assert((expr), "static assertion failed: " #expr)
#define NARRAY(arr) (int)(sizeof(arr) / sizeof(arr[0]))
#define ISPOW2(x) ((x) != 0 && ((x) & ((x) - 1)) == 0)
-#define KB(x) ((usize)(x) << 10)
-#define MB(x) ((usize)(x) << 20)
-#define GB(x) ((usize)(x) << 30)
+#define KB(x) ((x) * 1024)
#if defined(_WIN32)
# define OS_WINDOWS 1
@@ -60,7 +54,7 @@ typedef size_t usize;
#endif
#define ASSERT_ALWAYS(expr) if(!(expr)) { TRAP(); }
-#if BUILD_DEBUG
+#if ENABLE_ASSERTIONS
# define ASSERT(expr) ASSERT_ALWAYS(expr)
#else
# define ASSERT(expr) ((void)(expr))
@@ -75,29 +69,517 @@ typedef size_t usize;
TRAP(); \
}while(0)
+struct TConfig {
+ // Service Config
+ int LoginPort;
+ int ConnectionTimeout;
+ int MaxConnections;
+ int MaxStatusRecords;
+ int MinStatusInterval;
+ char QueryManagerHost[100];
+ int QueryManagerPort;
+ char QueryManagerPassword[30];
+
+ // Service Info
+ char StatusWorld[30];
+ char Url[100];
+ char Location[30];
+ char ServerType[30];
+ char ServerVersion[30];
+ char ClientVersion[30];
+ char Motd[256];
+};
+
+extern TConfig g_Config;
+
void LogAdd(const char *Prefix, const char *Format, ...) ATTR_PRINTF(2, 3);
void LogAddVerbose(const char *Prefix, const char *Function,
const char *File, int Line, const char *Format, ...) ATTR_PRINTF(5, 6);
struct tm GetLocalTime(time_t t);
+struct tm GetGMTime(time_t t);
int64 GetClockMonotonicMS(void);
-void SleepMS(int64 DurationMS);
+int GetMonotonicUptime(void);
+bool StringEmpty(const char *String);
bool StringEq(const char *A, const char *B);
bool StringEqCI(const char *A, const char *B);
-bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength);
bool StringCopy(char *Dest, int DestCapacity, const char *Src);
-bool EscapeString(char *Dest, int DestCapacity, const char *Src);
-uint32 HashString(const char *String);
+bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength);
+bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...) ATTR_PRINTF(3, 4);
+bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp);
+void StringClear(char *Dest, int DestCapacity);
+uint32 StringHash(const char *String);
+bool StringEscape(char *Dest, int DestCapacity, const char *Src);
+
+int UTF8SequenceSize(uint8 LeadingByte);
+bool UTF8IsTrailingByte(uint8 Byte);
+int UTF8EncodedSize(int Codepoint);
+int UTF8FindNextLeadingByte(const char *Src, int SrcLength);
+int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint);
+int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint);
+int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength);
+int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength);
-bool ParseIPAddress(int *Dest, const char *String);
bool ParseBoolean(bool *Dest, const char *String);
bool ParseInteger(int *Dest, const char *String);
bool ParseDuration(int *Dest, const char *String);
bool ParseSize(int *Dest, const char *String);
bool ParseString(char *Dest, int DestCapacity, const char *String);
+void ParseMotd(char *Dest, int DestCapacity, const char *String);
+bool ReadConfig(const char *FileName, TConfig *Config);
+
+// IMPORTANT(fusion): These macros should only be used when `Dest` is a char array
+// to simplify the call to `StringCopy` where we'd use `sizeof(Dest)` to determine
+// the size of the destination anyways.
+#define StringBufCopy(Dest, Src) StringCopy(Dest, sizeof(Dest), Src)
+#define StringBufCopyN(Dest, Src, SrcLength) StringCopyN(Dest, sizeof(Dest), Src, SrcLength)
+#define StringBufFormat(Dest, ...) StringFormat(Dest, sizeof(Dest), __VA_ARGS__)
+#define StringBufFormatTime(Dest, Format, Timestamp) \
+ StringFormatTime(Dest, sizeof(Dest), Format, Timestamp)
+#define StringBufClear(Dest) StringClear(Dest, sizeof(Dest));
+#define StringBufEscape(Dest, Src) StringEscape(Dest, sizeof(Dest), Src)
+#define ParseStringBuf(Dest, String) ParseString(Dest, sizeof(Dest), String)
+
+// Buffer Utility
+//==============================================================================
+inline uint8 BufferRead8(const uint8 *Buffer){
+ return Buffer[0];
+}
+
+inline uint16 BufferRead16LE(const uint8 *Buffer){
+ return (uint16)Buffer[0]
+ | ((uint16)Buffer[1] << 8);
+}
+
+inline uint16 BufferRead16BE(const uint8 *Buffer){
+ return ((uint16)Buffer[0] << 8)
+ | (uint16)Buffer[1];
+}
+
+inline uint32 BufferRead32LE(const uint8 *Buffer){
+ return (uint32)Buffer[0]
+ | ((uint32)Buffer[1] << 8)
+ | ((uint32)Buffer[2] << 16)
+ | ((uint32)Buffer[3] << 24);
+}
+
+inline uint32 BufferRead32BE(const uint8 *Buffer){
+ return ((uint32)Buffer[0] << 24)
+ | ((uint32)Buffer[1] << 16)
+ | ((uint32)Buffer[2] << 8)
+ | (uint32)Buffer[3];
+}
+
+inline uint64 BufferRead64LE(const uint8 *Buffer){
+ return (uint64)Buffer[0]
+ | ((uint64)Buffer[1] << 8)
+ | ((uint64)Buffer[2] << 16)
+ | ((uint64)Buffer[3] << 24)
+ | ((uint64)Buffer[4] << 32)
+ | ((uint64)Buffer[5] << 40)
+ | ((uint64)Buffer[6] << 48)
+ | ((uint64)Buffer[7] << 56);
+}
+
+inline uint64 BufferRead64BE(const uint8 *Buffer){
+ return ((uint64)Buffer[0] << 56)
+ | ((uint64)Buffer[1] << 48)
+ | ((uint64)Buffer[2] << 40)
+ | ((uint64)Buffer[3] << 32)
+ | ((uint64)Buffer[4] << 24)
+ | ((uint64)Buffer[5] << 16)
+ | ((uint64)Buffer[6] << 8)
+ | (uint64)Buffer[7];
+}
+
+inline void BufferWrite8(uint8 *Buffer, uint8 Value){
+ Buffer[0] = Value;
+}
+
+inline void BufferWrite16LE(uint8 *Buffer, uint16 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+}
+
+inline void BufferWrite16BE(uint8 *Buffer, uint16 Value){
+ Buffer[0] = (uint8)(Value >> 8);
+ Buffer[1] = (uint8)(Value >> 0);
+}
+
+inline void BufferWrite32LE(uint8 *Buffer, uint32 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+ Buffer[2] = (uint8)(Value >> 16);
+ Buffer[3] = (uint8)(Value >> 24);
+}
+
+inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
+ Buffer[0] = (uint8)(Value >> 24);
+ Buffer[1] = (uint8)(Value >> 16);
+ Buffer[2] = (uint8)(Value >> 8);
+ Buffer[3] = (uint8)(Value >> 0);
+}
+
+inline void BufferWrite64LE(uint8 *Buffer, uint64 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+ Buffer[2] = (uint8)(Value >> 16);
+ Buffer[3] = (uint8)(Value >> 24);
+ Buffer[4] = (uint8)(Value >> 32);
+ Buffer[5] = (uint8)(Value >> 40);
+ Buffer[6] = (uint8)(Value >> 48);
+ Buffer[7] = (uint8)(Value >> 56);
+}
+
+inline void BufferWrite64BE(uint8 *Buffer, uint64 Value){
+ Buffer[0] = (uint8)(Value >> 56);
+ Buffer[1] = (uint8)(Value >> 48);
+ Buffer[2] = (uint8)(Value >> 40);
+ Buffer[3] = (uint8)(Value >> 32);
+ Buffer[4] = (uint8)(Value >> 24);
+ Buffer[5] = (uint8)(Value >> 16);
+ Buffer[6] = (uint8)(Value >> 8);
+ Buffer[7] = (uint8)(Value >> 0);
+}
+
+struct TReadBuffer{
+ uint8 *Buffer;
+ int Size;
+ int Position;
+
+ TReadBuffer(void) : TReadBuffer(NULL, 0) {}
+ TReadBuffer(uint8 *Buffer, int Size)
+ : Buffer(Buffer), Size(Size), Position(0) {}
+
+ bool CanRead(int Bytes){
+ return (this->Position + Bytes) <= this->Size;
+ }
+
+ bool Overflowed(void){
+ return this->Position > this->Size;
+ }
+
+ bool ReadFlag(void){
+ return this->Read8() != 0x00;
+ }
+
+ uint8 Read8(void){
+ uint8 Result = 0;
+ if(this->CanRead(1)){
+ Result = BufferRead8(this->Buffer + this->Position);
+ }
+ this->Position += 1;
+ return Result;
+ }
+
+ uint16 Read16(void){
+ uint16 Result = 0;
+ if(this->CanRead(2)){
+ Result = BufferRead16LE(this->Buffer + this->Position);
+ }
+ this->Position += 2;
+ return Result;
+ }
+
+ uint16 Read16BE(void){
+ uint16 Result = 0;
+ if(this->CanRead(2)){
+ Result = BufferRead16BE(this->Buffer + this->Position);
+ }
+ this->Position += 2;
+ return Result;
+ }
+
+ uint32 Read32(void){
+ uint32 Result = 0;
+ if(this->CanRead(4)){
+ Result = BufferRead32LE(this->Buffer + this->Position);
+ }
+ this->Position += 4;
+ return Result;
+ }
+
+ uint32 Read32BE(void){
+ uint32 Result = 0;
+ if(this->CanRead(4)){
+ Result = BufferRead32BE(this->Buffer + this->Position);
+ }
+ this->Position += 4;
+ return Result;
+ }
+
+#if CLIENT_ENCODING_UTF8
+ void ReadString(char *Dest, int DestCapacity){
+ int Length = (int)this->Read16();
+ if(Length == 0xFFFF){
+ Length = (int)this->Read32();
+ }
+
+ if(Dest != NULL && DestCapacity > 0){
+ int Written = 0;
+ if(this->CanRead(Length) && Length < DestCapacity){
+ memcpy(Dest, this->Buffer + this->Position, Length);
+ Written = Length;
+ }
+ memset((Dest + Written), 0, (DestCapacity - Written));
+ }
+
+ this->Position += Length;
+ }
+#else
+ void ReadString(char *Dest, int DestCapacity){
+ int Length = (int)this->Read16();
+ if(Length == 0xFFFF){
+ Length = (int)this->Read32();
+ }
+
+ if(Dest != NULL && DestCapacity > 0){
+ int Written = 0;
+ if(this->CanRead(Length)){
+ const char *Src = (const char*)(this->Buffer + this->Position);
+ Written = Latin1ToUTF8(Dest, DestCapacity, Src, Length);
+ if(Written >= DestCapacity){
+ Written = 0;
+ }
+ }
+
+ memset((Dest + Written), 0, (DestCapacity - Written));
+ }
+
+ this->Position += Length;
+ }
+#endif
+
+ void ReadBytes(uint8 *Buffer, int Count){
+ if(this->CanRead(Count)){
+ memcpy(Buffer, this->Buffer + this->Position, Count);
+ }
+ this->Position += Count;
+ }
+};
+
+struct TWriteBuffer{
+ uint8 *Buffer;
+ int Size;
+ int Position;
+
+ TWriteBuffer(void) : TWriteBuffer(NULL, 0) {}
+ TWriteBuffer(uint8 *Buffer, int Size)
+ : Buffer(Buffer), Size(Size), Position(0) {}
+
+ bool CanWrite(int Bytes){
+ return (this->Position + Bytes) <= this->Size;
+ }
+
+ bool Overflowed(void){
+ return this->Position > this->Size;
+ }
+
+ void WriteFlag(bool Value){
+ this->Write8(Value ? 0x01 : 0x00);
+ }
+
+ void Write8(uint8 Value){
+ if(this->CanWrite(1)){
+ BufferWrite8(this->Buffer + this->Position, Value);
+ }
+ this->Position += 1;
+ }
+
+ void Write16(uint16 Value){
+ if(this->CanWrite(2)){
+ BufferWrite16LE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 2;
+ }
+
+ void Write16BE(uint16 Value){
+ if(this->CanWrite(2)){
+ BufferWrite16BE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 2;
+ }
+
+ void Write32(uint32 Value){
+ if(this->CanWrite(4)){
+ BufferWrite32LE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 4;
+ }
+
+ void Write32BE(uint32 Value){
+ if(this->CanWrite(4)){
+ BufferWrite32BE(this->Buffer + this->Position, Value);
+ }
+ this->Position += 4;
+ }
+
+#if CLIENT_ENCODING_UTF8
+ void WriteString(const char *String){
+ int StringLength = 0;
+ if(String != NULL){
+ StringLength = (int)strlen(String);
+ }
+
+ if(StringLength < 0xFFFF){
+ this->Write16((uint16)StringLength);
+ }else{
+ this->Write16(0xFFFF);
+ this->Write32((uint32)StringLength);
+ }
+
+ if(StringLength > 0 && this->CanWrite(StringLength)){
+ memcpy(this->Buffer + this->Position, String, StringLength);
+ }
+
+ this->Position += StringLength;
+ }
+#else
+ void WriteString(const char *String){
+ int StringLength = 0;
+ int OutputLength = 0;
+ if(String != NULL){
+ StringLength = (int)strlen(String);
+ OutputLength = UTF8ToLatin1(NULL, 0, String, (int)strlen(String));
+ }
+
+ if(OutputLength < 0xFFFF){
+ this->Write16((uint16)OutputLength);
+ }else{
+ this->Write16(0xFFFF);
+ this->Write32((uint32)OutputLength);
+ }
+
+ if(OutputLength > 0 && this->CanWrite(OutputLength)){
+ int Written = UTF8ToLatin1((char*)(this->Buffer + this->Position),
+ (this->Size - this->Position), String, StringLength);
+ ASSERT(Written == OutputLength);
+ }
+
+ this->Position += OutputLength;
+ }
+#endif
+
+ void Rewrite16(int Position, uint16 Value){
+ if((Position + 2) <= this->Position && !this->Overflowed()){
+ BufferWrite16LE(this->Buffer + Position, Value);
+ }
+ }
+
+ void Insert32(int Position, uint32 Value){
+ if(Position <= this->Position){
+ if(this->CanWrite(4)){
+ memmove(this->Buffer + Position + 4,
+ this->Buffer + Position,
+ this->Position - Position);
+ BufferWrite32LE(this->Buffer + Position, Value);
+ }
+
+ this->Position += 4;
+ }
+ }
+};
+
+// crypto.cc
+//==============================================================================
+typedef void RSAKey;
+RSAKey *RSALoadPEM(const char *FileName);
+void RSAFree(RSAKey *Key);
+bool RSADecrypt(RSAKey *Key, uint8 *Data, int Size);
+void XTEAEncrypt(const uint32 *Key, uint8 *Data, int Size);
+void XTEADecrypt(const uint32 *Key, uint8 *Data, int Size);
+
+// query.cc
+//==============================================================================
+enum {
+ APPLICATION_TYPE_GAME = 1,
+ APPLICATION_TYPE_LOGIN = 2,
+ APPLICATION_TYPE_WEB = 3,
+};
+
+enum {
+ QUERY_STATUS_OK = 0,
+ QUERY_STATUS_ERROR = 1,
+ QUERY_STATUS_FAILED = 3,
+};
+
+enum {
+ QUERY_LOGIN = 0,
+ QUERY_LOGIN_ACCOUNT = 11,
+ QUERY_GET_WORLDS = 150,
+};
+
+struct TQueryManagerConnection{
+ int Socket;
+};
+
+struct TCharacterLoginData{
+ char Name[30];
+ char WorldName[30];
+ int WorldAddress;
+ int WorldPort;
+};
+
+struct TWorld {
+ char Name[30];
+ int Type;
+ int NumPlayers;
+ int MaxPlayers;
+ int OnlinePeak;
+ int OnlinePeakTimestamp;
+ int LastStartup;
+ int LastShutdown;
+};
+
+bool Connect(TQueryManagerConnection *Connection);
+void Disconnect(TQueryManagerConnection *Connection);
+bool IsConnected(TQueryManagerConnection *Connection);
+TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize);
+int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
+ TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer);
+int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
+ int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
+ int *PremiumDays);
+int GetWorld(const char *WorldName, TWorld *OutWorld);
+bool InitQuery(void);
+void ExitQuery(void);
+
+// status.cc
+//==============================================================================
+const char *GetStatusString(void);
+
+// connections.cc
+//==============================================================================
+enum ConnectionState {
+ CONNECTION_FREE = 0,
+ CONNECTION_READING = 1,
+ CONNECTION_PROCESSING = 2,
+ CONNECTION_WRITING = 3,
+};
+
+struct TConnection {
+ ConnectionState State;
+ int Socket;
+ int IPAddress;
+ int StartTime;
+ int RWSize;
+ int RWPosition;
+ uint32 RandomSeed;
+ uint32 XTEA[4];
+ char RemoteAddress[32];
+ uint8 Buffer[KB(2)];
+};
+
+struct TStatusRecord {
+ int IPAddress;
+ int Timestamp;
+};
-typedef void ConfigKVCallback(const char *Key, const char *Val);
-bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback);
+void ProcessConnections(void);
+bool InitConnections(void);
+void ExitConnections(void);
+void ProcessLoginRequest(TConnection *Connection);
+void ProcessStatusRequest(TConnection *Connection);
-#endif //TIBIA_LOGIN_COMMON_HH_
+#endif //TIBIA_COMMON_H_
diff --git a/src/connections.cc b/src/connections.cc
index ef89f66..774a21d 100644
--- a/src/connections.cc
+++ b/src/connections.cc
@@ -1,33 +1,30 @@
-#include "login.hh"
-
-// TODO(fusion): Support windows eventually?
-#if OS_LINUX
-# include <errno.h>
-# include <fcntl.h>
-# include <netinet/in.h>
-# include <poll.h>
-# include <sys/socket.h>
-# include <unistd.h>
-# include <time.h>
-#else
-# error "Operating system not currently supported."
-#endif
+#include "common.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <sys/socket.h>
+#include <unistd.h>
#if TIBIA772
-static const int TERMINALVERSION[] = {772, 772, 772};
+static const int TERMINALVERSION[] = {772, 772, 772};
#else
-static const int TERMINALVERSION[] = {770, 770, 770};
+static const int TERMINALVERSION[] = {770, 770, 770};
#endif
-static RSAKey *g_PrivateKey = NULL;
-static int g_Listener = -1;
-static TConnection *g_Connections = NULL;
+static RSAKey *g_PrivateKey;
+
+static int g_Listener = -1;
+static TConnection *g_Connections;
+static int g_MaxConnections;
+
+static TStatusRecord *g_StatusRecords;
+static int g_MaxStatusRecords;
// Connection Handling
//==============================================================================
-// NOTE(fusion): This is very similar to the connection handling code used by the
-// query manager with a few subtle differences including the encryption scheme.
-int ListenerBind(uint16 Port){
+static int ListenerBind(uint16 Port){
int Socket = socket(AF_INET, SOCK_STREAM, 0);
if(Socket == -1){
LOG_ERR("Failed to create listener socket: (%d) %s", errno, strerrordesc_np(errno));
@@ -73,7 +70,7 @@ int ListenerBind(uint16 Port){
return Socket;
}
-int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){
+static int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){
while(true){
sockaddr_in SocketAddr = {};
socklen_t SocketAddrLen = sizeof(SocketAddr);
@@ -110,16 +107,16 @@ int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){
}
}
-void CloseConnection(TConnection *Connection){
+static void CloseConnection(TConnection *Connection){
if(Connection->Socket != -1){
close(Connection->Socket);
Connection->Socket = -1;
}
}
-TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
+static TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
int ConnectionIndex = -1;
- for(int i = 0; i < g_MaxConnections; i += 1){
+ for(int i = 0; i < g_Config.MaxConnections; i += 1){
if(g_Connections[i].State == CONNECTION_FREE){
ConnectionIndex = i;
break;
@@ -129,22 +126,25 @@ TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
TConnection *Connection = NULL;
if(ConnectionIndex != -1){
Connection = &g_Connections[ConnectionIndex];
- Connection->State = CONNECTION_HANDSHAKE;
+ Connection->State = CONNECTION_READING;
Connection->Socket = Socket;
- Connection->StartTime = g_MonotonicTimeMS;
+ Connection->IPAddress = (int)Addr;
+ Connection->StartTime = GetMonotonicUptime();
Connection->RandomSeed = (uint32)rand();
- snprintf(Connection->IPAddress, sizeof(Connection->IPAddress),
- "%d.%d.%d.%d", ((int)(Addr >> 24) & 0xFF), ((int)(Addr >> 16) & 0xFF),
- ((int)(Addr >> 8) & 0xFF), ((int)(Addr >> 0) & 0xFF));
- snprintf(Connection->RemoteAddress, sizeof(Connection->RemoteAddress),
- "%s:%d", Connection->IPAddress, (int)Port);
+ StringBufFormat(Connection->RemoteAddress,
+ "%d.%d.%d.%d:%d",
+ ((Connection->IPAddress >> 24) & 0xFF),
+ ((Connection->IPAddress >> 16) & 0xFF),
+ ((Connection->IPAddress >> 8) & 0xFF),
+ ((Connection->IPAddress >> 0) & 0xFF),
+ (int)Port);
LOG("Connection %s assigned to slot %d",
Connection->RemoteAddress, ConnectionIndex);
}
return Connection;
}
-void ReleaseConnection(TConnection *Connection){
+static void ReleaseConnection(TConnection *Connection){
if(Connection->State != CONNECTION_FREE){
LOG("Connection %s released", Connection->RemoteAddress);
CloseConnection(Connection);
@@ -153,12 +153,12 @@ void ReleaseConnection(TConnection *Connection){
}
}
-void CheckConnectionInput(TConnection *Connection, int Events){
- if((Events & POLLIN) == 0 || Connection->Socket == -1){
+static void CheckConnectionInput(TConnection *Connection, int Events){
+ if(Connection->Socket == -1 || (Events & POLLIN) == 0){
return;
}
- if(Connection->State != CONNECTION_HANDSHAKE){
+ if(Connection->State != CONNECTION_READING){
LOG_ERR("Connection %s (State: %d) sending out-of-order data",
Connection->RemoteAddress, Connection->State);
CloseConnection(Connection);
@@ -166,13 +166,8 @@ void CheckConnectionInput(TConnection *Connection, int Events){
}
while(true){
- int ReadSize = Connection->RWSize;
- if(ReadSize == 0){
- ReadSize = 2 - Connection->RWPosition;
- ASSERT(ReadSize > 0);
- }
-
- int BytesRead = read(Connection->Socket,
+ int ReadSize = (Connection->RWSize > 0 ? Connection->RWSize : 2);
+ int BytesRead = (int)read(Connection->Socket,
(Connection->Buffer + Connection->RWPosition),
(ReadSize - Connection->RWPosition));
if(BytesRead == -1){
@@ -189,9 +184,12 @@ void CheckConnectionInput(TConnection *Connection, int Events){
Connection->RWPosition += BytesRead;
if(Connection->RWPosition >= ReadSize){
- if(Connection->RWSize == 0){
- int PayloadSize = BufferRead16LE(Connection->Buffer);
- if(PayloadSize <= 0 || PayloadSize > NARRAY(Connection->Buffer)){
+ if(Connection->RWSize != 0){
+ Connection->State = CONNECTION_PROCESSING;
+ break;
+ }else if(Connection->RWPosition == 2){
+ int PayloadSize = (int)BufferRead16LE(Connection->Buffer);
+ if(PayloadSize <= 0 || PayloadSize > (int)sizeof(Connection->Buffer)){
CloseConnection(Connection);
break;
}
@@ -199,28 +197,54 @@ void CheckConnectionInput(TConnection *Connection, int Events){
Connection->RWSize = PayloadSize;
Connection->RWPosition = 0;
}else{
- Connection->State = CONNECTION_LOGIN;
- break;
+ PANIC("Invalid input state (State: %d, RWSize: %d, RWPosition: %d)",
+ Connection->State, Connection->RWSize, Connection->RWPosition);
}
}
}
+}
- if(Connection->State == CONNECTION_LOGIN){
+static void CheckConnectionRequest(TConnection *Connection){
+ if(Connection->Socket == -1){
+ return;
+ }
+
+ if(Connection->State != CONNECTION_PROCESSING){
+ return;
+ }
+
+ // PARANOID(fusion): A non-empty payload is guaranteed, due to how we parse
+ // input in `CheckConnectionInput` just above.
+ ASSERT(Connection->RWSize > 0);
+
+ int Command = Connection->Buffer[0];
+ if(Command == 1){
ProcessLoginRequest(Connection);
+ }else if(Command == 255){
+ ProcessStatusRequest(Connection);
+ }else{
+ LOG_ERR("Invalid command %d from %s (expected 1 or 255)",
+ Command, Connection->RemoteAddress);
+ CloseConnection(Connection);
}
}
-void CheckConnectionOutput(TConnection *Connection, int Events){
- if((Events & POLLOUT) == 0 || Connection->Socket == -1){
+static void CheckConnectionOutput(TConnection *Connection, int Events){
+ // NOTE(fusion): We're only polling `POLLOUT` when the connection is WRITING,
+ // but we want to allow requests to complete in a single cycle, so we always
+ // check for output if the connection is WRITING.
+ (void)Events;
+
+ if(Connection->Socket == -1){
return;
}
- if(Connection->State != CONNECTION_WRITE){
+ if(Connection->State != CONNECTION_WRITING){
return;
}
while(true){
- int BytesWritten = write(Connection->Socket,
+ int BytesWritten = (int)write(Connection->Socket,
(Connection->Buffer + Connection->RWPosition),
(Connection->RWSize - Connection->RWPosition));
if(BytesWritten == -1){
@@ -238,18 +262,18 @@ void CheckConnectionOutput(TConnection *Connection, int Events){
}
}
-void CheckConnection(TConnection *Connection, int Events){
+static void CheckConnection(TConnection *Connection, int Events){
ASSERT((Events & POLLNVAL) == 0);
if((Events & (POLLERR | POLLHUP)) != 0){
CloseConnection(Connection);
}
- if(g_LoginTimeout > 0){
- int LoginTime = (g_MonotonicTimeMS - Connection->StartTime);
- if(LoginTime >= g_LoginTimeout){
- LOG_WARN("Connection %s TIMEDOUT (LoginTime: %dms, Timeout: %dms)",
- Connection->RemoteAddress, LoginTime, g_LoginTimeout);
+ if(g_Config.ConnectionTimeout > 0){
+ int ElapsedTime = GetMonotonicUptime() - Connection->StartTime;
+ if(ElapsedTime >= g_Config.ConnectionTimeout){
+ LOG_WARN("Connection %s TIMEDOUT (ElapsedTime: %ds, Timeout: %ds)",
+ Connection->RemoteAddress, ElapsedTime, g_Config.ConnectionTimeout);
CloseConnection(Connection);
}
}
@@ -259,8 +283,12 @@ void CheckConnection(TConnection *Connection, int Events){
}
}
-void ProcessConnections(void){
- // NOTE(fusion): Accept new connections.
+static void AcceptConnections(int Events){
+ ASSERT(g_Listener != -1);
+ if((Events & POLLIN) == 0){
+ return;
+ }
+
while(true){
uint32 Addr;
uint16 Port;
@@ -270,56 +298,78 @@ void ProcessConnections(void){
}
if(AssignConnection(Socket, Addr, Port) == NULL){
- LOG_ERR("Rejecting connection from %08X:%d due to max number of"
- " connections being reached (%d)", Addr, Port, g_MaxConnections);
+ LOG_ERR("Rejecting connection %08X:%d:"
+ " max number of connections reached (%d)",
+ Addr, Port, g_Config.MaxConnections);
close(Socket);
}
}
+}
- // NOTE(fusion): Gather active connections.
- int NumConnections = 0;
- int *ConnectionIndices = (int*)alloca(g_MaxConnections * sizeof(int));
- pollfd *ConnectionFds = (pollfd*)alloca(g_MaxConnections * sizeof(pollfd));
- for(int i = 0; i < g_MaxConnections; i += 1){
- if(g_Connections[i].State == CONNECTION_FREE || g_Connections[i].Socket == -1){
- continue;
- }
+void ProcessConnections(void){
+ int NumFds = 0;
+ int MaxFds = g_MaxConnections + 1;
+ pollfd *Fds = (pollfd*)alloca(MaxFds * sizeof(pollfd));
+ int *ConnectionIndices = (int*)alloca(MaxFds * sizeof(int));
- ConnectionIndices[NumConnections] = i;
- ConnectionFds[NumConnections].fd = g_Connections[i].Socket;
- ConnectionFds[NumConnections].events = POLLIN | POLLOUT;
- ConnectionFds[NumConnections].revents = 0;
- NumConnections += 1;
+ if(g_Listener != -1){
+ Fds[NumFds].fd = g_Listener;
+ Fds[NumFds].events = POLLIN;
+ Fds[NumFds].revents = 0;
+ ConnectionIndices[NumFds] = -1;
+ NumFds += 1;
}
- if(NumConnections <= 0){
- return;
+ for(int i = 0; i < g_Config.MaxConnections; i += 1){
+ if(g_Connections[i].State == CONNECTION_FREE){
+ continue;
+ }
+
+ Fds[NumFds].fd = g_Connections[i].Socket;
+ Fds[NumFds].events = POLLIN;
+ if(g_Connections[i].State == CONNECTION_WRITING){
+ Fds[NumFds].events |= POLLOUT;
+ }
+ Fds[NumFds].revents = 0;
+ ConnectionIndices[NumFds] = i;
+ NumFds += 1;
}
- // NOTE(fusion): Poll connections.
- int NumEvents = poll(ConnectionFds, NumConnections, 0);
+ // NOTE(fusion): Block for 1 second at most, so we can properly timeout
+ // idle connections.
+ ASSERT(NumFds > 0);
+ int NumEvents = poll(Fds, NumFds, 1);
if(NumEvents == -1){
- LOG_ERR("Failed to poll connections: (%d) %s", errno, strerrordesc_np(errno));
+ if(errno != ETIMEDOUT && errno != EINTR){
+ LOG_ERR("Failed to poll connections: (%d) %s",
+ errno, strerrordesc_np(errno));
+ }
return;
}
// NOTE(fusion): Process connections.
- for(int i = 0; i < NumConnections; i += 1){
- TConnection *Connection = &g_Connections[ConnectionIndices[i]];
- int Events = (int)ConnectionFds[i].revents;
- CheckConnectionInput(Connection, Events);
- CheckConnectionOutput(Connection, Events);
- CheckConnection(Connection, Events);
+ for(int i = 0; i < NumFds; i += 1){
+ int Index = ConnectionIndices[i];
+ int Events = (int)Fds[i].revents;
+ if(Index >= 0 && Index < g_Config.MaxConnections){
+ TConnection *Connection = &g_Connections[Index];
+ CheckConnectionInput(Connection, Events);
+ CheckConnectionRequest(Connection);
+ CheckConnectionOutput(Connection, Events);
+ CheckConnection(Connection, Events);
+ }else if(Index == -1 && Fds[i].fd == g_Listener){
+ AcceptConnections(Events);
+ }else{
+ LOG_ERR("Unknown connection index %d", Index);
+ }
}
}
bool InitConnections(void){
+ ASSERT(g_PrivateKey == NULL);
ASSERT(g_Listener == -1);
ASSERT(g_Connections == NULL);
-
- LOG("Login port: %d", g_LoginPort);
- LOG("Max connections: %d", g_MaxConnections);
- LOG("Login timeout: %dms", g_LoginTimeout);
+ ASSERT(g_StatusRecords == NULL);
g_PrivateKey = RSALoadPEM("tibia.pem");
if(g_PrivateKey == NULL){
@@ -327,17 +377,23 @@ bool InitConnections(void){
return false;
}
- g_Listener = ListenerBind(g_LoginPort);
+ g_Listener = ListenerBind((uint16)g_Config.LoginPort);
if(g_Listener == -1){
- LOG_ERR("Failed to bind listener");
+ LOG_ERR("Failed to bind listener to port %d", g_Config.LoginPort);
return false;
}
- g_Connections = (TConnection*)calloc(g_MaxConnections, sizeof(TConnection));
+ g_MaxConnections = g_Config.MaxConnections;
+ g_Connections = (TConnection*)calloc(
+ g_MaxConnections, sizeof(TConnection));
for(int i = 0; i < g_MaxConnections; i += 1){
g_Connections[i].State = CONNECTION_FREE;
}
+ g_MaxStatusRecords = g_Config.MaxStatusRecords;
+ g_StatusRecords = (TStatusRecord*)calloc(
+ g_MaxStatusRecords, sizeof(TStatusRecord));
+
return true;
}
@@ -360,13 +416,18 @@ void ExitConnections(void){
free(g_Connections);
g_Connections = NULL;
}
+
+ if(g_StatusRecords != NULL){
+ free(g_StatusRecords);
+ g_StatusRecords = NULL;
+ }
}
-// Connection Requests
+// Login Request
//==============================================================================
-TWriteBuffer PrepareResponse(TConnection *Connection){
- if(Connection->State != CONNECTION_LOGIN){
- LOG_ERR("Connection %s is not processing login (State: %d)",
+static TWriteBuffer PrepareXTEAResponse(TConnection *Connection){
+ if(Connection->State != CONNECTION_PROCESSING){
+ LOG_ERR("Connection %s is not PROCESSING (State: %d)",
Connection->RemoteAddress, Connection->State);
CloseConnection(Connection);
return TWriteBuffer(NULL, 0);
@@ -378,9 +439,9 @@ TWriteBuffer PrepareResponse(TConnection *Connection){
return WriteBuffer;
}
-void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
- if(Connection->State != CONNECTION_LOGIN){
- LOG_ERR("Connection %s is not processing login (State: %d)",
+static void SendXTEAResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
+ if(Connection->State != CONNECTION_PROCESSING){
+ LOG_ERR("Connection %s is not PROCESSING (State: %d)",
Connection->RemoteAddress, Connection->State);
CloseConnection(Connection);
return;
@@ -410,25 +471,25 @@ void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
XTEAEncrypt(Connection->XTEA,
WriteBuffer->Buffer + 2,
WriteBuffer->Position - 2);
- Connection->State = CONNECTION_WRITE;
+ Connection->State = CONNECTION_WRITING;
Connection->RWSize = WriteBuffer->Position;
Connection->RWPosition = 0;
}
-void SendLoginError(TConnection *Connection, const char *Message){
- TWriteBuffer WriteBuffer = PrepareResponse(Connection);
+static void SendLoginError(TConnection *Connection, const char *Message){
+ TWriteBuffer WriteBuffer = PrepareXTEAResponse(Connection);
WriteBuffer.Write8(10); // LOGIN_ERROR
WriteBuffer.WriteString(Message);
- SendResponse(Connection, &WriteBuffer);
+ SendXTEAResponse(Connection, &WriteBuffer);
}
-void SendCharacterList(TConnection *Connection, int NumCharacters,
+static void SendCharacterList(TConnection *Connection, int NumCharacters,
TCharacterLoginData *Characters, int PremiumDays){
- TWriteBuffer WriteBuffer = PrepareResponse(Connection);
+ TWriteBuffer WriteBuffer = PrepareXTEAResponse(Connection);
- if(g_Motd[0] != 0){
+ if(g_Config.Motd[0] != 0){
WriteBuffer.Write8(20); // MOTD
- WriteBuffer.WriteString(g_Motd);
+ WriteBuffer.WriteString(g_Config.Motd);
}
WriteBuffer.Write8(100); // CHARACTER_LIST
@@ -444,35 +505,28 @@ void SendCharacterList(TConnection *Connection, int NumCharacters,
}
WriteBuffer.Write16((uint16)PremiumDays);
- SendResponse(Connection, &WriteBuffer);
+ SendXTEAResponse(Connection, &WriteBuffer);
}
void ProcessLoginRequest(TConnection *Connection){
if(Connection->RWSize != 145){
- LOG_ERR("Invalid login request size %d from %s (expected 145)",
- Connection->RWSize, Connection->RemoteAddress);
+ LOG_ERR("Invalid login request size from %s (expected 145, got %d)",
+ Connection->RemoteAddress, Connection->RWSize);
CloseConnection(Connection);
return;
}
- TReadBuffer InputBuffer(Connection->Buffer, Connection->RWSize);
- int Command = InputBuffer.Read8();
- if(Command != 1){
- LOG_ERR("Invalid login command %d from %s (expected 1)",
- Command, Connection->RemoteAddress);
- CloseConnection(Connection);
- return;
- }
-
- int TerminalType = InputBuffer.Read16();
- int TerminalVersion = InputBuffer.Read16();
- InputBuffer.Read32(); // DATSIGNATURE
- InputBuffer.Read32(); // SPRSIGNATURE
- InputBuffer.Read32(); // PICSIGNATURE
+ TReadBuffer ReadBuffer(Connection->Buffer, Connection->RWSize);
+ ReadBuffer.Read8(); // always 1 for a login request
+ int TerminalType = ReadBuffer.Read16();
+ int TerminalVersion = ReadBuffer.Read16();
+ ReadBuffer.Read32(); // DATSIGNATURE
+ ReadBuffer.Read32(); // SPRSIGNATURE
+ ReadBuffer.Read32(); // PICSIGNATURE
uint8 AsymmetricData[128];
- InputBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData));
- if(InputBuffer.Overflowed()){
+ ReadBuffer.ReadBytes(AsymmetricData, sizeof(AsymmetricData));
+ if(ReadBuffer.Overflowed()){
LOG_ERR("Input buffer overflowed while reading login command from %s",
Connection->RemoteAddress);
CloseConnection(Connection);
@@ -489,17 +543,17 @@ void ProcessLoginRequest(TConnection *Connection){
return;
}
- TReadBuffer Buffer(AsymmetricData, sizeof(AsymmetricData));
- Buffer.Read8(); // always zero
- Connection->XTEA[0] = Buffer.Read32();
- Connection->XTEA[1] = Buffer.Read32();
- Connection->XTEA[2] = Buffer.Read32();
- Connection->XTEA[3] = Buffer.Read32();
+ ReadBuffer = TReadBuffer(AsymmetricData, sizeof(AsymmetricData));
+ ReadBuffer.Read8(); // always zero
+ Connection->XTEA[0] = ReadBuffer.Read32();
+ Connection->XTEA[1] = ReadBuffer.Read32();
+ Connection->XTEA[2] = ReadBuffer.Read32();
+ Connection->XTEA[3] = ReadBuffer.Read32();
char Password[30];
- int AccountID = Buffer.Read32();
- Buffer.ReadString(Password, sizeof(Password));
- if(Buffer.Overflowed()){
+ int AccountID = ReadBuffer.Read32();
+ ReadBuffer.ReadString(Password, sizeof(Password));
+ if(ReadBuffer.Overflowed()){
LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress);
CloseConnection(Connection);
return;
@@ -519,10 +573,17 @@ void ProcessLoginRequest(TConnection *Connection){
return;
}
+ char IPString[16];
+ StringBufFormat(IPString, "%d.%d.%d.%d",
+ ((Connection->IPAddress >> 24) & 0xFF),
+ ((Connection->IPAddress >> 16) & 0xFF),
+ ((Connection->IPAddress >> 8) & 0xFF),
+ ((Connection->IPAddress >> 0) & 0xFF));
+
int NumCharacters = 0;
int PremiumDays = 0;
TCharacterLoginData Characters[50];
- int LoginCode = LoginAccount(AccountID, Password, Connection->IPAddress,
+ int LoginCode = LoginAccount(AccountID, Password, IPString,
NARRAY(Characters), &NumCharacters, Characters, &PremiumDays);
switch(LoginCode){
case 0:{
@@ -565,3 +626,84 @@ void ProcessLoginRequest(TConnection *Connection){
}
}
}
+
+// Status Request
+//==============================================================================
+static bool AllowStatusRequest(int IPAddress){
+ TStatusRecord *Record = NULL;
+ int LeastRecentlyUsedIndex = 0;
+ int LeastRecentlyUsedTime = g_StatusRecords[0].Timestamp;
+ int TimeNow = GetMonotonicUptime();
+ for(int i = 0; i < g_MaxStatusRecords; i += 1){
+ if(g_StatusRecords[i].Timestamp < LeastRecentlyUsedTime){
+ LeastRecentlyUsedIndex = i;
+ LeastRecentlyUsedTime = g_StatusRecords[i].Timestamp;
+ }
+
+ if(g_StatusRecords[i].IPAddress == IPAddress){
+ Record = &g_StatusRecords[i];
+ break;
+ }
+ }
+
+ bool Result = false;
+ if(Record == NULL){
+ Record = &g_StatusRecords[LeastRecentlyUsedIndex];
+ Record->IPAddress = IPAddress;
+ Record->Timestamp = TimeNow;
+ Result = true;
+ }else if((TimeNow - Record->Timestamp) >= g_Config.MinStatusInterval){
+ Record->Timestamp = TimeNow;
+ Result = true;
+ }
+
+ return Result;
+}
+
+static void SendStatusString(TConnection *Connection, const char *StatusString){
+ if(Connection->State != CONNECTION_PROCESSING){
+ LOG_ERR("Connection %s is not PROCESSING (State: %d)",
+ Connection->RemoteAddress, Connection->State);
+ CloseConnection(Connection);
+ return;
+ }
+
+ int Length = (int)strlen(StatusString);
+ if(Length > (int)sizeof(Connection->Buffer)){
+ Length = (int)sizeof(Connection->Buffer);
+ }
+
+ Connection->RWSize = Length;
+ Connection->RWPosition = 0;
+ memcpy(Connection->Buffer, StatusString, Length);
+ Connection->State = CONNECTION_WRITING;
+}
+
+void ProcessStatusRequest(TConnection *Connection){
+ if(!AllowStatusRequest(Connection->IPAddress)){
+ LOG_ERR("Too many status requests from %s", Connection->RemoteAddress);
+ CloseConnection(Connection);
+ return;
+ }
+
+ TReadBuffer ReadBuffer(Connection->Buffer, Connection->RWSize);
+ ReadBuffer.Read8(); // always 255 for a status request
+ int Format = (int)ReadBuffer.Read8();
+ if(Format == 255){ // XML
+ char Request[5] = {};
+ ReadBuffer.ReadBytes((uint8*)Request, 4);
+ if(StringEqCI(Request, "info")){
+ const char *StatusString = GetStatusString();
+ SendStatusString(Connection, StatusString);
+ }else{
+ LOG_WARN("Invalid status request \"%s\" from %s",
+ Request, Connection->RemoteAddress);
+ CloseConnection(Connection);
+ }
+ }else{
+ LOG_WARN("Invalid status format %d from %s",
+ Format, Connection->RemoteAddress);
+ CloseConnection(Connection);
+ }
+}
+
diff --git a/src/crypto.cc b/src/crypto.cc
index 8be90fd..6e50b33 100644
--- a/src/crypto.cc
+++ b/src/crypto.cc
@@ -1,4 +1,4 @@
-#include "login.hh"
+#include "common.hh"
#include <openssl/err.h>
#include <openssl/rsa.h>
@@ -7,7 +7,9 @@
static void DumpOpenSSLErrors(const char *Where, const char *What){
LOG_ERR("OpenSSL error(s) while executing %s at %s:", What, Where);
ERR_print_errors_cb(
- [](const char *str, usize len, void *u) -> int {
+ [](const char *str, size_t len, void *u) -> int {
+ (void)u;
+
// NOTE(fusion): These error strings already have trailing newlines,
// for whatever reason.
if(len > 0 && str[len - 1] == '\n'){
diff --git a/src/login.cc b/src/login.cc
deleted file mode 100644
index a4fa5b6..0000000
--- a/src/login.cc
+++ /dev/null
@@ -1,130 +0,0 @@
-#include "login.hh"
-
-// TODO(fusion): Support windows eventually?
-#if OS_LINUX
-# include <errno.h>
-# include <signal.h>
-#else
-# error "Operating system not currently supported."
-#endif
-
-// Shutdown Signal
-int g_ShutdownSignal = 0;
-
-// Time
-int g_MonotonicTimeMS = 0;
-
-// Config
-char g_Motd[256] = "";
-int g_UpdateRate = 20;
-int g_LoginPort = 7171;
-int g_MaxConnections = 10;
-int g_LoginTimeout = 10000;
-char g_QueryManagerHost[100] = "127.0.0.1";
-int g_QueryManagerPort = 7174;
-char g_QueryManagerPassword[30] = "";
-
-static void ParseMotd(char *Dest, int DestCapacity, const char *String){
- char *Motd = (char*)alloca(DestCapacity);
- ParseString(Motd, DestCapacity, String);
- if(Motd[0] != 0){
- snprintf(Dest, DestCapacity, "%u\n%s", HashString(Motd), Motd);
- }
-}
-
-static void LoginKVCallback(const char *Key, const char *Val){
- if(StringEqCI(Key, "LoginPort")){
- ParseInteger(&g_LoginPort, Val);
- }else if(StringEqCI(Key, "MaxConnections")){
- ParseInteger(&g_MaxConnections, Val);
- }else if(StringEqCI(Key, "LoginTimeout")){
- ParseDuration(&g_LoginTimeout, Val);
- }else if(StringEqCI(Key, "UpdateRate")){
- ParseInteger(&g_UpdateRate, Val);
- }else if(StringEqCI(Key, "QueryManagerHost")){
- ParseString(g_QueryManagerHost,
- sizeof(g_QueryManagerHost), Val);
- }else if(StringEqCI(Key, "QueryManagerPort")){
- ParseInteger(&g_QueryManagerPort, Val);
- }else if(StringEqCI(Key, "QueryManagerPassword")){
- ParseString(g_QueryManagerPassword,
- sizeof(g_QueryManagerPassword), Val);
- }else if(StringEqCI(Key, "MOTD")){
- ParseMotd(g_Motd, sizeof(g_Motd), Val);
- }else{
- LOG_WARN("Unknown config \"%s\"", Key);
- }
-}
-
-static bool SigHandler(int SigNr, sighandler_t Handler){
- struct sigaction Action = {};
- Action.sa_handler = Handler;
- sigfillset(&Action.sa_mask);
- if(sigaction(SigNr, &Action, NULL) == -1){
- LOG_ERR("Failed to change handler for signal %d (%s): (%d) %s",
- SigNr, sigdescr_np(SigNr), errno, strerrordesc_np(errno));
- return false;
- }
- return true;
-}
-
-static void ShutdownHandler(int SigNr){
- g_ShutdownSignal = SigNr;
-}
-
-int main(int argc, char **argv){
- (void)argc;
- (void)argv;
-
- g_ShutdownSignal = 0;
- if(!SigHandler(SIGPIPE, SIG_IGN)
- || !SigHandler(SIGINT, ShutdownHandler)
- || !SigHandler(SIGTERM, ShutdownHandler)){
- return EXIT_FAILURE;
- }
-
- int64 StartTime = GetClockMonotonicMS();
- g_MonotonicTimeMS = 0;
-
- LOG("Tibia Login Server v0.1");
- if(!ReadConfig("config.cfg", LoginKVCallback)){
- return EXIT_FAILURE;
- }
-
- atexit(ExitConnections);
- atexit(ExitQuery);
-
- if(!InitConnections()){
- return EXIT_FAILURE;
- }
-
- if(!InitQuery()){
- return EXIT_FAILURE;
- }
-
- // NOTE(fusion): Print MOTD with escape codes.
- char Motd[30];
- if(EscapeString(Motd, sizeof(Motd), g_Motd)){
- LOG("MOTD: \"%s\"", Motd);
- }else{
- LOG("MOTD: \"%s...\"", Motd);
- }
-
- LOG("Running at %d updates per second...", g_UpdateRate);
- int64 UpdateInterval = 1000 / (int64)g_UpdateRate;
- while(g_ShutdownSignal == 0){
- int64 UpdateStart = GetClockMonotonicMS();
- g_MonotonicTimeMS = (int)(UpdateStart - StartTime);
- ProcessConnections();
- int64 UpdateEnd = GetClockMonotonicMS();
- int64 NextUpdate = UpdateStart + UpdateInterval;
- if(NextUpdate > UpdateEnd){
- SleepMS(NextUpdate - UpdateEnd);
- }
- }
-
- LOG("Received signal %d (%s), shutting down...",
- g_ShutdownSignal, sigdescr_np(g_ShutdownSignal));
-
- return EXIT_SUCCESS;
-}
diff --git a/src/login.hh b/src/login.hh
deleted file mode 100644
index aa391a1..0000000
--- a/src/login.hh
+++ /dev/null
@@ -1,401 +0,0 @@
-#ifndef TIBIA_LOGIN_HH_
-#define TIBIA_LOGIN_HH_ 1
-
-#include "common.hh"
-
-// Shutdown Signal
-extern int g_ShutdownSignal;
-
-// Time
-extern int g_MonotonicTimeMS;
-
-// Config
-extern char g_Motd[256];
-extern int g_UpdateRate;
-extern int g_LoginPort;
-extern int g_MaxConnections;
-extern int g_LoginTimeout;
-extern char g_QueryManagerHost[100];
-extern int g_QueryManagerPort;
-extern char g_QueryManagerPassword[30];
-
-// Buffer Utility
-//==============================================================================
-inline uint8 BufferRead8(const uint8 *Buffer){
- return Buffer[0];
-}
-
-inline uint16 BufferRead16LE(const uint8 *Buffer){
- return (uint16)Buffer[0]
- | ((uint16)Buffer[1] << 8);
-}
-
-inline uint16 BufferRead16BE(const uint8 *Buffer){
- return ((uint16)Buffer[0] << 8)
- | (uint16)Buffer[1];
-}
-
-inline uint32 BufferRead32LE(const uint8 *Buffer){
- return (uint32)Buffer[0]
- | ((uint32)Buffer[1] << 8)
- | ((uint32)Buffer[2] << 16)
- | ((uint32)Buffer[3] << 24);
-}
-
-inline uint32 BufferRead32BE(const uint8 *Buffer){
- return ((uint32)Buffer[0] << 24)
- | ((uint32)Buffer[1] << 16)
- | ((uint32)Buffer[2] << 8)
- | (uint32)Buffer[3];
-}
-
-inline uint64 BufferRead64LE(const uint8 *Buffer){
- return (uint64)Buffer[0]
- | ((uint64)Buffer[1] << 8)
- | ((uint64)Buffer[2] << 16)
- | ((uint64)Buffer[3] << 24)
- | ((uint64)Buffer[4] << 32)
- | ((uint64)Buffer[5] << 40)
- | ((uint64)Buffer[6] << 48)
- | ((uint64)Buffer[7] << 56);
-}
-
-inline uint64 BufferRead64BE(const uint8 *Buffer){
- return ((uint64)Buffer[0] << 56)
- | ((uint64)Buffer[1] << 48)
- | ((uint64)Buffer[2] << 40)
- | ((uint64)Buffer[3] << 32)
- | ((uint64)Buffer[4] << 24)
- | ((uint64)Buffer[5] << 16)
- | ((uint64)Buffer[6] << 8)
- | (uint64)Buffer[7];
-}
-
-inline void BufferWrite8(uint8 *Buffer, uint8 Value){
- Buffer[0] = Value;
-}
-
-inline void BufferWrite16LE(uint8 *Buffer, uint16 Value){
- Buffer[0] = (uint8)(Value >> 0);
- Buffer[1] = (uint8)(Value >> 8);
-}
-
-inline void BufferWrite16BE(uint8 *Buffer, uint16 Value){
- Buffer[0] = (uint8)(Value >> 8);
- Buffer[1] = (uint8)(Value >> 0);
-}
-
-inline void BufferWrite32LE(uint8 *Buffer, uint32 Value){
- Buffer[0] = (uint8)(Value >> 0);
- Buffer[1] = (uint8)(Value >> 8);
- Buffer[2] = (uint8)(Value >> 16);
- Buffer[3] = (uint8)(Value >> 24);
-}
-
-inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
- Buffer[0] = (uint8)(Value >> 24);
- Buffer[1] = (uint8)(Value >> 16);
- Buffer[2] = (uint8)(Value >> 8);
- Buffer[3] = (uint8)(Value >> 0);
-}
-
-inline void BufferWrite64LE(uint8 *Buffer, uint64 Value){
- Buffer[0] = (uint8)(Value >> 0);
- Buffer[1] = (uint8)(Value >> 8);
- Buffer[2] = (uint8)(Value >> 16);
- Buffer[3] = (uint8)(Value >> 24);
- Buffer[4] = (uint8)(Value >> 32);
- Buffer[5] = (uint8)(Value >> 40);
- Buffer[6] = (uint8)(Value >> 48);
- Buffer[7] = (uint8)(Value >> 56);
-}
-
-inline void BufferWrite64BE(uint8 *Buffer, uint64 Value){
- Buffer[0] = (uint8)(Value >> 56);
- Buffer[1] = (uint8)(Value >> 48);
- Buffer[2] = (uint8)(Value >> 40);
- Buffer[3] = (uint8)(Value >> 32);
- Buffer[4] = (uint8)(Value >> 24);
- Buffer[5] = (uint8)(Value >> 16);
- Buffer[6] = (uint8)(Value >> 8);
- Buffer[7] = (uint8)(Value >> 0);
-}
-
-struct TReadBuffer{
- uint8 *Buffer;
- int Size;
- int Position;
-
- TReadBuffer(void) : TReadBuffer(NULL, 0) {}
- TReadBuffer(uint8 *Buffer, int Size)
- : Buffer(Buffer), Size(Size), Position(0) {}
-
- bool CanRead(int Bytes){
- return (this->Position + Bytes) <= this->Size;
- }
-
- bool Overflowed(void){
- return this->Position > this->Size;
- }
-
- bool ReadFlag(void){
- return this->Read8() != 0x00;
- }
-
- uint8 Read8(void){
- uint8 Result = 0;
- if(this->CanRead(1)){
- Result = BufferRead8(this->Buffer + this->Position);
- }
- this->Position += 1;
- return Result;
- }
-
- uint16 Read16(void){
- uint16 Result = 0;
- if(this->CanRead(2)){
- Result = BufferRead16LE(this->Buffer + this->Position);
- }
- this->Position += 2;
- return Result;
- }
-
- uint16 Read16BE(void){
- uint16 Result = 0;
- if(this->CanRead(2)){
- Result = BufferRead16BE(this->Buffer + this->Position);
- }
- this->Position += 2;
- return Result;
- }
-
- uint32 Read32(void){
- uint32 Result = 0;
- if(this->CanRead(4)){
- Result = BufferRead32LE(this->Buffer + this->Position);
- }
- this->Position += 4;
- return Result;
- }
-
- uint32 Read32BE(void){
- uint32 Result = 0;
- if(this->CanRead(4)){
- Result = BufferRead32BE(this->Buffer + this->Position);
- }
- this->Position += 4;
- return Result;
- }
-
- void ReadString(char *Dest, int DestCapacity){
- int Length = (int)this->Read16();
- if(Length == 0xFFFF){
- Length = (int)this->Read32();
- }
-
- if(Dest != NULL && DestCapacity > 0){
- if(Length < DestCapacity && this->CanRead(Length)){
- memcpy(Dest, this->Buffer + this->Position, Length);
- Dest[Length] = 0;
- }else{
- Dest[0] = 0;
- }
- }
-
- this->Position += Length;
- }
-
- void ReadBytes(uint8 *Buffer, int Count){
- if(this->CanRead(Count)){
- memcpy(Buffer, this->Buffer + this->Position, Count);
- }
- this->Position += Count;
- }
-};
-
-struct TWriteBuffer{
- uint8 *Buffer;
- int Size;
- int Position;
-
- TWriteBuffer(void) : TWriteBuffer(NULL, 0) {}
- TWriteBuffer(uint8 *Buffer, int Size)
- : Buffer(Buffer), Size(Size), Position(0) {}
-
- bool CanWrite(int Bytes){
- return (this->Position + Bytes) <= this->Size;
- }
-
- bool Overflowed(void){
- return this->Position > this->Size;
- }
-
- void WriteFlag(bool Value){
- this->Write8(Value ? 0x01 : 0x00);
- }
-
- void Write8(uint8 Value){
- if(this->CanWrite(1)){
- BufferWrite8(this->Buffer + this->Position, Value);
- }
- this->Position += 1;
- }
-
- void Write16(uint16 Value){
- if(this->CanWrite(2)){
- BufferWrite16LE(this->Buffer + this->Position, Value);
- }
- this->Position += 2;
- }
-
- void Write16BE(uint16 Value){
- if(this->CanWrite(2)){
- BufferWrite16BE(this->Buffer + this->Position, Value);
- }
- this->Position += 2;
- }
-
- void Write32(uint32 Value){
- if(this->CanWrite(4)){
- BufferWrite32LE(this->Buffer + this->Position, Value);
- }
- this->Position += 4;
- }
-
- void Write32BE(uint32 Value){
- if(this->CanWrite(4)){
- BufferWrite32BE(this->Buffer + this->Position, Value);
- }
- this->Position += 4;
- }
-
- void WriteString(const char *String){
- int StringLength = 0;
- if(String != NULL){
- StringLength = (int)strlen(String);
- }
-
- if(StringLength < 0xFFFF){
- this->Write16((uint16)StringLength);
- }else{
- this->Write16(0xFFFF);
- this->Write32((uint32)StringLength);
- }
-
- if(StringLength > 0 && this->CanWrite(StringLength)){
- memcpy(this->Buffer + this->Position, String, StringLength);
- }
-
- this->Position += StringLength;
- }
-
- void Rewrite16(int Position, uint16 Value){
- if((Position + 2) <= this->Position && !this->Overflowed()){
- BufferWrite16LE(this->Buffer + Position, Value);
- }
- }
-
- void Insert32(int Position, uint32 Value){
- if(Position <= this->Position){
- if(this->CanWrite(4)){
- memmove(this->Buffer + Position + 4,
- this->Buffer + Position,
- this->Position - Position);
- BufferWrite32LE(this->Buffer + Position, Value);
- }
-
- this->Position += 4;
- }
- }
-};
-
-// crypto.cc
-//==============================================================================
-typedef void RSAKey;
-RSAKey *RSALoadPEM(const char *FileName);
-void RSAFree(RSAKey *Key);
-bool RSADecrypt(RSAKey *Key, uint8 *Data, int Size);
-void XTEAEncrypt(const uint32 *Key, uint8 *Data, int Size);
-void XTEADecrypt(const uint32 *Key, uint8 *Data, int Size);
-
-// query.cc
-//==============================================================================
-enum : int {
- APPLICATION_TYPE_GAME = 1,
- APPLICATION_TYPE_LOGIN = 2,
- APPLICATION_TYPE_WEB = 3,
-};
-
-enum : int {
- QUERY_STATUS_OK = 0,
- QUERY_STATUS_ERROR = 1,
- QUERY_STATUS_FAILED = 3,
-};
-
-struct TCharacterLoginData{
- char Name[30];
- char WorldName[30];
- int WorldAddress;
- int WorldPort;
-};
-
-struct TQueryManagerConnection{
- int Socket;
- uint8 Buffer[KB(16)];
-};
-
-bool Connect(TQueryManagerConnection *Connection);
-void Disconnect(TQueryManagerConnection *Connection);
-bool IsConnected(TQueryManagerConnection *Connection);
-TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType);
-int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
- TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer);
-int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
- int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
- int *PremiumDays);
-bool InitQuery(void);
-void ExitQuery(void);
-
-// connections.cc
-//==============================================================================
-enum ConnectionState{
- CONNECTION_FREE = 0,
- CONNECTION_HANDSHAKE = 1,
- CONNECTION_LOGIN = 2,
- CONNECTION_WRITE = 3,
-};
-
-struct TConnection{
- ConnectionState State;
- int Socket;
- int StartTime;
- int RWSize;
- int RWPosition;
- uint32 RandomSeed;
- uint32 XTEA[4];
- char IPAddress[16];
- char RemoteAddress[30];
- uint8 Buffer[KB(2)];
-};
-
-int ListenerBind(uint16 Port);
-int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort);
-void CloseConnection(TConnection *Connection);
-TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port);
-void ReleaseConnection(TConnection *Connection);
-void ProcessLoginRequest(TConnection *Connection);
-void CheckConnectionInput(TConnection *Connection, int Events);
-void CheckConnectionOutput(TConnection *Connection, int Events);
-void CheckConnection(TConnection *Connection, int Events);
-void ProcessConnections(void);
-bool InitConnections(void);
-void ExitConnections(void);
-
-TWriteBuffer PrepareResponse(TConnection *Connection);
-void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer);
-void SendLoginError(TConnection *Connection, const char *Message);
-void SendCharacterList(TConnection *Connection, int NumCharacters,
- TCharacterLoginData *Characters, int PremiumDays);
-void ProcessLoginRequest(TConnection *Connection);
-
-#endif //TIBIA_LOGIN_HH_
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..637da2f
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,709 @@
+#include "common.hh"
+
+#include <errno.h>
+#include <signal.h>
+
+int64 g_StartTimeMS = 0;
+int g_ShutdownSignal = 0;
+TConfig g_Config = {};
+
+void LogAdd(const char *Prefix, const char *Format, ...){
+ char Entry[4096];
+ va_list ap;
+ va_start(ap, Format);
+ vsnprintf(Entry, sizeof(Entry), Format, ap);
+ va_end(ap);
+
+ // NOTE(fusion): Trim trailing whitespace.
+ int Length = (int)strlen(Entry);
+ while(Length > 0 && isspace(Entry[Length - 1])){
+ Entry[Length - 1] = 0;
+ Length -= 1;
+ }
+
+ if(Length > 0){
+ char TimeString[128];
+ StringBufFormatTime(TimeString, "%Y-%m-%d %H:%M:%S", (int)time(NULL));
+ fprintf(stdout, "%s [%s] %s\n", TimeString, Prefix, Entry);
+ fflush(stdout);
+ }
+}
+
+void LogAddVerbose(const char *Prefix, const char *Function,
+ const char *File, int Line, const char *Format, ...){
+ char Entry[4096];
+ va_list ap;
+ va_start(ap, Format);
+ vsnprintf(Entry, sizeof(Entry), Format, ap);
+ va_end(ap);
+
+ // NOTE(fusion): Trim trailing whitespace.
+ int Length = (int)strlen(Entry);
+ while(Length > 0 && isspace(Entry[Length - 1])){
+ Entry[Length - 1] = 0;
+ Length -= 1;
+ }
+
+ if(Length > 0){
+ (void)File;
+ (void)Line;
+ char TimeString[128];
+ StringBufFormatTime(TimeString, "%Y-%m-%d %H:%M:%S", (int)time(NULL));
+ fprintf(stdout, "%s [%s] %s: %s\n", TimeString, Prefix, Function, Entry);
+ fflush(stdout);
+ }
+}
+
+struct tm GetLocalTime(time_t t){
+ struct tm result;
+#if COMPILER_MSVC
+ localtime_s(&result, &t);
+#else
+ localtime_r(&t, &result);
+#endif
+ return result;
+}
+
+struct tm GetGMTime(time_t t){
+ struct tm result;
+#if COMPILER_MSVC
+ gmtime_s(&result, &t);
+#else
+ gmtime_r(&t, &result);
+#endif
+ return result;
+}
+
+int64 GetClockMonotonicMS(void){
+#if OS_WINDOWS
+ LARGE_INTEGER Counter, Frequency;
+ QueryPerformanceCounter(&Counter);
+ QueryPerformanceFrequency(&Frequency);
+ return (int64)((Counter.QuadPart * 1000) / Frequency.QuadPart);
+#else
+ // NOTE(fusion): The coarse monotonic clock has a larger resolution but is
+ // supposed to be faster, even avoiding system calls in some cases. It should
+ // be fine for millisecond precision which is what we're using.
+ struct timespec Time;
+ clock_gettime(CLOCK_MONOTONIC_COARSE, &Time);
+ return ((int64)Time.tv_sec * 1000)
+ + ((int64)Time.tv_nsec / 1000000);
+#endif
+}
+
+int GetMonotonicUptime(void){
+ return (int)((GetClockMonotonicMS() - g_StartTimeMS) / 1000);
+}
+
+bool StringEmpty(const char *String){
+ return String[0] == 0;
+}
+
+bool StringEq(const char *A, const char *B){
+ int Index = 0;
+ while(A[Index] != 0 && A[Index] == B[Index]){
+ Index += 1;
+ }
+ return A[Index] == B[Index];
+}
+
+bool StringEqCI(const char *A, const char *B){
+ int Index = 0;
+ while(A[Index] != 0 && tolower(A[Index]) == tolower(B[Index])){
+ Index += 1;
+ }
+ return tolower(A[Index]) == tolower(B[Index]);
+}
+
+bool StringCopy(char *Dest, int DestCapacity, const char *Src){
+ int SrcLength = (Src != NULL ? (int)strlen(Src) : 0);
+ return StringCopyN(Dest, DestCapacity, Src, SrcLength);
+}
+
+bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ ASSERT(DestCapacity > 0);
+ bool Result = (SrcLength < DestCapacity);
+ if(Result && SrcLength > 0){
+ memcpy(Dest, Src, SrcLength);
+ Dest[SrcLength] = 0;
+ }else{
+ Dest[0] = 0;
+ }
+ return Result;
+}
+
+bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...){
+ va_list ap;
+ va_start(ap, Format);
+ int Written = vsnprintf(Dest, DestCapacity, Format, ap);
+ va_end(ap);
+ return Written >= 0 && Written < DestCapacity;
+}
+
+bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp){
+ struct tm tm = GetLocalTime((time_t)Timestamp);
+ int Result = (int)strftime(Dest, DestCapacity, Format, &tm);
+
+ // NOTE(fusion): `strftime` will return ZERO if it's unable to fit the result
+ // in the supplied buffer, which is annoying because ZERO may not represent a
+ // failure if the result is an empty string.
+ ASSERT(Result >= 0 && Result < DestCapacity);
+ if(Result == 0){
+ memset(Dest, 0, DestCapacity);
+ }
+
+ return Result != 0;
+}
+
+void StringClear(char *Dest, int DestCapacity){
+ ASSERT(DestCapacity > 0);
+ memset(Dest, 0, DestCapacity);
+}
+
+uint32 StringHash(const char *String){
+ // FNV1a 32-bits
+ uint32 Hash = 0x811C9DC5U;
+ for(int i = 0; String[i] != 0; i += 1){
+ Hash ^= (uint32)String[i];
+ Hash *= 0x01000193U;
+ }
+ return Hash;
+}
+
+bool StringEscape(char *Dest, int DestCapacity, const char *Src){
+ int WritePos = 0;
+ for(int ReadPos = 0; Src[ReadPos] != 0 && WritePos < DestCapacity; ReadPos += 1){
+ int EscapeCh = -1;
+ switch(Src[ReadPos]){
+ case '\a': EscapeCh = 'a'; break;
+ case '\b': EscapeCh = 'b'; break;
+ case '\t': EscapeCh = 't'; break;
+ case '\n': EscapeCh = 'n'; break;
+ case '\v': EscapeCh = 'v'; break;
+ case '\f': EscapeCh = 'f'; break;
+ case '\r': EscapeCh = 'r'; break;
+ case '\"': EscapeCh = '\"'; break;
+ case '\'': EscapeCh = '\''; break;
+ case '\\': EscapeCh = '\\'; break;
+ }
+
+ if(EscapeCh != -1){
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = '\\';
+ WritePos += 1;
+ }
+
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = EscapeCh;
+ WritePos += 1;
+ }
+ }else{
+ if((WritePos + 1) <= DestCapacity){
+ Dest[WritePos] = Src[ReadPos];
+ WritePos += 1;
+ }
+ }
+ }
+
+ if(WritePos < DestCapacity){
+ Dest[WritePos] = 0;
+ return true;
+ }else{
+ Dest[DestCapacity - 1] = 0;
+ return false;
+ }
+}
+
+int UTF8SequenceSize(uint8 LeadingByte){
+ if((LeadingByte & 0x80) == 0){
+ return 1;
+ }else if((LeadingByte & 0xE0) == 0xC0){
+ return 2;
+ }else if((LeadingByte & 0xF0) == 0xE0){
+ return 3;
+ }else if((LeadingByte & 0xF8) == 0xF0){
+ return 4;
+ }else{
+ return 0;
+ }
+}
+
+bool UTF8IsTrailingByte(uint8 Byte){
+ return (Byte & 0xC0) == 0x80;
+}
+
+int UTF8EncodedSize(int Codepoint){
+ if(Codepoint < 0){
+ return 0;
+ }else if(Codepoint <= 0x7F){
+ return 1;
+ }else if(Codepoint <= 0x07FF){
+ return 2;
+ }else if(Codepoint <= 0xFFFF){
+ return 3;
+ }else if(Codepoint <= 0x10FFFF){
+ return 4;
+ }else{
+ return 0;
+ }
+}
+
+int UTF8FindNextLeadingByte(const char *Src, int SrcLength){
+ int Offset = 0;
+ while(Offset < SrcLength){
+ // NOTE(fusion): Allow the first byte to be a leading byte, in case we
+ // just want to advance from one leading byte to another.
+ if(Offset > 0 && !UTF8IsTrailingByte(Src[Offset])){
+ break;
+ }
+ Offset += 1;
+ }
+ return Offset;
+}
+
+int UTF8DecodeOne(const uint8 *Src, int SrcLength, int *OutCodepoint){
+ if(SrcLength <= 0){
+ return 0;
+ }
+
+ int Size = UTF8SequenceSize(Src[0]);
+ if(Size <= 0 || Size > SrcLength){
+ return 0;
+ }
+
+ for(int i = 1; i < Size; i += 1){
+ if(!UTF8IsTrailingByte(Src[i])){
+ return 0;
+ }
+ }
+
+ int Codepoint = 0;
+ switch(Size){
+ case 1:{
+ Codepoint = (int)Src[0];
+ break;
+ }
+
+ case 2:{
+ Codepoint = ((int)(Src[0] & 0x1F) << 6)
+ | ((int)(Src[1] & 0x3F) << 0);
+ break;
+ }
+
+ case 3:{
+ Codepoint = ((int)(Src[0] & 0x0F) << 12)
+ | ((int)(Src[1] & 0x3F) << 6)
+ | ((int)(Src[2] & 0x3F) << 0);
+ break;
+ }
+
+ case 4:{
+ Codepoint = ((int)(Src[0] & 0x07) << 18)
+ | ((int)(Src[1] & 0x3F) << 12)
+ | ((int)(Src[2] & 0x3F) << 6)
+ | ((int)(Src[3] & 0x3F) << 0);
+ break;
+ }
+ }
+
+ if(OutCodepoint){
+ *OutCodepoint = Codepoint;
+ }
+
+ return Size;
+}
+
+int UTF8EncodeOne(uint8 *Dest, int DestCapacity, int Codepoint){
+ int Size = UTF8EncodedSize(Codepoint);
+ if(Size > 0 && Size <= DestCapacity){
+ switch(Size){
+ case 1:{
+ Dest[0] = (uint8)Codepoint;
+ break;
+ }
+
+ case 2:{
+ Dest[0] = (uint8)(0xC0 | (0x1F & (Codepoint >> 6)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+
+ case 3:{
+ Dest[0] = (uint8)(0xE0 | (0x0F & (Codepoint >> 12)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 6)));
+ Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+
+ case 4:{
+ Dest[0] = (uint8)(0xF0 | (0x07 & (Codepoint >> 18)));
+ Dest[1] = (uint8)(0x80 | (0x3F & (Codepoint >> 12)));
+ Dest[2] = (uint8)(0x80 | (0x3F & (Codepoint >> 6)));
+ Dest[3] = (uint8)(0x80 | (0x3F & (Codepoint >> 0)));
+ break;
+ }
+ }
+ }
+
+ return Size;
+}
+
+// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather
+// convert any characters, INCLUDING the null-terminator, contained in the src
+// string. Invalid or NON-LATIN1 codepoints are translated into '?'.
+int UTF8ToLatin1(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ int ReadPos = 0;
+ int WritePos = 0;
+ while(ReadPos < SrcLength){
+ int Codepoint = -1;
+ int Size = UTF8DecodeOne((uint8*)(Src + ReadPos), (SrcLength - ReadPos), &Codepoint);
+ if(Size > 0){
+ ReadPos += Size;
+ }else{
+ ReadPos += UTF8FindNextLeadingByte((Src + ReadPos), (SrcLength - ReadPos));
+ }
+
+ if(WritePos < DestCapacity){
+ if(Codepoint >= 0 && Codepoint <= 0xFF){
+ Dest[WritePos] = (char)Codepoint;
+ }else{
+ Dest[WritePos] = '?';
+ }
+ }
+ WritePos += 1;
+ }
+
+ return WritePos;
+}
+
+// IMPORTANT(fusion): This function WON'T handle null-termination. It'll rather
+// convert any characters, INCLUDING the null-terminator, contained in the src
+// string. Note that LATIN1 characters translates directly into UNICODE codepoints.
+int Latin1ToUTF8(char *Dest, int DestCapacity, const char *Src, int SrcLength){
+ int WritePos = 0;
+ for(int ReadPos = 0; ReadPos < SrcLength; ReadPos += 1){
+ WritePos += UTF8EncodeOne((uint8*)(Dest + WritePos),
+ (DestCapacity - WritePos), (uint8)Src[ReadPos]);
+ }
+ return WritePos;
+}
+
+bool ParseBoolean(bool *Dest, const char *String){
+ ASSERT(Dest && String);
+ *Dest = StringEqCI(String, "true")
+ || StringEqCI(String, "on")
+ || StringEqCI(String, "yes");
+ return *Dest
+ || StringEqCI(String, "false")
+ || StringEqCI(String, "off")
+ || StringEqCI(String, "no");
+}
+
+bool ParseInteger(int *Dest, const char *String){
+ ASSERT(Dest && String);
+ const char *StringEnd;
+ *Dest = (int)strtol(String, (char**)&StringEnd, 0);
+ return StringEnd > String;
+}
+
+bool ParseDuration(int *Dest, const char *String){
+ ASSERT(Dest && String);
+ const char *Suffix;
+ *Dest = (int)strtol(String, (char**)&Suffix, 0);
+ if(Suffix == String){
+ return false;
+ }
+
+ while(Suffix[0] != 0 && isspace(Suffix[0])){
+ Suffix += 1;
+ }
+
+ if(Suffix[0] == 'S' || Suffix[0] == 's'){
+ *Dest *= (1);
+ }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
+ *Dest *= (60);
+ }else if(Suffix[0] == 'H' || Suffix[0] == 'h'){
+ *Dest *= (60 * 60);
+ }
+
+ return true;
+}
+
+bool ParseSize(int *Dest, const char *String){
+ ASSERT(Dest && String);
+ const char *Suffix;
+ *Dest = (int)strtol(String, (char**)&Suffix, 0);
+ if(Suffix == String){
+ return false;
+ }
+
+ while(Suffix[0] != 0 && isspace(Suffix[0])){
+ Suffix += 1;
+ }
+
+ if(Suffix[0] == 'K' || Suffix[0] == 'k'){
+ *Dest *= (1024);
+ }else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
+ *Dest *= (1024 * 1024);
+ }
+
+ return true;
+}
+
+bool ParseString(char *Dest, int DestCapacity, const char *String){
+ ASSERT(Dest && DestCapacity > 0 && String);
+ int StringStart = 0;
+ int StringEnd = (int)strlen(String);
+ if(StringEnd >= 2){
+ if((String[0] == '"' && String[StringEnd - 1] == '"')
+ || (String[0] == '\'' && String[StringEnd - 1] == '\'')
+ || (String[0] == '`' && String[StringEnd - 1] == '`')){
+ StringStart += 1;
+ StringEnd -= 1;
+ }
+ }
+
+ return StringCopyN(Dest, DestCapacity,
+ &String[StringStart], (StringEnd - StringStart));
+}
+
+void ParseMotd(char *Dest, int DestCapacity, const char *String){
+ char *Motd = (char*)alloca(DestCapacity);
+ ParseString(Motd, DestCapacity, String);
+ if(Motd[0] != 0){
+ StringFormat(Dest, DestCapacity, "%u\n%s", StringHash(Motd), Motd);
+ }
+}
+
+bool ReadConfig(const char *FileName, TConfig *Config){
+ FILE *File = fopen(FileName, "rb");
+ if(File == NULL){
+ LOG_ERR("Failed to open config file \"%s\"", FileName);
+ return false;
+ }
+
+ bool EndOfFile = false;
+ for(int LineNumber = 1; !EndOfFile; LineNumber += 1){
+ const int MaxLineSize = 1024;
+ char Line[MaxLineSize];
+ int LineSize = 0;
+ int KeyStart = -1;
+ int EqualPos = -1;
+ while(true){
+ int ch = fgetc(File);
+ if(ch == EOF || ch == '\n'){
+ if(ch == EOF){
+ EndOfFile = true;
+ }
+ break;
+ }
+
+ if(LineSize < MaxLineSize){
+ Line[LineSize] = (char)ch;
+ }
+
+ if(KeyStart == -1 && !isspace(ch)){
+ KeyStart = LineSize;
+ }
+
+ if(EqualPos == -1 && ch == '='){
+ EqualPos = LineSize;
+ }
+
+ LineSize += 1;
+ }
+
+ // NOTE(fusion): Check line size limit.
+ if(LineSize > MaxLineSize){
+ LOG_WARN("%s:%d: Exceeded line size limit of %d characters",
+ FileName, LineNumber, MaxLineSize);
+ continue;
+ }
+
+ // NOTE(fusion): Check empty line or comment.
+ if(KeyStart == -1 || Line[KeyStart] == '#'){
+ continue;
+ }
+
+ // NOTE(fusion): Check assignment.
+ if(EqualPos == -1){
+ LOG_WARN("%s:%d: No assignment found on non empty line",
+ FileName, LineNumber);
+ continue;
+ }
+
+ // NOTE(fusion): Check empty key.
+ int KeyEnd = EqualPos;
+ while(KeyEnd > KeyStart && isspace(Line[KeyEnd - 1])){
+ KeyEnd -= 1;
+ }
+
+ if(KeyStart == KeyEnd){
+ LOG_WARN("%s:%d: Empty key", FileName, LineNumber);
+ continue;
+ }
+
+ // NOTE(fusion): Check empty value.
+ int ValStart = EqualPos + 1;
+ int ValEnd = LineSize;
+ while(ValStart < ValEnd && isspace(Line[ValStart])){
+ ValStart += 1;
+ }
+
+ while(ValEnd > ValStart && isspace(Line[ValEnd - 1])){
+ ValEnd -= 1;
+ }
+
+ if(ValStart == ValEnd){
+ LOG_WARN("%s:%d: Empty value", FileName, LineNumber);
+ continue;
+ }
+
+ // NOTE(fusion): Parse KV pair.
+ char Key[256];
+ if(!StringBufCopyN(Key, &Line[KeyStart], (KeyEnd - KeyStart))){
+ LOG_WARN("%s:%d: Exceeded key size limit of %d characters",
+ FileName, LineNumber, (int)(sizeof(Key) - 1));
+ continue;
+ }
+
+ char Val[256];
+ if(!StringBufCopyN(Val, &Line[ValStart], (ValEnd - ValStart))){
+ LOG_WARN("%s:%d: Exceeded value size limit of %d characters",
+ FileName, LineNumber, (int)(sizeof(Val) - 1));
+ continue;
+ }
+
+ if(StringEqCI(Key, "LoginPort")){
+ ParseInteger(&Config->LoginPort, Val);
+ }else if(StringEqCI(Key, "ConnectionTimeout")){
+ ParseDuration(&Config->ConnectionTimeout, Val);
+ }else if(StringEqCI(Key, "MaxConnections")){
+ ParseInteger(&Config->MaxConnections, Val);
+ }else if(StringEqCI(Key, "MaxStatusRecords")){
+ ParseInteger(&Config->MaxStatusRecords, Val);
+ }else if(StringEqCI(Key, "MinStatusInterval")){
+ ParseDuration(&Config->MinStatusInterval, Val);
+ }else if(StringEqCI(Key, "QueryManagerHost")){
+ ParseStringBuf(Config->QueryManagerHost, Val);
+ }else if(StringEqCI(Key, "QueryManagerPort")){
+ ParseInteger(&Config->QueryManagerPort, Val);
+ }else if(StringEqCI(Key, "QueryManagerPassword")){
+ ParseStringBuf(Config->QueryManagerPassword, Val);
+ }else if(StringEqCI(Key, "StatusWorld")){
+ ParseStringBuf(Config->StatusWorld, Val);
+ }else if(StringEqCI(Key, "URL")){
+ ParseStringBuf(Config->Url, Val);
+ }else if(StringEqCI(Key, "Location")){
+ ParseStringBuf(Config->Location, Val);
+ }else if(StringEqCI(Key, "ServerType")){
+ ParseStringBuf(Config->ServerType, Val);
+ }else if(StringEqCI(Key, "ServerVersion")){
+ ParseStringBuf(Config->ServerVersion, Val);
+ }else if(StringEqCI(Key, "ClientVersion")){
+ ParseStringBuf(Config->ClientVersion, Val);
+ }else if(StringEqCI(Key, "MOTD")){
+ ParseMotd(Config->Motd, sizeof(Config->Motd), Val);
+ }else{
+ LOG_WARN("Unknown config \"%s\"", Key);
+ }
+ }
+
+ fclose(File);
+ return true;
+}
+
+static bool SigHandler(int SigNr, sighandler_t Handler){
+ struct sigaction Action = {};
+ Action.sa_handler = Handler;
+ sigfillset(&Action.sa_mask);
+ if(sigaction(SigNr, &Action, NULL) == -1){
+ LOG_ERR("Failed to change handler for signal %d (%s): (%d) %s",
+ SigNr, sigdescr_np(SigNr), errno, strerrordesc_np(errno));
+ return false;
+ }
+ return true;
+}
+
+static void ShutdownHandler(int SigNr){
+ g_ShutdownSignal = SigNr;
+ //WakeConnections?
+}
+
+int main(int argc, const char **argv){
+ (void)argc;
+ (void)argv;
+
+ g_StartTimeMS = GetClockMonotonicMS();
+ g_ShutdownSignal = 0;
+ if(!SigHandler(SIGPIPE, SIG_IGN)
+ || !SigHandler(SIGINT, ShutdownHandler)
+ || !SigHandler(SIGTERM, ShutdownHandler)){
+ return EXIT_FAILURE;
+ }
+
+ // Service Config
+ g_Config.LoginPort = 7171;
+ g_Config.ConnectionTimeout = 5; // seconds
+ g_Config.MaxConnections = 10;
+ g_Config.MaxStatusRecords = 1024;
+ g_Config.MinStatusInterval = 300; // seconds
+ StringBufCopy(g_Config.QueryManagerHost, "127.0.0.1");
+ g_Config.QueryManagerPort = 7173;
+ StringBufCopy(g_Config.QueryManagerPassword, "");
+
+ // Service Info
+ StringBufCopy(g_Config.StatusWorld, "");
+ StringBufCopy(g_Config.Url, "");
+ StringBufCopy(g_Config.Location, "");
+ StringBufCopy(g_Config.ServerType, "");
+ StringBufCopy(g_Config.ServerVersion, "");
+ StringBufCopy(g_Config.ClientVersion, "");
+ StringBufCopy(g_Config.Motd, "");
+
+ LOG("Tibia Login v0.2");
+ if(!ReadConfig("config.cfg", &g_Config)){
+ return EXIT_FAILURE;
+ }
+
+ LOG("Login port: %d", g_Config.LoginPort);
+ LOG("Connection timeout: %ds", g_Config.ConnectionTimeout);
+ LOG("Max connections: %d", g_Config.MaxConnections);
+ LOG("Max status records: %d", g_Config.MaxStatusRecords);
+ LOG("Min status interval: %ds", g_Config.MinStatusInterval);
+ LOG("Query manager host: \"%s\"", g_Config.QueryManagerHost);
+ LOG("Query manager port: %d", g_Config.QueryManagerPort);
+ LOG("Status world: \"%s\"", g_Config.StatusWorld);
+ LOG("URL: \"%s\"", g_Config.Url);
+ LOG("Location: \"%s\"", g_Config.Location);
+ LOG("Server type: \"%s\"", g_Config.ServerType);
+ LOG("Server version: \"%s\"", g_Config.ServerVersion);
+ LOG("Client version: \"%s\"", g_Config.ClientVersion);
+ LOG("MOTD: \"%s\"", g_Config.Motd);
+
+
+ { // NOTE(fusion): Print MOTD preview with escape codes.
+ char MotdPreview[30];
+ if(StringBufEscape(MotdPreview, g_Config.Motd)){
+ LOG("MOTD: \"%s\"", MotdPreview);
+ }else{
+ LOG("MOTD: \"%s...\"", MotdPreview);
+ }
+ }
+
+ atexit(ExitQuery);
+ atexit(ExitConnections);
+ if(!InitQuery() || !InitConnections()){
+ return EXIT_FAILURE;
+ }
+
+ LOG("Running...");
+ while(g_ShutdownSignal == 0){
+ ProcessConnections();
+ }
+
+ LOG("Received signal %d (%s), shutting down...",
+ g_ShutdownSignal, sigdescr_np(g_ShutdownSignal));
+ return EXIT_SUCCESS;
+}
+
diff --git a/src/query.cc b/src/query.cc
index 21d44ec..1722265 100644
--- a/src/query.cc
+++ b/src/query.cc
@@ -1,18 +1,13 @@
-#include "login.hh"
-
-// TODO(fusion): Support windows eventually?
-#if OS_LINUX
-# include <errno.h>
-# include <netdb.h>
-# include <sys/socket.h>
-# include <unistd.h>
-#else
-# error "Operating system not currently supported."
-#endif
+#include "common.hh"
+
+#include <errno.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <unistd.h>
static TQueryManagerConnection *g_QueryManagerConnection;
-bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
+static bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
ASSERT(HostName != NULL && OutAddr != NULL);
addrinfo *Result = NULL;
addrinfo Hints = {};
@@ -39,6 +34,34 @@ bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
return Resolved;
}
+static bool WriteExact(int Fd, const uint8 *Buffer, int Size){
+ int BytesToWrite = Size;
+ const uint8 *WritePtr = Buffer;
+ while(BytesToWrite > 0){
+ int Ret = (int)write(Fd, WritePtr, BytesToWrite);
+ if(Ret == -1){
+ return false;
+ }
+ BytesToWrite -= Ret;
+ WritePtr += Ret;
+ }
+ return true;
+}
+
+static bool ReadExact(int Fd, uint8 *Buffer, int Size){
+ int BytesToRead = Size;
+ uint8 *ReadPtr = Buffer;
+ while(BytesToRead > 0){
+ int Ret = (int)read(Fd, ReadPtr, BytesToRead);
+ if(Ret == -1 || Ret == 0){
+ return false;
+ }
+ BytesToRead -= Ret;
+ ReadPtr += Ret;
+ }
+ return true;
+}
+
bool Connect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
LOG_ERR("Already connected");
@@ -46,8 +69,8 @@ bool Connect(TQueryManagerConnection *Connection){
}
in_addr_t Addr;
- if(!ResolveHostName(g_QueryManagerHost, &Addr)){
- LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_QueryManagerHost);
+ if(!ResolveHostName(g_Config.QueryManagerHost, &Addr)){
+ LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_Config.QueryManagerHost);
return false;
}
@@ -59,7 +82,7 @@ bool Connect(TQueryManagerConnection *Connection){
sockaddr_in QueryManagerAddress = {};
QueryManagerAddress.sin_family = AF_INET;
- QueryManagerAddress.sin_port = htons((uint16)g_QueryManagerPort);
+ QueryManagerAddress.sin_port = htons((uint16)g_Config.QueryManagerPort);
QueryManagerAddress.sin_addr.s_addr = Addr;
if(connect(Connection->Socket, (sockaddr*)&QueryManagerAddress, sizeof(QueryManagerAddress)) == -1){
LOG_ERR("Failed to connect: (%d) %s", errno, strerrordesc_np(errno));
@@ -67,9 +90,10 @@ bool Connect(TQueryManagerConnection *Connection){
return false;
}
- TWriteBuffer WriteBuffer = PrepareQuery(Connection, 0);
+ uint8 LoginBuffer[1024];
+ TWriteBuffer WriteBuffer = PrepareQuery(QUERY_LOGIN, LoginBuffer, sizeof(LoginBuffer));
WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN);
- WriteBuffer.WriteString(g_QueryManagerPassword);
+ WriteBuffer.WriteString(g_Config.QueryManagerPassword);
int Status = ExecuteQuery(Connection, false, &WriteBuffer, NULL);
if(Status != QUERY_STATUS_OK){
LOG_ERR("Failed to login to query manager (%d)", Status);
@@ -91,47 +115,20 @@ bool IsConnected(TQueryManagerConnection *Connection){
return Connection->Socket != -1;
}
-TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType){
- TWriteBuffer WriteBuffer(Connection->Buffer, sizeof(Connection->Buffer));
+TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize){
+ TWriteBuffer WriteBuffer(Buffer, BufferSize);
WriteBuffer.Write16(0); // Request Size
WriteBuffer.Write8((uint8)QueryType);
return WriteBuffer;
}
-static bool WriteExact(int Fd, const uint8 *Buffer, int Size){
- int BytesToWrite = Size;
- const uint8 *WritePtr = Buffer;
- while(BytesToWrite > 0){
- int Ret = (int)write(Fd, WritePtr, BytesToWrite);
- if(Ret == -1){
- return false;
- }
- BytesToWrite -= Ret;
- WritePtr += Ret;
- }
- return true;
-}
-
-static bool ReadExact(int Fd, uint8 *Buffer, int Size){
- int BytesToRead = Size;
- uint8 *ReadPtr = Buffer;
- while(BytesToRead > 0){
- int Ret = (int)read(Fd, ReadPtr, BytesToRead);
- if(Ret == -1 || Ret == 0){
- return false;
- }
- BytesToRead -= Ret;
- ReadPtr += Ret;
- }
- return true;
-}
-
int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer){
- ASSERT(WriteBuffer != NULL
- && WriteBuffer->Buffer == Connection->Buffer
- && WriteBuffer->Size == sizeof(Connection->Buffer)
- && WriteBuffer->Position > 2);
+ // IMPORTANT(fusion): This is similar to the Go version where there is no
+ // connection buffer, and the response is read into the same buffer used
+ // by `WriteBuffer. This helps prevent allocating and moving data around
+ // when reconnecting in the middle of a query.
+ ASSERT(WriteBuffer != NULL && WriteBuffer->Position > 2);
int RequestSize = WriteBuffer->Position - 2;
if(RequestSize < 0xFFFF){
@@ -147,31 +144,15 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
}
const int MaxAttempts = 2;
+ uint8 *Buffer = WriteBuffer->Buffer;
+ int BufferSize = WriteBuffer->Size;
+ int WriteSize = WriteBuffer->Position;
for(int Attempt = 1; true; Attempt += 1){
- int WriteSize = WriteBuffer->Position;
- if(!IsConnected(Connection)){
- if(!AutoReconnect){
- return QUERY_STATUS_FAILED;
- }
-
- // IMPORTANT(fusion): There is no way around this. `Connect` will
- // use the connection buffer to send the login query so we need to
- // save and restore it to not lose any data. One improvement here
- // would be to use a stack or statically allocated buffer, although
- // using malloc/free should have no real impact on performance as
- // we're already doing BLOCKING I/O here.
- uint8 *TempBuffer = (uint8*)malloc(WriteSize);
- memcpy(TempBuffer, Connection->Buffer, WriteSize);
- bool Reconnected = Connect(Connection);
- memcpy(Connection->Buffer, TempBuffer, WriteSize);
- free(TempBuffer);
-
- if(!Reconnected){
- return QUERY_STATUS_FAILED;
- }
+ if(!IsConnected(Connection) && (!AutoReconnect || !Connect(Connection))){
+ return QUERY_STATUS_FAILED;
}
- if(!WriteExact(Connection->Socket, Connection->Buffer, WriteSize)){
+ if(!WriteExact(Connection->Socket, Buffer, WriteSize)){
Disconnect(Connection);
if(Attempt >= MaxAttempts){
LOG_ERR("Failed to write request");
@@ -200,20 +181,20 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
ResponseSize = BufferRead32LE(Help);
}
- if(ResponseSize <= 0 || ResponseSize > (int)sizeof(Connection->Buffer)){
+ if(ResponseSize <= 0 || ResponseSize > BufferSize){
Disconnect(Connection);
LOG_ERR("Invalid response size %d (BufferSize: %d)",
- ResponseSize, (int)sizeof(Connection->Buffer));
+ ResponseSize, BufferSize);
return QUERY_STATUS_FAILED;
}
- if(!ReadExact(Connection->Socket, Connection->Buffer, ResponseSize)){
+ if(!ReadExact(Connection->Socket, Buffer, ResponseSize)){
Disconnect(Connection);
LOG_ERR("Failed to read response");
return QUERY_STATUS_FAILED;
}
- TReadBuffer ReadBuffer(Connection->Buffer, ResponseSize);
+ TReadBuffer ReadBuffer(Buffer, ResponseSize);
int Status = ReadBuffer.Read8();
if(OutReadBuffer){
*OutReadBuffer = ReadBuffer;
@@ -225,7 +206,8 @@ int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
int *PremiumDays){
- TWriteBuffer WriteBuffer = PrepareQuery(g_QueryManagerConnection, 11);
+ uint8 Buffer[KB(4)];
+ TWriteBuffer WriteBuffer = PrepareQuery(QUERY_LOGIN_ACCOUNT, Buffer, sizeof(Buffer));
WriteBuffer.Write32((uint32)AccountID);
WriteBuffer.WriteString(Password);
WriteBuffer.WriteString(IPAddress);
@@ -261,12 +243,45 @@ int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
return Result;
}
+int GetWorld(const char *WorldName, TWorld *OutWorld){
+ ASSERT(WorldName && OutWorld);
+ uint8 Buffer[4096];
+ TReadBuffer ReadBuffer;
+ TWriteBuffer WriteBuffer = PrepareQuery(QUERY_GET_WORLDS, Buffer, sizeof(Buffer));
+ int Status = ExecuteQuery(g_QueryManagerConnection, true, &WriteBuffer, &ReadBuffer);
+ int Result = (Status == QUERY_STATUS_OK ? 0 : -1);
+ memset(OutWorld, 0, sizeof(TWorld));
+ if(Status == QUERY_STATUS_OK){
+ int NumWorlds = (int)ReadBuffer.Read8();
+ for(int i = 0; i < NumWorlds; i += 1){
+ TWorld World = {};
+ ReadBuffer.ReadString(World.Name, sizeof(World.Name));
+ World.Type = (int)ReadBuffer.Read8();
+ World.NumPlayers = (int)ReadBuffer.Read16();
+ World.MaxPlayers = (int)ReadBuffer.Read16();
+ World.OnlinePeak = (int)ReadBuffer.Read16();
+ World.OnlinePeakTimestamp = (int)ReadBuffer.Read32();
+ World.LastStartup = (int)ReadBuffer.Read32();
+ World.LastShutdown = (int)ReadBuffer.Read32();
+
+ if(StringEmpty(WorldName)){
+ // NOTE(fusion): Pick the world with the most players.
+ if(i == 0 || World.NumPlayers > OutWorld->NumPlayers){
+ *OutWorld = World;
+ }
+ }else if(StringEqCI(WorldName, World.Name)){
+ *OutWorld = World;
+ break;
+ }
+ }
+ }else{
+ LOG_ERR("Request failed");
+ }
+ return Result;
+}
+
bool InitQuery(void){
ASSERT(g_QueryManagerConnection == NULL);
-
- LOG("QueryManagerHost: %s", g_QueryManagerHost);
- LOG("QueryManagerPort: %d", g_QueryManagerPort);
-
g_QueryManagerConnection = (TQueryManagerConnection*)calloc(1, sizeof(TQueryManagerConnection));
g_QueryManagerConnection->Socket = -1;
if(!Connect(g_QueryManagerConnection)){
@@ -283,3 +298,4 @@ void ExitQuery(void){
g_QueryManagerConnection = NULL;
}
}
+
diff --git a/src/status.cc b/src/status.cc
new file mode 100644
index 0000000..cb960b8
--- /dev/null
+++ b/src/status.cc
@@ -0,0 +1,222 @@
+#include "common.hh"
+
+// IMPORTANT(fusion): We want connections to use the status string directly for
+// output, but because it could be refreshed while connections are still using
+// it, we need to have them buffered, to allow old versions to remain valid, at
+// least for a couple refreshes. Note that `
+
+static int g_LastStatusRefresh = 0;
+static int g_StatusStringIndex = 0;
+static char g_StatusString[3][KB(2)];
+
+struct XMLBuffer{
+ char *Data;
+ int Size;
+ int Position;
+};
+
+static void XMLNullTerminate(XMLBuffer *Buffer){
+ ASSERT(Buffer->Size > 0);
+ if(Buffer->Position < Buffer->Size){
+ Buffer->Data[Buffer->Position] = 0;
+ }else{
+ Buffer->Data[Buffer->Size - 1] = 0;
+ }
+}
+
+static void XMLAppendChar(XMLBuffer *Buffer, char Ch){
+ if(Buffer->Position < Buffer->Size){
+ Buffer->Data[Buffer->Position] = Ch;
+ }
+ Buffer->Position += 1;
+}
+
+static void XMLAppendNumber(XMLBuffer *Buffer, int64 Num){
+ if(Num == 0){
+ XMLAppendChar(Buffer, '0');
+ return;
+ }
+
+ if(Num < 0){
+ XMLAppendChar(Buffer, '-');
+ Num = -Num;
+ }
+
+ char String[64] = {};
+ int StringLen = 0;
+ while(Num > 0){
+ ASSERT(StringLen < (int)sizeof(String));
+ String[StringLen] = (Num % 10) + '0';
+ Num = (Num / 10);
+ StringLen += 1;
+ }
+
+ for(int i = 0; i < StringLen; i += 1){
+ XMLAppendChar(Buffer, String[(StringLen - 1) - i]);
+ }
+}
+
+static void XMLAppendString(XMLBuffer *Buffer, const char *String){
+ const char *P = String;
+ while(P[0]){
+ XMLAppendChar(Buffer, P[0]);
+ P += 1;
+ }
+}
+
+static void XMLAppendStringEscaped(XMLBuffer *Buffer, const char *String){
+ const char *P = String;
+ while(P[0]){
+ switch(P[0]){
+ case '\t': XMLAppendString(Buffer, "&#9;"); break;
+ case '\n': XMLAppendString(Buffer, "&#10;"); break;
+ case '"': XMLAppendString(Buffer, "&quot;"); break;
+ case '&': XMLAppendString(Buffer, "&amp;"); break;
+ case '\'': XMLAppendString(Buffer, "&apos;"); break;
+ case '<': XMLAppendString(Buffer, "&lt;"); break;
+ case '>': XMLAppendString(Buffer, "&gt;"); break;
+ default: XMLAppendChar(Buffer, P[0]); break;
+ }
+ P += 1;
+ }
+}
+
+static void XMLAppendStringFV(XMLBuffer *Buffer, const char *Format, va_list Args){
+ const char *P = Format;
+ while(P[0]){
+ // IMPORTANT(fusion): This function only implements a small subset of
+ // the printf syntax.
+ if(P[0] == '%'){
+ P += 1;
+
+ // IMPORTANT(fusion): Small integral parameters such as char and short
+ // are promoted to int, similar to how they're promoted in arithmetic
+ // operations. This means there are only really 4 and 8 bytes integers.
+ int Length = 4;
+ if(P[0] == 'h' && P[1] == 'h'){
+ P += 2;
+ }else if(P[0] == 'l' && P[1] == 'l'){
+ Length = 8;
+ P += 2;
+ }else if(P[0] == 'h'){
+ P += 1;
+ }else if(P[0] == 'l'){
+ Length = 8;
+ P += 1;
+ }
+
+ switch(P[0]){
+ case '%':{
+ XMLAppendChar(Buffer, '%');
+ break;
+ }
+
+ case 'c':{
+ int Ch = va_arg(Args, int);
+ XMLAppendChar(Buffer, (char)Ch);
+ break;
+ }
+
+ case 'd':{
+ int64 Num = (Length == 4 ? va_arg(Args, int) : va_arg(Args, int64));
+ XMLAppendNumber(Buffer, Num);
+ break;
+ }
+
+ case 's':{
+ const char *String = va_arg(Args, const char*);
+ XMLAppendStringEscaped(Buffer, String);
+ break;
+ }
+
+ default:{
+ LOG_ERR("Invalid XML format specifier \"%c\"", P[0]);
+ break;
+ }
+ }
+
+ if(P[0]){
+ P += 1;
+ }
+
+ }else{
+ XMLAppendChar(Buffer, P[0]);
+ P += 1;
+ }
+ }
+}
+
+static void XMLAppendStringF(XMLBuffer *Buffer, const char *Format, ...){
+ va_list Args;
+ va_start(Args, Format);
+ XMLAppendStringFV(Buffer, Format, Args);
+ va_end(Args);
+}
+
+const char *GetStatusString(void){
+ int TimeNow = (int)time(NULL);
+ if((TimeNow - g_LastStatusRefresh) >= g_Config.MinStatusInterval){
+ const char *WorldName = "";
+ int Uptime = 0;
+ int NumPlayers = 0;
+ int MaxPlayers = 0;
+ int OnlinePeak = 0;
+
+ TWorld World = {};
+ if(GetWorld(g_Config.StatusWorld, &World) == 0){
+ WorldName = World.Name;
+ if(World.LastStartup != 0 && World.LastStartup > World.LastShutdown){
+ Uptime = (int)time(NULL) - World.LastStartup;
+ }
+ NumPlayers = World.NumPlayers;
+ MaxPlayers = World.MaxPlayers;
+ OnlinePeak = World.OnlinePeak;
+
+ // IMPORTANT(fusion): This could be a common behaviour but, on OTSERVLIST,
+ // the server will show as OFFLINE if the the online peak is less than
+ // the number of online players. This shouldn't usually be a problem since
+ // the online character list and online peak are updated together in the
+ // same CREATE_PLAYERLIST query, but is something to keep in mind.
+ if(OnlinePeak < NumPlayers){
+ OnlinePeak = NumPlayers;
+ }
+ }else{
+ LOG_ERR("Failed to query world data...");
+ }
+
+ // NOTE(fusion): Skip line with MOTD hash.
+ const char *Motd = g_Config.Motd;
+ while(Motd[0]){
+ if(Motd[0] == '\n'){
+ Motd += 1;
+ break;
+ }
+ Motd += 1;
+ }
+
+ g_StatusStringIndex = (g_StatusStringIndex + 1) % NARRAY(g_StatusString);
+ XMLBuffer Buffer = {};
+ Buffer.Data = g_StatusString[g_StatusStringIndex];
+ Buffer.Size = sizeof(g_StatusString[g_StatusStringIndex]);
+ XMLAppendString(&Buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ XMLAppendString(&Buffer, "<tsqp version=\"1.0\">");
+ XMLAppendStringF(&Buffer,
+ "<serverinfo servername=\"%s\" uptime=\"%d\" url=\"%s\""
+ " location=\"%s\" server=\"%s\" version=\"%s\""
+ " client=\"%s\"/>",
+ WorldName, Uptime, g_Config.Url, g_Config.Location,
+ g_Config.ServerType, g_Config.ServerVersion,
+ g_Config.ClientVersion);
+ XMLAppendStringF(&Buffer,
+ "<players online=\"%d\" max=\"%d\" peak=\"%d\"/>",
+ NumPlayers, MaxPlayers, OnlinePeak);
+ XMLAppendStringF(&Buffer, "<motd>%s</motd>", Motd);
+ XMLAppendString(&Buffer, "</tsqp>");
+ XMLNullTerminate(&Buffer);
+
+ g_LastStatusRefresh = TimeNow;
+ }
+
+ return g_StatusString[g_StatusStringIndex];
+}
+