Compare commits

...

10 Commits

Author SHA1 Message Date
fusion32
f1c839fe7c turn invalid or untrusted proxy headers into hard failures 2026-06-20 21:57:26 -03:00
fusion32
27ae278028 support for proxy headers 2026-03-20 17:50:54 -03:00
fusion32
66a3212bc1 remove duplicate MOTD log 2026-01-14 18:13:47 -03:00
fusion32
3283d9343d fix poll timeout
A small mistake that caused the polling timeout to be 1 millisecond
rather than a second, increasing the CPU usage unnecessarily.
2025-12-23 23:06:37 -03:00
fusion32
fedf1e954a a few adjustments 2025-11-18 19:16:46 -03:00
fusion32
62f4d9e442 remove config.cfg 2025-11-18 19:03:58 -03:00
fusion32
2cfc39ed3c remove status string buffering
I originally had planned for it to be a separate service but it
make a lot more sense to have it bundled with the login server and
avoiding spinning up yet another service. So instead of using the
status string directly for output, we're now copying it into the
connection's buffer.
2025-11-18 18:50:51 -03:00
fusion32
6036d4f37a overall improvements + support STATUS requests 2025-11-18 15:35:22 -03:00
fusion32
b101f3809e avoid doing a login query if the account id is empty/zero 2025-10-31 00:53:55 -03:00
fusion32
a8a6a14018 support 7.72 with non-default -DTIBIA772=1 switch 2025-09-19 23:46:26 -03:00
14 changed files with 1971 additions and 1201 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@ -1,13 +1,22 @@
# 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
```
### Extra Features (Advanced)
There are also some extra features that can be enabled by appending the following definitions to the `CFLAGS` variable in your `Makefile`. They're completely optional but can be useful in specific scenarios.
- `-DTIBIA772=1`
- Check for client version 7.72 rather than 7.7. The login protocols for both versions are the same, but because the game protocols are not, there is no reason to allow both to go through.
- `-DALLOW_LOCAL_PROXY=1`
- Allow **local** connections to send a proxy header. Ideally we'd have a list of trusted proxies to prevent clients from spoofing their addresses. In reality, it is almost always simpler to have a **local** relay. This is also the only way to get a multi-route proxy without having to drastically change the way connections are handled.
## Running
Similar to the game server, the login server won't boot up if it's not able to connect to the [Query Manager](https://github.com/fusion32/tibia-querymanager). That said, running it is straighforward, requiring only the RSA private key `tibia.pem` and `config.cfg` files to be in the working directory. It is always recommended that the server is setup as a service. There is a *systemd* configuration file (`tibia-login.service`) in the repository that may be used for that purpose. The process is very similar to the one described in the [Game Server](https://github.com/fusion32/tibia-game) so I won't repeat myself here.
Similar to the game server, the login server won't boot up if it's not able to connect to the [Query Manager](https://github.com/fusion32/tibia-querymanager). That said, running it is straighforward, requiring only the RSA private key `tibia.pem` and `config.cfg` files to be in the working directory. For testing purposes you could simply compile and launch the application from the shell, but if you plan to run the game server on a dedicated machine, it is recommended that it is setup as a service. There is a *systemd* configuration file (`tibia-login.service`) in the repository that may be used for that purpose. The process is very similar to the one described in the [Game Server](https://github.com/fusion32/tibia-game) so I won't repeat myself here.

View File

@ -1,8 +0,0 @@
MOTD = "Welcome to Tibia!"
UpdateRate = 20
LoginPort = 7171
MaxConnections = 10
LoginTimeout = 10s
QueryManagerHost = "127.0.0.1"
QueryManagerPort = 7173
QueryManagerPassword = "a6glaf0c"

19
config.cfg.dist Normal file
View File

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

View File

@ -1,375 +0,0 @@
#include "common.hh"
void LogAdd(const char *Prefix, const char *Format, ...){
char Entry[4096];
va_list ap;
va_start(ap, Format);
vsnprintf(Entry, sizeof(Entry), Format, ap);
va_end(ap);
if(Entry[0] != 0){
struct tm LocalTime = GetLocalTime(time(NULL));
fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s\n",
LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
Prefix, Entry);
fflush(stdout);
}
}
void LogAddVerbose(const char *Prefix, const char *Function,
const char *File, int Line, const char *Format, ...){
char Entry[4096];
va_list ap;
va_start(ap, Format);
vsnprintf(Entry, sizeof(Entry), Format, ap);
va_end(ap);
if(Entry[0] != 0){
(void)File;
(void)Line;
struct tm LocalTime = GetLocalTime(time(NULL));
fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s: %s\n",
LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
Prefix, Function, Entry);
fflush(stdout);
}
}
struct tm GetLocalTime(time_t t){
struct tm result;
#if COMPILER_MSVC
localtime_s(&result, &t);
#else
localtime_r(&t, &result);
#endif
return result;
}
int64 GetClockMonotonicMS(void){
#if OS_WINDOWS
LARGE_INTEGER Counter, Frequency;
QueryPerformanceCounter(&Counter);
QueryPerformanceFrequency(&Frequency);
return (int64)((Counter.QuadPart * 1000) / Frequency.QuadPart);
#else
struct timespec Time;
clock_gettime(CLOCK_MONOTONIC, &Time);
return ((int64)Time.tv_sec * 1000)
+ ((int64)Time.tv_nsec / 1000000);
#endif
}
void SleepMS(int64 DurationMS){
#if OS_WINDOWS
Sleep((DWORD)DurationMS);
#else
struct timespec Duration;
Duration.tv_sec = (time_t)(DurationMS / 1000);
Duration.tv_nsec = (long)((DurationMS % 1000) * 1000000);
nanosleep(&Duration, NULL);
#endif
}
bool StringEq(const char *A, const char *B){
int Index = 0;
while(true){
if(A[Index] != B[Index]){
return false;
}else if(A[Index] == 0){
return true;
}
Index += 1;
}
}
bool StringEqCI(const char *A, const char *B){
int Index = 0;
while(true){
if(tolower(A[Index]) != tolower(B[Index])){
return false;
}else if(A[Index] == 0){
return true;
}
Index += 1;
}
}
bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){
ASSERT(DestCapacity > 0);
bool Result = (SrcLength < DestCapacity);
if(Result && SrcLength > 0){
memcpy(Dest, Src, SrcLength);
Dest[SrcLength] = 0;
}else{
Dest[0] = 0;
}
return Result;
}
bool StringCopy(char *Dest, int DestCapacity, const char *Src){
// IMPORTANT(fusion): `sqlite3_column_text` may return NULL if the column is
// also NULL so we have an incentive to properly handle the case where `Src`
// is NULL.
int SrcLength = (Src != NULL ? (int)strlen(Src) : 0);
return StringCopyN(Dest, DestCapacity, Src, SrcLength);
}
bool EscapeString(char *Dest, int DestCapacity, const char *Src){
int WritePos = 0;
for(int ReadPos = 0; Src[ReadPos] != 0 && WritePos < DestCapacity; ReadPos += 1){
int EscapeCh = -1;
switch(Src[ReadPos]){
case '\a': EscapeCh = 'a'; break;
case '\b': EscapeCh = 'b'; break;
case '\t': EscapeCh = 't'; break;
case '\n': EscapeCh = 'n'; break;
case '\v': EscapeCh = 'v'; break;
case '\f': EscapeCh = 'f'; break;
case '\r': EscapeCh = 'r'; break;
case '\"': EscapeCh = '\"'; break;
case '\'': EscapeCh = '\''; break;
case '\\': EscapeCh = '\\'; break;
}
if(EscapeCh != -1){
if((WritePos + 1) <= DestCapacity){
Dest[WritePos] = '\\';
WritePos += 1;
}
if((WritePos + 1) <= DestCapacity){
Dest[WritePos] = EscapeCh;
WritePos += 1;
}
}else{
if((WritePos + 1) <= DestCapacity){
Dest[WritePos] = Src[ReadPos];
WritePos += 1;
}
}
}
if(WritePos < DestCapacity){
Dest[WritePos] = 0;
return true;
}else{
Dest[DestCapacity - 1] = 0;
return false;
}
}
uint32 HashString(const char *String){
// FNV1a 32-bits
uint32 Hash = 0x811C9DC5U;
for(int i = 0; String[i] != 0; i += 1){
Hash ^= (uint32)String[i];
Hash *= 0x01000193U;
}
return Hash;
}
bool ParseIPAddress(int *Dest, const char *String){
ASSERT(Dest != NULL && String != NULL);
int Addr[4];
if(sscanf(String, "%d.%d.%d.%d", &Addr[0], &Addr[1], &Addr[2], &Addr[3]) != 4){
LOG_ERR("Invalid IP Address format \"%s\"", String);
return false;
}
if(Addr[0] < 0 || Addr[0] > 0xFF
|| Addr[1] < 0 || Addr[1] > 0xFF
|| Addr[2] < 0 || Addr[2] > 0xFF
|| Addr[3] < 0 || Addr[3] > 0xFF){
LOG_ERR("Invalid IP Address \"%s\"", String);
return false;
}
*Dest = (Addr[0] << 24)
| (Addr[1] << 16)
| (Addr[2] << 8)
| (Addr[3] << 0);
return true;
}
bool ParseBoolean(bool *Dest, const char *String){
ASSERT(Dest != NULL && String != NULL);
*Dest = StringEqCI(String, "true");
return *Dest || StringEqCI(String, "false");
}
bool ParseInteger(int *Dest, const char *String){
ASSERT(Dest != NULL && String != NULL);
const char *StringEnd;
*Dest = (int)strtol(String, (char**)&StringEnd, 0);
return StringEnd > String;
}
bool ParseDuration(int *Dest, const char *String){
ASSERT(Dest != NULL && String != NULL);
const char *Suffix;
*Dest = (int)strtol(String, (char**)&Suffix, 0);
if(Suffix == String){
return false;
}
while(Suffix[0] != 0 && isspace(Suffix[0])){
Suffix += 1;
}
if(Suffix[0] == 'S' || Suffix[0] == 's'){
*Dest *= (1000);
}else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
*Dest *= (60 * 1000);
}else if(Suffix[0] == 'H' || Suffix[0] == 'h'){
*Dest *= (60 * 60 * 1000);
}
return true;
}
bool ParseSize(int *Dest, const char *String){
ASSERT(Dest != NULL && String != NULL);
const char *Suffix;
*Dest = (int)strtol(String, (char**)&Suffix, 0);
if(Suffix == String){
return false;
}
while(Suffix[0] != 0 && isspace(Suffix[0])){
Suffix += 1;
}
if(Suffix[0] == 'K' || Suffix[0] == 'k'){
*Dest *= (1024);
}else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
*Dest *= (1024 * 1024);
}
return true;
}
bool ParseString(char *Dest, int DestCapacity, const char *String){
ASSERT(Dest != NULL && DestCapacity > 0 && String != NULL);
int StringStart = 0;
int StringEnd = (int)strlen(String);
if(StringEnd >= 2){
if((String[0] == '"' && String[StringEnd - 1] == '"')
|| (String[0] == '\'' && String[StringEnd - 1] == '\'')
|| (String[0] == '`' && String[StringEnd - 1] == '`')){
StringStart += 1;
StringEnd -= 1;
}
}
return StringCopyN(Dest, DestCapacity,
&String[StringStart], (StringEnd - StringStart));
}
bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback){
ASSERT(FileName != NULL && KVCallback != NULL);
FILE *File = fopen(FileName, "rb");
if(File == NULL){
LOG_ERR("Failed to open config file \"%s\"", FileName);
return false;
}
bool EndOfFile = false;
for(int LineNumber = 1; !EndOfFile; LineNumber += 1){
char Line[1024];
int MaxLineSize = (int)sizeof(Line);
int LineSize = 0;
int KeyStart = -1;
int EqualPos = -1;
while(true){
int ch = fgetc(File);
if(ch == EOF || ch == '\n'){
if(ch == EOF){
EndOfFile = true;
}
break;
}
if(LineSize < MaxLineSize){
Line[LineSize] = (char)ch;
}
if(KeyStart == -1 && !isspace(ch)){
KeyStart = LineSize;
}
if(EqualPos == -1 && ch == '='){
EqualPos = LineSize;
}
LineSize += 1;
}
// NOTE(fusion): Check line size limit.
if(LineSize > MaxLineSize){
LOG_WARN("%s:%d: Exceeded line size limit of %d characters",
FileName, LineNumber, MaxLineSize);
continue;
}
// NOTE(fusion): Check empty line or comment.
if(KeyStart == -1 || Line[KeyStart] == '#'){
continue;
}
// NOTE(fusion): Check assignment.
if(EqualPos == -1){
LOG_WARN("%s:%d: No assignment found on non empty line",
FileName, LineNumber);
continue;
}
// NOTE(fusion): Check empty key.
int KeyEnd = EqualPos;
while(KeyEnd > KeyStart && isspace(Line[KeyEnd - 1])){
KeyEnd -= 1;
}
if(KeyStart == KeyEnd){
LOG_WARN("%s:%d: Empty key", FileName, LineNumber);
continue;
}
// NOTE(fusion): Check empty value.
int ValStart = EqualPos + 1;
int ValEnd = LineSize;
while(ValStart < ValEnd && isspace(Line[ValStart])){
ValStart += 1;
}
while(ValEnd > ValStart && isspace(Line[ValEnd - 1])){
ValEnd -= 1;
}
if(ValStart == ValEnd){
LOG_WARN("%s:%d: Empty value", FileName, LineNumber);
continue;
}
// NOTE(fusion): Parse KV pair.
char Key[256];
if(!StringCopyN(Key, (int)sizeof(Key), &Line[KeyStart], (KeyEnd - KeyStart))){
LOG_WARN("%s:%d: Exceeded key size limit of %d characters",
FileName, LineNumber, (int)(sizeof(Key) - 1));
continue;
}
char Val[256];
if(!StringCopyN(Val, (int)sizeof(Val), &Line[ValStart], (ValEnd - ValStart))){
LOG_WARN("%s:%d: Exceeded value size limit of %d characters",
FileName, LineNumber, (int)(sizeof(Val) - 1));
continue;
}
KVCallback(Key, Val);
}
fclose(File);
return true;
}

View File

@ -1,31 +1,25 @@
#ifndef TIBIA_LOGIN_COMMON_HH_
#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,521 @@ 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);
typedef void ConfigKVCallback(const char *Key, const char *Val);
bool ReadConfig(const char *FileName, ConfigKVCallback *KVCallback);
// 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)
#endif //TIBIA_LOGIN_COMMON_HH_
// Buffer Utility
//==============================================================================
inline uint8 BufferRead8(const uint8 *Buffer){
return Buffer[0];
}
inline uint16 BufferRead16LE(const uint8 *Buffer){
return (uint16)Buffer[0]
| ((uint16)Buffer[1] << 8);
}
inline uint16 BufferRead16BE(const uint8 *Buffer){
return ((uint16)Buffer[0] << 8)
| (uint16)Buffer[1];
}
inline uint32 BufferRead32LE(const uint8 *Buffer){
return (uint32)Buffer[0]
| ((uint32)Buffer[1] << 8)
| ((uint32)Buffer[2] << 16)
| ((uint32)Buffer[3] << 24);
}
inline uint32 BufferRead32BE(const uint8 *Buffer){
return ((uint32)Buffer[0] << 24)
| ((uint32)Buffer[1] << 16)
| ((uint32)Buffer[2] << 8)
| (uint32)Buffer[3];
}
inline uint64 BufferRead64LE(const uint8 *Buffer){
return (uint64)Buffer[0]
| ((uint64)Buffer[1] << 8)
| ((uint64)Buffer[2] << 16)
| ((uint64)Buffer[3] << 24)
| ((uint64)Buffer[4] << 32)
| ((uint64)Buffer[5] << 40)
| ((uint64)Buffer[6] << 48)
| ((uint64)Buffer[7] << 56);
}
inline uint64 BufferRead64BE(const uint8 *Buffer){
return ((uint64)Buffer[0] << 56)
| ((uint64)Buffer[1] << 48)
| ((uint64)Buffer[2] << 40)
| ((uint64)Buffer[3] << 32)
| ((uint64)Buffer[4] << 24)
| ((uint64)Buffer[5] << 16)
| ((uint64)Buffer[6] << 8)
| (uint64)Buffer[7];
}
inline void BufferWrite8(uint8 *Buffer, uint8 Value){
Buffer[0] = Value;
}
inline void BufferWrite16LE(uint8 *Buffer, uint16 Value){
Buffer[0] = (uint8)(Value >> 0);
Buffer[1] = (uint8)(Value >> 8);
}
inline void BufferWrite16BE(uint8 *Buffer, uint16 Value){
Buffer[0] = (uint8)(Value >> 8);
Buffer[1] = (uint8)(Value >> 0);
}
inline void BufferWrite32LE(uint8 *Buffer, uint32 Value){
Buffer[0] = (uint8)(Value >> 0);
Buffer[1] = (uint8)(Value >> 8);
Buffer[2] = (uint8)(Value >> 16);
Buffer[3] = (uint8)(Value >> 24);
}
inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
Buffer[0] = (uint8)(Value >> 24);
Buffer[1] = (uint8)(Value >> 16);
Buffer[2] = (uint8)(Value >> 8);
Buffer[3] = (uint8)(Value >> 0);
}
inline void BufferWrite64LE(uint8 *Buffer, uint64 Value){
Buffer[0] = (uint8)(Value >> 0);
Buffer[1] = (uint8)(Value >> 8);
Buffer[2] = (uint8)(Value >> 16);
Buffer[3] = (uint8)(Value >> 24);
Buffer[4] = (uint8)(Value >> 32);
Buffer[5] = (uint8)(Value >> 40);
Buffer[6] = (uint8)(Value >> 48);
Buffer[7] = (uint8)(Value >> 56);
}
inline void BufferWrite64BE(uint8 *Buffer, uint64 Value){
Buffer[0] = (uint8)(Value >> 56);
Buffer[1] = (uint8)(Value >> 48);
Buffer[2] = (uint8)(Value >> 40);
Buffer[3] = (uint8)(Value >> 32);
Buffer[4] = (uint8)(Value >> 24);
Buffer[5] = (uint8)(Value >> 16);
Buffer[6] = (uint8)(Value >> 8);
Buffer[7] = (uint8)(Value >> 0);
}
struct TReadBuffer{
uint8 *Buffer;
int Size;
int Position;
TReadBuffer(void) : TReadBuffer(NULL, 0) {}
TReadBuffer(uint8 *Buffer, int Size)
: Buffer(Buffer), Size(Size), Position(0) {}
bool CanRead(int Bytes){
return (this->Position + Bytes) <= this->Size;
}
bool Overflowed(void){
return this->Position > this->Size;
}
bool ReadFlag(void){
return this->Read8() != 0x00;
}
uint8 Read8(void){
uint8 Result = 0;
if(this->CanRead(1)){
Result = BufferRead8(this->Buffer + this->Position);
}
this->Position += 1;
return Result;
}
uint16 Read16(void){
uint16 Result = 0;
if(this->CanRead(2)){
Result = BufferRead16LE(this->Buffer + this->Position);
}
this->Position += 2;
return Result;
}
uint16 Read16BE(void){
uint16 Result = 0;
if(this->CanRead(2)){
Result = BufferRead16BE(this->Buffer + this->Position);
}
this->Position += 2;
return Result;
}
uint32 Read32(void){
uint32 Result = 0;
if(this->CanRead(4)){
Result = BufferRead32LE(this->Buffer + this->Position);
}
this->Position += 4;
return Result;
}
uint32 Read32BE(void){
uint32 Result = 0;
if(this->CanRead(4)){
Result = BufferRead32BE(this->Buffer + this->Position);
}
this->Position += 4;
return Result;
}
#if CLIENT_ENCODING_UTF8
void ReadString(char *Dest, int DestCapacity){
int Length = (int)this->Read16();
if(Length == 0xFFFF){
Length = (int)this->Read32();
}
if(Dest != NULL && DestCapacity > 0){
int Written = 0;
if(this->CanRead(Length) && Length < DestCapacity){
memcpy(Dest, this->Buffer + this->Position, Length);
Written = Length;
}
memset((Dest + Written), 0, (DestCapacity - Written));
}
this->Position += Length;
}
#else
void ReadString(char *Dest, int DestCapacity){
int Length = (int)this->Read16();
if(Length == 0xFFFF){
Length = (int)this->Read32();
}
if(Dest != NULL && DestCapacity > 0){
int Written = 0;
if(this->CanRead(Length)){
const char *Src = (const char*)(this->Buffer + this->Position);
Written = Latin1ToUTF8(Dest, DestCapacity, Src, Length);
if(Written >= DestCapacity){
Written = 0;
}
}
memset((Dest + Written), 0, (DestCapacity - Written));
}
this->Position += Length;
}
#endif
void ReadBytes(uint8 *Buffer, int Count){
if(this->CanRead(Count)){
memcpy(Buffer, this->Buffer + this->Position, Count);
}
this->Position += Count;
}
};
struct TWriteBuffer{
uint8 *Buffer;
int Size;
int Position;
TWriteBuffer(void) : TWriteBuffer(NULL, 0) {}
TWriteBuffer(uint8 *Buffer, int Size)
: Buffer(Buffer), Size(Size), Position(0) {}
bool CanWrite(int Bytes){
return (this->Position + Bytes) <= this->Size;
}
bool Overflowed(void){
return this->Position > this->Size;
}
void WriteFlag(bool Value){
this->Write8(Value ? 0x01 : 0x00);
}
void Write8(uint8 Value){
if(this->CanWrite(1)){
BufferWrite8(this->Buffer + this->Position, Value);
}
this->Position += 1;
}
void Write16(uint16 Value){
if(this->CanWrite(2)){
BufferWrite16LE(this->Buffer + this->Position, Value);
}
this->Position += 2;
}
void Write16BE(uint16 Value){
if(this->CanWrite(2)){
BufferWrite16BE(this->Buffer + this->Position, Value);
}
this->Position += 2;
}
void Write32(uint32 Value){
if(this->CanWrite(4)){
BufferWrite32LE(this->Buffer + this->Position, Value);
}
this->Position += 4;
}
void Write32BE(uint32 Value){
if(this->CanWrite(4)){
BufferWrite32BE(this->Buffer + this->Position, Value);
}
this->Position += 4;
}
#if CLIENT_ENCODING_UTF8
void WriteString(const char *String){
int StringLength = 0;
if(String != NULL){
StringLength = (int)strlen(String);
}
if(StringLength < 0xFFFF){
this->Write16((uint16)StringLength);
}else{
this->Write16(0xFFFF);
this->Write32((uint32)StringLength);
}
if(StringLength > 0 && this->CanWrite(StringLength)){
memcpy(this->Buffer + this->Position, String, StringLength);
}
this->Position += StringLength;
}
#else
void WriteString(const char *String){
int StringLength = 0;
int OutputLength = 0;
if(String != NULL){
StringLength = (int)strlen(String);
OutputLength = UTF8ToLatin1(NULL, 0, String, (int)strlen(String));
}
if(OutputLength < 0xFFFF){
this->Write16((uint16)OutputLength);
}else{
this->Write16(0xFFFF);
this->Write32((uint32)OutputLength);
}
if(OutputLength > 0 && this->CanWrite(OutputLength)){
int Written = UTF8ToLatin1((char*)(this->Buffer + this->Position),
(this->Size - this->Position), String, StringLength);
ASSERT(Written == OutputLength);
}
this->Position += OutputLength;
}
#endif
void Rewrite16(int Position, uint16 Value){
if((Position + 2) <= this->Position && !this->Overflowed()){
BufferWrite16LE(this->Buffer + Position, Value);
}
}
void Insert32(int Position, uint32 Value){
if(Position <= this->Position){
if(this->CanWrite(4)){
memmove(this->Buffer + Position + 4,
this->Buffer + Position,
this->Position - Position);
BufferWrite32LE(this->Buffer + Position, Value);
}
this->Position += 4;
}
}
};
// crypto.cc
//==============================================================================
typedef void RSAKey;
RSAKey *RSALoadPEM(const char *FileName);
void RSAFree(RSAKey *Key);
bool RSADecrypt(RSAKey *Key, uint8 *Data, int Size);
void XTEAEncrypt(const uint32 *Key, uint8 *Data, int Size);
void XTEADecrypt(const uint32 *Key, uint8 *Data, int Size);
// query.cc
//==============================================================================
enum {
APPLICATION_TYPE_GAME = 1,
APPLICATION_TYPE_LOGIN = 2,
APPLICATION_TYPE_WEB = 3,
};
enum {
QUERY_STATUS_OK = 0,
QUERY_STATUS_ERROR = 1,
QUERY_STATUS_FAILED = 3,
};
enum {
QUERY_LOGIN = 0,
QUERY_LOGIN_ACCOUNT = 11,
QUERY_GET_WORLDS = 150,
};
struct TQueryManagerConnection{
int Socket;
};
struct TCharacterLoginData{
char Name[30];
char WorldName[30];
int WorldAddress;
int WorldPort;
};
struct TWorld {
char Name[30];
int Type;
int NumPlayers;
int MaxPlayers;
int OnlinePeak;
int OnlinePeakTimestamp;
int LastStartup;
int LastShutdown;
};
bool Connect(TQueryManagerConnection *Connection);
void Disconnect(TQueryManagerConnection *Connection);
bool IsConnected(TQueryManagerConnection *Connection);
TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize);
int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer);
int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
int *PremiumDays);
int GetWorld(const char *WorldName, TWorld *OutWorld);
bool InitQuery(void);
void ExitQuery(void);
// status.cc
//==============================================================================
const char *GetStatusString(void);
// connections.cc
//==============================================================================
enum ConnectionState {
CONNECTION_FREE = 0,
CONNECTION_READING = 1,
CONNECTION_PROCESSING = 2,
CONNECTION_WRITING = 3,
};
struct TConnection {
ConnectionState State;
int Socket;
int IPAddress;
int RemotePort;
int StartTime;
int RWSize;
int RWPosition;
#if ALLOW_LOCAL_PROXY
bool ReadingProxyHeader;
#endif
uint32 RandomSeed;
uint32 XTEA[4];
char RemoteAddress[32];
uint8 Buffer[KB(2)];
};
struct TStatusRecord {
int IPAddress;
int Timestamp;
};
void ProcessConnections(void);
bool InitConnections(void);
void ExitConnections(void);
void ProcessLoginRequest(TConnection *Connection);
void ProcessStatusRequest(TConnection *Connection);
#endif //TIBIA_COMMON_H_

View File

@ -1,28 +1,30 @@
#include "login.hh"
#include "common.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>
#if TIBIA772
static const int TERMINALVERSION[] = {772, 772, 772};
#else
# error "Operating system not currently supported."
static const int TERMINALVERSION[] = {770, 770, 770};
#endif
static const int TERMINALVERSION[] = {770, 770, 770};
static RSAKey *g_PrivateKey = NULL;
static RSAKey *g_PrivateKey;
static int g_Listener = -1;
static TConnection *g_Connections = NULL;
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));
@ -68,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);
@ -105,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;
@ -124,22 +126,26 @@ 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->RemotePort = (int)Port;
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),
Connection->RemotePort);
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);
@ -148,12 +154,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);
@ -161,13 +167,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){
@ -184,38 +185,138 @@ void CheckConnectionInput(TConnection *Connection, int Events){
Connection->RWPosition += BytesRead;
if(Connection->RWPosition >= ReadSize){
ASSERT(Connection->RWPosition == ReadSize);
if(Connection->RWSize == 0){
int PayloadSize = BufferRead16LE(Connection->Buffer);
if(PayloadSize <= 0 || PayloadSize > NARRAY(Connection->Buffer)){
#if ALLOW_LOCAL_PROXY
if(Connection->Buffer[0] == 0x0D && Connection->Buffer[1] == 0x0A){
ASSERT(sizeof(Connection->Buffer) >= 28);
Connection->RWSize = 28;
Connection->ReadingProxyHeader = true;
}else if(Connection->Buffer[0] == 0xD9 && Connection->Buffer[1] == 0xFF){
ASSERT(sizeof(Connection->Buffer) >= 6);
Connection->RWSize = 6;
Connection->ReadingProxyHeader = true;
}else
#endif
{
int PayloadSize = (int)BufferRead16LE(Connection->Buffer);
if(PayloadSize <= 0 || PayloadSize > (int)sizeof(Connection->Buffer)){
CloseConnection(Connection);
break;
}
Connection->RWSize = PayloadSize;
Connection->RWPosition = 0;
}else{
Connection->State = CONNECTION_LOGIN;
}
}
#if ALLOW_LOCAL_PROXY
else if(Connection->ReadingProxyHeader){
uint8 *SourceAddr = NULL;
if(Connection->Buffer[0] == 0x0D && Connection->Buffer[1] == 0x0A){
ASSERT(ReadSize == 28);
// NOTE(fusion): HAProxy-V2 header.
static const uint8_t HAProxyV2Signature[] = {
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51,
0x55, 0x49, 0x54, 0x0A,
};
if(memcmp(Connection->Buffer, HAProxyV2Signature, 12) == 0){
SourceAddr = Connection->Buffer + 16;
}
}else if(Connection->Buffer[0] == 0xD9 && Connection->Buffer[1] == 0xFF){
// NOTE(fusion): Our custom proxy.
ASSERT(ReadSize == 6);
SourceAddr = Connection->Buffer + 2;
}
if(SourceAddr == NULL || Connection->IPAddress != INADDR_LOOPBACK){
ASSERT(ReadSize >= 2);
if(SourceAddr == NULL){
LOG_ERR("Invalid proxy header (%02X, %02X) from host %s",
(int)Connection->Buffer[0], (int)Connection->Buffer[1],
Connection->RemoteAddress);
}
if(Connection->IPAddress != INADDR_LOOPBACK){
LOG_ERR("Received proxy header (%02X, %02X) from unknown host %s",
(int)Connection->Buffer[0], (int)Connection->Buffer[1],
Connection->RemoteAddress);
}
CloseConnection(Connection);
break;
}
LOG("Connection proxy header %s -> %d.%d.%d.%d:%d",
Connection->RemoteAddress,
(int)SourceAddr[0], (int)SourceAddr[1],
(int)SourceAddr[2], (int)SourceAddr[3],
Connection->RemotePort);
Connection->IPAddress = BufferRead32BE(SourceAddr);
StringBufFormat(Connection->RemoteAddress,
"%d.%d.%d.%d:%d",
(int)SourceAddr[0], (int)SourceAddr[1],
(int)SourceAddr[2], (int)SourceAddr[3],
Connection->RemotePort);
Connection->RWSize = 0;
Connection->RWPosition = 0;
Connection->ReadingProxyHeader = false;
}
#endif
else{
Connection->State = CONNECTION_PROCESSING;
break;
}
}
}
if(Connection->State == CONNECTION_LOGIN){
ProcessLoginRequest(Connection);
}
}
void CheckConnectionOutput(TConnection *Connection, int Events){
if((Events & POLLOUT) == 0 || Connection->Socket == -1){
static void CheckConnectionRequest(TConnection *Connection){
if(Connection->Socket == -1){
return;
}
if(Connection->State != CONNECTION_WRITE){
if(Connection->State != CONNECTION_PROCESSING){
return;
}
// PARANOID(fusion): A non-empty payload is guaranteed, due to how we parse
// input in `CheckConnectionInput` just above.
ASSERT(Connection->RWSize > 0);
int Command = Connection->Buffer[0];
if(Command == 1){
ProcessLoginRequest(Connection);
}else if(Command == 255){
ProcessStatusRequest(Connection);
}else{
LOG_ERR("Invalid command %d from %s (expected 1 or 255)",
Command, Connection->RemoteAddress);
CloseConnection(Connection);
}
}
static void CheckConnectionOutput(TConnection *Connection, int Events){
// NOTE(fusion): We're only polling `POLLOUT` when the connection is WRITING,
// but we want to allow requests to complete in a single cycle, so we always
// check for output if the connection is WRITING.
(void)Events;
if(Connection->Socket == -1){
return;
}
if(Connection->State != CONNECTION_WRITING){
return;
}
while(true){
int BytesWritten = write(Connection->Socket,
int BytesWritten = (int)write(Connection->Socket,
(Connection->Buffer + Connection->RWPosition),
(Connection->RWSize - Connection->RWPosition));
if(BytesWritten == -1){
@ -225,26 +326,29 @@ void CheckConnectionOutput(TConnection *Connection, int Events){
break;
}
// NOTE(fusion): Both login and status protocols will serve a single
// request so we always close the connection after it is served.
Connection->RWPosition += BytesWritten;
if(Connection->RWPosition >= Connection->RWSize){
ASSERT(Connection->RWPosition == Connection->RWSize);
CloseConnection(Connection);
break;
}
}
}
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);
}
}
@ -254,8 +358,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;
@ -265,56 +373,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){
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));
if(g_Listener != -1){
Fds[NumFds].fd = g_Listener;
Fds[NumFds].events = POLLIN;
Fds[NumFds].revents = 0;
ConnectionIndices[NumFds] = -1;
NumFds += 1;
}
for(int i = 0; i < g_Config.MaxConnections; i += 1){
if(g_Connections[i].State == CONNECTION_FREE){
continue;
}
ConnectionIndices[NumConnections] = i;
ConnectionFds[NumConnections].fd = g_Connections[i].Socket;
ConnectionFds[NumConnections].events = POLLIN | POLLOUT;
ConnectionFds[NumConnections].revents = 0;
NumConnections += 1;
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;
}
if(NumConnections <= 0){
return;
}
// 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, 1000);
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;
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){
@ -322,17 +452,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;
}
@ -355,13 +491,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);
@ -373,9 +514,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;
@ -405,25 +546,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
@ -439,54 +580,64 @@ 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);
return;
}
if(!RSADecrypt(g_PrivateKey, AsymmetricData, sizeof(AsymmetricData))){
// IMPORTANT(fusion): Without a checksum, there is no way of validating
// the asymmetric data. The best we can do is to verify that the first
// plaintext byte is ZERO, but that alone isn't enough.
if(!RSADecrypt(g_PrivateKey, AsymmetricData, sizeof(AsymmetricData)) || AsymmetricData[0] != 0){
LOG_ERR("Failed to decrypt asymmetric data from %s",
Connection->RemoteAddress);
CloseConnection(Connection);
return;
}
TReadBuffer Buffer(AsymmetricData, sizeof(AsymmetricData));
Buffer.Read8(); // always zero
Connection->XTEA[0] = Buffer.Read32();
Connection->XTEA[1] = Buffer.Read32();
Connection->XTEA[2] = Buffer.Read32();
Connection->XTEA[3] = Buffer.Read32();
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 = ReadBuffer.Read32();
ReadBuffer.ReadString(Password, sizeof(Password));
if(ReadBuffer.Overflowed()){
LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress);
CloseConnection(Connection);
return;
}
if(AccountID <= 0){
SendLoginError(Connection, "You must enter an account number.");
return;
}
if(TerminalType < 0 || TerminalType >= NARRAY(TERMINALVERSION)
|| TERMINALVERSION[TerminalType] != TerminalVersion){
@ -497,19 +648,17 @@ void ProcessLoginRequest(TConnection *Connection){
return;
}
char Password[30];
int AccountID = Buffer.Read32();
Buffer.ReadString(Password, sizeof(Password));
if(Buffer.Overflowed()){
LOG_ERR("Malformed asymmetric data from %s", Connection->RemoteAddress);
CloseConnection(Connection);
return;
}
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:{
@ -552,3 +701,84 @@ void ProcessLoginRequest(TConnection *Connection){
}
}
}
// Status Request
//==============================================================================
static bool AllowStatusRequest(int IPAddress){
TStatusRecord *Record = NULL;
int LeastRecentlyUsedIndex = 0;
int LeastRecentlyUsedTime = g_StatusRecords[0].Timestamp;
int TimeNow = GetMonotonicUptime();
for(int i = 0; i < g_MaxStatusRecords; i += 1){
if(g_StatusRecords[i].Timestamp < LeastRecentlyUsedTime){
LeastRecentlyUsedIndex = i;
LeastRecentlyUsedTime = g_StatusRecords[i].Timestamp;
}
if(g_StatusRecords[i].IPAddress == IPAddress){
Record = &g_StatusRecords[i];
break;
}
}
bool Result = false;
if(Record == NULL){
Record = &g_StatusRecords[LeastRecentlyUsedIndex];
Record->IPAddress = IPAddress;
Record->Timestamp = TimeNow;
Result = true;
}else if((TimeNow - Record->Timestamp) >= g_Config.MinStatusInterval){
Record->Timestamp = TimeNow;
Result = true;
}
return Result;
}
static void SendStatusString(TConnection *Connection, const char *StatusString){
if(Connection->State != CONNECTION_PROCESSING){
LOG_ERR("Connection %s is not PROCESSING (State: %d)",
Connection->RemoteAddress, Connection->State);
CloseConnection(Connection);
return;
}
int Length = (int)strlen(StatusString);
if(Length > (int)sizeof(Connection->Buffer)){
Length = (int)sizeof(Connection->Buffer);
}
Connection->RWSize = Length;
Connection->RWPosition = 0;
memcpy(Connection->Buffer, StatusString, Length);
Connection->State = CONNECTION_WRITING;
}
void ProcessStatusRequest(TConnection *Connection){
if(!AllowStatusRequest(Connection->IPAddress)){
LOG_ERR("Too many status requests from %s", Connection->RemoteAddress);
CloseConnection(Connection);
return;
}
TReadBuffer ReadBuffer(Connection->Buffer, Connection->RWSize);
ReadBuffer.Read8(); // always 255 for a status request
int Format = (int)ReadBuffer.Read8();
if(Format == 255){ // XML
char Request[5] = {};
ReadBuffer.ReadBytes((uint8*)Request, 4);
if(StringEqCI(Request, "info")){
const char *StatusString = GetStatusString();
SendStatusString(Connection, StatusString);
}else{
LOG_WARN("Invalid status request \"%s\" from %s",
Request, Connection->RemoteAddress);
CloseConnection(Connection);
}
}else{
LOG_WARN("Invalid status format %d from %s",
Format, Connection->RemoteAddress);
CloseConnection(Connection);
}
}

View File

@ -1,4 +1,4 @@
#include "login.hh"
#include "common.hh"
#include <openssl/err.h>
#include <openssl/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'){

View File

@ -1,130 +0,0 @@
#include "login.hh"
// TODO(fusion): Support windows eventually?
#if OS_LINUX
# include <errno.h>
# include <signal.h>
#else
# error "Operating system not currently supported."
#endif
// Shutdown Signal
int g_ShutdownSignal = 0;
// Time
int g_MonotonicTimeMS = 0;
// Config
char g_Motd[256] = "";
int g_UpdateRate = 20;
int g_LoginPort = 7171;
int g_MaxConnections = 10;
int g_LoginTimeout = 10000;
char g_QueryManagerHost[100] = "127.0.0.1";
int g_QueryManagerPort = 7174;
char g_QueryManagerPassword[30] = "";
static void ParseMotd(char *Dest, int DestCapacity, const char *String){
char *Motd = (char*)alloca(DestCapacity);
ParseString(Motd, DestCapacity, String);
if(Motd[0] != 0){
snprintf(Dest, DestCapacity, "%u\n%s", HashString(Motd), Motd);
}
}
static void LoginKVCallback(const char *Key, const char *Val){
if(StringEqCI(Key, "LoginPort")){
ParseInteger(&g_LoginPort, Val);
}else if(StringEqCI(Key, "MaxConnections")){
ParseInteger(&g_MaxConnections, Val);
}else if(StringEqCI(Key, "LoginTimeout")){
ParseDuration(&g_LoginTimeout, Val);
}else if(StringEqCI(Key, "UpdateRate")){
ParseInteger(&g_UpdateRate, Val);
}else if(StringEqCI(Key, "QueryManagerHost")){
ParseString(g_QueryManagerHost,
sizeof(g_QueryManagerHost), Val);
}else if(StringEqCI(Key, "QueryManagerPort")){
ParseInteger(&g_QueryManagerPort, Val);
}else if(StringEqCI(Key, "QueryManagerPassword")){
ParseString(g_QueryManagerPassword,
sizeof(g_QueryManagerPassword), Val);
}else if(StringEqCI(Key, "MOTD")){
ParseMotd(g_Motd, sizeof(g_Motd), Val);
}else{
LOG_WARN("Unknown config \"%s\"", Key);
}
}
static bool SigHandler(int SigNr, sighandler_t Handler){
struct sigaction Action = {};
Action.sa_handler = Handler;
sigfillset(&Action.sa_mask);
if(sigaction(SigNr, &Action, NULL) == -1){
LOG_ERR("Failed to change handler for signal %d (%s): (%d) %s",
SigNr, sigdescr_np(SigNr), errno, strerrordesc_np(errno));
return false;
}
return true;
}
static void ShutdownHandler(int SigNr){
g_ShutdownSignal = SigNr;
}
int main(int argc, char **argv){
(void)argc;
(void)argv;
g_ShutdownSignal = 0;
if(!SigHandler(SIGPIPE, SIG_IGN)
|| !SigHandler(SIGINT, ShutdownHandler)
|| !SigHandler(SIGTERM, ShutdownHandler)){
return EXIT_FAILURE;
}
int64 StartTime = GetClockMonotonicMS();
g_MonotonicTimeMS = 0;
LOG("Tibia Login Server v0.1");
if(!ReadConfig("config.cfg", LoginKVCallback)){
return EXIT_FAILURE;
}
atexit(ExitConnections);
atexit(ExitQuery);
if(!InitConnections()){
return EXIT_FAILURE;
}
if(!InitQuery()){
return EXIT_FAILURE;
}
// NOTE(fusion): Print MOTD with escape codes.
char Motd[30];
if(EscapeString(Motd, sizeof(Motd), g_Motd)){
LOG("MOTD: \"%s\"", Motd);
}else{
LOG("MOTD: \"%s...\"", Motd);
}
LOG("Running at %d updates per second...", g_UpdateRate);
int64 UpdateInterval = 1000 / (int64)g_UpdateRate;
while(g_ShutdownSignal == 0){
int64 UpdateStart = GetClockMonotonicMS();
g_MonotonicTimeMS = (int)(UpdateStart - StartTime);
ProcessConnections();
int64 UpdateEnd = GetClockMonotonicMS();
int64 NextUpdate = UpdateStart + UpdateInterval;
if(NextUpdate > UpdateEnd){
SleepMS(NextUpdate - UpdateEnd);
}
}
LOG("Received signal %d (%s), shutting down...",
g_ShutdownSignal, sigdescr_np(g_ShutdownSignal));
return EXIT_SUCCESS;
}

View File

@ -1,401 +0,0 @@
#ifndef TIBIA_LOGIN_HH_
#define TIBIA_LOGIN_HH_ 1
#include "common.hh"
// Shutdown Signal
extern int g_ShutdownSignal;
// Time
extern int g_MonotonicTimeMS;
// Config
extern char g_Motd[256];
extern int g_UpdateRate;
extern int g_LoginPort;
extern int g_MaxConnections;
extern int g_LoginTimeout;
extern char g_QueryManagerHost[100];
extern int g_QueryManagerPort;
extern char g_QueryManagerPassword[30];
// Buffer Utility
//==============================================================================
inline uint8 BufferRead8(const uint8 *Buffer){
return Buffer[0];
}
inline uint16 BufferRead16LE(const uint8 *Buffer){
return (uint16)Buffer[0]
| ((uint16)Buffer[1] << 8);
}
inline uint16 BufferRead16BE(const uint8 *Buffer){
return ((uint16)Buffer[0] << 8)
| (uint16)Buffer[1];
}
inline uint32 BufferRead32LE(const uint8 *Buffer){
return (uint32)Buffer[0]
| ((uint32)Buffer[1] << 8)
| ((uint32)Buffer[2] << 16)
| ((uint32)Buffer[3] << 24);
}
inline uint32 BufferRead32BE(const uint8 *Buffer){
return ((uint32)Buffer[0] << 24)
| ((uint32)Buffer[1] << 16)
| ((uint32)Buffer[2] << 8)
| (uint32)Buffer[3];
}
inline uint64 BufferRead64LE(const uint8 *Buffer){
return (uint64)Buffer[0]
| ((uint64)Buffer[1] << 8)
| ((uint64)Buffer[2] << 16)
| ((uint64)Buffer[3] << 24)
| ((uint64)Buffer[4] << 32)
| ((uint64)Buffer[5] << 40)
| ((uint64)Buffer[6] << 48)
| ((uint64)Buffer[7] << 56);
}
inline uint64 BufferRead64BE(const uint8 *Buffer){
return ((uint64)Buffer[0] << 56)
| ((uint64)Buffer[1] << 48)
| ((uint64)Buffer[2] << 40)
| ((uint64)Buffer[3] << 32)
| ((uint64)Buffer[4] << 24)
| ((uint64)Buffer[5] << 16)
| ((uint64)Buffer[6] << 8)
| (uint64)Buffer[7];
}
inline void BufferWrite8(uint8 *Buffer, uint8 Value){
Buffer[0] = Value;
}
inline void BufferWrite16LE(uint8 *Buffer, uint16 Value){
Buffer[0] = (uint8)(Value >> 0);
Buffer[1] = (uint8)(Value >> 8);
}
inline void BufferWrite16BE(uint8 *Buffer, uint16 Value){
Buffer[0] = (uint8)(Value >> 8);
Buffer[1] = (uint8)(Value >> 0);
}
inline void BufferWrite32LE(uint8 *Buffer, uint32 Value){
Buffer[0] = (uint8)(Value >> 0);
Buffer[1] = (uint8)(Value >> 8);
Buffer[2] = (uint8)(Value >> 16);
Buffer[3] = (uint8)(Value >> 24);
}
inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
Buffer[0] = (uint8)(Value >> 24);
Buffer[1] = (uint8)(Value >> 16);
Buffer[2] = (uint8)(Value >> 8);
Buffer[3] = (uint8)(Value >> 0);
}
inline void BufferWrite64LE(uint8 *Buffer, uint64 Value){
Buffer[0] = (uint8)(Value >> 0);
Buffer[1] = (uint8)(Value >> 8);
Buffer[2] = (uint8)(Value >> 16);
Buffer[3] = (uint8)(Value >> 24);
Buffer[4] = (uint8)(Value >> 32);
Buffer[5] = (uint8)(Value >> 40);
Buffer[6] = (uint8)(Value >> 48);
Buffer[7] = (uint8)(Value >> 56);
}
inline void BufferWrite64BE(uint8 *Buffer, uint64 Value){
Buffer[0] = (uint8)(Value >> 56);
Buffer[1] = (uint8)(Value >> 48);
Buffer[2] = (uint8)(Value >> 40);
Buffer[3] = (uint8)(Value >> 32);
Buffer[4] = (uint8)(Value >> 24);
Buffer[5] = (uint8)(Value >> 16);
Buffer[6] = (uint8)(Value >> 8);
Buffer[7] = (uint8)(Value >> 0);
}
struct TReadBuffer{
uint8 *Buffer;
int Size;
int Position;
TReadBuffer(void) : TReadBuffer(NULL, 0) {}
TReadBuffer(uint8 *Buffer, int Size)
: Buffer(Buffer), Size(Size), Position(0) {}
bool CanRead(int Bytes){
return (this->Position + Bytes) <= this->Size;
}
bool Overflowed(void){
return this->Position > this->Size;
}
bool ReadFlag(void){
return this->Read8() != 0x00;
}
uint8 Read8(void){
uint8 Result = 0;
if(this->CanRead(1)){
Result = BufferRead8(this->Buffer + this->Position);
}
this->Position += 1;
return Result;
}
uint16 Read16(void){
uint16 Result = 0;
if(this->CanRead(2)){
Result = BufferRead16LE(this->Buffer + this->Position);
}
this->Position += 2;
return Result;
}
uint16 Read16BE(void){
uint16 Result = 0;
if(this->CanRead(2)){
Result = BufferRead16BE(this->Buffer + this->Position);
}
this->Position += 2;
return Result;
}
uint32 Read32(void){
uint32 Result = 0;
if(this->CanRead(4)){
Result = BufferRead32LE(this->Buffer + this->Position);
}
this->Position += 4;
return Result;
}
uint32 Read32BE(void){
uint32 Result = 0;
if(this->CanRead(4)){
Result = BufferRead32BE(this->Buffer + this->Position);
}
this->Position += 4;
return Result;
}
void ReadString(char *Dest, int DestCapacity){
int Length = (int)this->Read16();
if(Length == 0xFFFF){
Length = (int)this->Read32();
}
if(Dest != NULL && DestCapacity > 0){
if(Length < DestCapacity && this->CanRead(Length)){
memcpy(Dest, this->Buffer + this->Position, Length);
Dest[Length] = 0;
}else{
Dest[0] = 0;
}
}
this->Position += Length;
}
void ReadBytes(uint8 *Buffer, int Count){
if(this->CanRead(Count)){
memcpy(Buffer, this->Buffer + this->Position, Count);
}
this->Position += Count;
}
};
struct TWriteBuffer{
uint8 *Buffer;
int Size;
int Position;
TWriteBuffer(void) : TWriteBuffer(NULL, 0) {}
TWriteBuffer(uint8 *Buffer, int Size)
: Buffer(Buffer), Size(Size), Position(0) {}
bool CanWrite(int Bytes){
return (this->Position + Bytes) <= this->Size;
}
bool Overflowed(void){
return this->Position > this->Size;
}
void WriteFlag(bool Value){
this->Write8(Value ? 0x01 : 0x00);
}
void Write8(uint8 Value){
if(this->CanWrite(1)){
BufferWrite8(this->Buffer + this->Position, Value);
}
this->Position += 1;
}
void Write16(uint16 Value){
if(this->CanWrite(2)){
BufferWrite16LE(this->Buffer + this->Position, Value);
}
this->Position += 2;
}
void Write16BE(uint16 Value){
if(this->CanWrite(2)){
BufferWrite16BE(this->Buffer + this->Position, Value);
}
this->Position += 2;
}
void Write32(uint32 Value){
if(this->CanWrite(4)){
BufferWrite32LE(this->Buffer + this->Position, Value);
}
this->Position += 4;
}
void Write32BE(uint32 Value){
if(this->CanWrite(4)){
BufferWrite32BE(this->Buffer + this->Position, Value);
}
this->Position += 4;
}
void WriteString(const char *String){
int StringLength = 0;
if(String != NULL){
StringLength = (int)strlen(String);
}
if(StringLength < 0xFFFF){
this->Write16((uint16)StringLength);
}else{
this->Write16(0xFFFF);
this->Write32((uint32)StringLength);
}
if(StringLength > 0 && this->CanWrite(StringLength)){
memcpy(this->Buffer + this->Position, String, StringLength);
}
this->Position += StringLength;
}
void Rewrite16(int Position, uint16 Value){
if((Position + 2) <= this->Position && !this->Overflowed()){
BufferWrite16LE(this->Buffer + Position, Value);
}
}
void Insert32(int Position, uint32 Value){
if(Position <= this->Position){
if(this->CanWrite(4)){
memmove(this->Buffer + Position + 4,
this->Buffer + Position,
this->Position - Position);
BufferWrite32LE(this->Buffer + Position, Value);
}
this->Position += 4;
}
}
};
// crypto.cc
//==============================================================================
typedef void RSAKey;
RSAKey *RSALoadPEM(const char *FileName);
void RSAFree(RSAKey *Key);
bool RSADecrypt(RSAKey *Key, uint8 *Data, int Size);
void XTEAEncrypt(const uint32 *Key, uint8 *Data, int Size);
void XTEADecrypt(const uint32 *Key, uint8 *Data, int Size);
// query.cc
//==============================================================================
enum : int {
APPLICATION_TYPE_GAME = 1,
APPLICATION_TYPE_LOGIN = 2,
APPLICATION_TYPE_WEB = 3,
};
enum : int {
QUERY_STATUS_OK = 0,
QUERY_STATUS_ERROR = 1,
QUERY_STATUS_FAILED = 3,
};
struct TCharacterLoginData{
char Name[30];
char WorldName[30];
int WorldAddress;
int WorldPort;
};
struct TQueryManagerConnection{
int Socket;
uint8 Buffer[KB(16)];
};
bool Connect(TQueryManagerConnection *Connection);
void Disconnect(TQueryManagerConnection *Connection);
bool IsConnected(TQueryManagerConnection *Connection);
TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType);
int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
TWriteBuffer *WriteBuffer, TReadBuffer *OutReadBuffer);
int LoginAccount(int AccountID, const char *Password, const char *IPAddress,
int MaxCharacters, int *NumCharacters, TCharacterLoginData *Characters,
int *PremiumDays);
bool InitQuery(void);
void ExitQuery(void);
// connections.cc
//==============================================================================
enum ConnectionState{
CONNECTION_FREE = 0,
CONNECTION_HANDSHAKE = 1,
CONNECTION_LOGIN = 2,
CONNECTION_WRITE = 3,
};
struct TConnection{
ConnectionState State;
int Socket;
int StartTime;
int RWSize;
int RWPosition;
uint32 RandomSeed;
uint32 XTEA[4];
char IPAddress[16];
char RemoteAddress[30];
uint8 Buffer[KB(2)];
};
int ListenerBind(uint16 Port);
int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort);
void CloseConnection(TConnection *Connection);
TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port);
void ReleaseConnection(TConnection *Connection);
void ProcessLoginRequest(TConnection *Connection);
void CheckConnectionInput(TConnection *Connection, int Events);
void CheckConnectionOutput(TConnection *Connection, int Events);
void CheckConnection(TConnection *Connection, int Events);
void ProcessConnections(void);
bool InitConnections(void);
void ExitConnections(void);
TWriteBuffer PrepareResponse(TConnection *Connection);
void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer);
void SendLoginError(TConnection *Connection, const char *Message);
void SendCharacterList(TConnection *Connection, int NumCharacters,
TCharacterLoginData *Characters, int PremiumDays);
void ProcessLoginRequest(TConnection *Connection);
#endif //TIBIA_LOGIN_HH_

707
src/main.cc Normal file
View File

@ -0,0 +1,707 @@
#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);
{ // NOTE(fusion): Print MOTD preview with escape codes.
char MotdPreview[30];
if(StringBufEscape(MotdPreview, g_Config.Motd)){
LOG("MOTD: \"%s\"", MotdPreview);
}else{
LOG("MOTD: \"%s...\"", MotdPreview);
}
}
atexit(ExitQuery);
atexit(ExitConnections);
if(!InitQuery() || !InitConnections()){
return EXIT_FAILURE;
}
LOG("Running...");
while(g_ShutdownSignal == 0){
ProcessConnections();
}
LOG("Received signal %d (%s), shutting down...",
g_ShutdownSignal, sigdescr_np(g_ShutdownSignal));
return EXIT_SUCCESS;
}

View File

@ -1,18 +1,13 @@
#include "login.hh"
#include "common.hh"
// TODO(fusion): Support windows eventually?
#if OS_LINUX
#include <errno.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#else
# error "Operating system not currently supported."
#endif
static TQueryManagerConnection *g_QueryManagerConnection;
bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
static bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
ASSERT(HostName != NULL && OutAddr != NULL);
addrinfo *Result = NULL;
addrinfo Hints = {};
@ -39,65 +34,6 @@ bool ResolveHostName(const char *HostName, in_addr_t *OutAddr){
return Resolved;
}
bool Connect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
LOG_ERR("Already connected");
return false;
}
in_addr_t Addr;
if(!ResolveHostName(g_QueryManagerHost, &Addr)){
LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_QueryManagerHost);
return false;
}
Connection->Socket = socket(AF_INET, SOCK_STREAM, 0);
if(Connection->Socket == -1){
LOG_ERR("Failed to create socket: (%d) %s", errno, strerrordesc_np(errno));
return false;
}
sockaddr_in QueryManagerAddress = {};
QueryManagerAddress.sin_family = AF_INET;
QueryManagerAddress.sin_port = htons((uint16)g_QueryManagerPort);
QueryManagerAddress.sin_addr.s_addr = Addr;
if(connect(Connection->Socket, (sockaddr*)&QueryManagerAddress, sizeof(QueryManagerAddress)) == -1){
LOG_ERR("Failed to connect: (%d) %s", errno, strerrordesc_np(errno));
Disconnect(Connection);
return false;
}
TWriteBuffer WriteBuffer = PrepareQuery(Connection, 0);
WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN);
WriteBuffer.WriteString(g_QueryManagerPassword);
int Status = ExecuteQuery(Connection, false, &WriteBuffer, NULL);
if(Status != QUERY_STATUS_OK){
LOG_ERR("Failed to login to query manager (%d)", Status);
Disconnect(Connection);
return false;
}
return true;
}
void Disconnect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
close(Connection->Socket);
Connection->Socket = -1;
}
}
bool IsConnected(TQueryManagerConnection *Connection){
return Connection->Socket != -1;
}
TWriteBuffer PrepareQuery(TQueryManagerConnection *Connection, int QueryType){
TWriteBuffer WriteBuffer(Connection->Buffer, sizeof(Connection->Buffer));
WriteBuffer.Write16(0); // Request Size
WriteBuffer.Write8((uint8)QueryType);
return WriteBuffer;
}
static bool WriteExact(int Fd, const uint8 *Buffer, int Size){
int BytesToWrite = Size;
const uint8 *WritePtr = Buffer;
@ -126,12 +62,73 @@ static bool ReadExact(int Fd, uint8 *Buffer, int Size){
return true;
}
bool Connect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
LOG_ERR("Already connected");
return false;
}
in_addr_t Addr;
if(!ResolveHostName(g_Config.QueryManagerHost, &Addr)){
LOG_ERR("Failed to resolve query manager's host name \"%s\"", g_Config.QueryManagerHost);
return false;
}
Connection->Socket = socket(AF_INET, SOCK_STREAM, 0);
if(Connection->Socket == -1){
LOG_ERR("Failed to create socket: (%d) %s", errno, strerrordesc_np(errno));
return false;
}
sockaddr_in QueryManagerAddress = {};
QueryManagerAddress.sin_family = AF_INET;
QueryManagerAddress.sin_port = htons((uint16)g_Config.QueryManagerPort);
QueryManagerAddress.sin_addr.s_addr = Addr;
if(connect(Connection->Socket, (sockaddr*)&QueryManagerAddress, sizeof(QueryManagerAddress)) == -1){
LOG_ERR("Failed to connect: (%d) %s", errno, strerrordesc_np(errno));
Disconnect(Connection);
return false;
}
uint8 LoginBuffer[1024];
TWriteBuffer WriteBuffer = PrepareQuery(QUERY_LOGIN, LoginBuffer, sizeof(LoginBuffer));
WriteBuffer.Write8((uint8)APPLICATION_TYPE_LOGIN);
WriteBuffer.WriteString(g_Config.QueryManagerPassword);
int Status = ExecuteQuery(Connection, false, &WriteBuffer, NULL);
if(Status != QUERY_STATUS_OK){
LOG_ERR("Failed to login to query manager (%d)", Status);
Disconnect(Connection);
return false;
}
return true;
}
void Disconnect(TQueryManagerConnection *Connection){
if(Connection->Socket != -1){
close(Connection->Socket);
Connection->Socket = -1;
}
}
bool IsConnected(TQueryManagerConnection *Connection){
return Connection->Socket != -1;
}
TWriteBuffer PrepareQuery(int QueryType, uint8 *Buffer, int BufferSize){
TWriteBuffer WriteBuffer(Buffer, BufferSize);
WriteBuffer.Write16(0); // Request Size
WriteBuffer.Write8((uint8)QueryType);
return WriteBuffer;
}
int ExecuteQuery(TQueryManagerConnection *Connection, bool AutoReconnect,
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;
for(int Attempt = 1; true; Attempt += 1){
uint8 *Buffer = WriteBuffer->Buffer;
int BufferSize = WriteBuffer->Size;
int WriteSize = WriteBuffer->Position;
if(!IsConnected(Connection)){
if(!AutoReconnect){
for(int Attempt = 1; true; Attempt += 1){
if(!IsConnected(Connection) && (!AutoReconnect || !Connect(Connection))){
return QUERY_STATUS_FAILED;
}
// IMPORTANT(fusion): There is no way around this. `Connect` will
// use the connection buffer to send the login query so we need to
// save and restore it to not lose any data. One improvement here
// would be to use a stack or statically allocated buffer, although
// using malloc/free should have no real impact on performance as
// we're already doing BLOCKING I/O here.
uint8 *TempBuffer = (uint8*)malloc(WriteSize);
memcpy(TempBuffer, Connection->Buffer, WriteSize);
bool Reconnected = Connect(Connection);
memcpy(Connection->Buffer, TempBuffer, WriteSize);
free(TempBuffer);
if(!Reconnected){
return QUERY_STATUS_FAILED;
}
}
if(!WriteExact(Connection->Socket, Connection->Buffer, WriteSize)){
if(!WriteExact(Connection->Socket, Buffer, WriteSize)){
Disconnect(Connection);
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;
}
}

215
src/status.cc Normal file
View File

@ -0,0 +1,215 @@
#include "common.hh"
static int g_LastStatusRefresh;
static char g_StatusString[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 = TimeNow - 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;
}
XMLBuffer Buffer = {};
Buffer.Data = g_StatusString;
Buffer.Size = sizeof(g_StatusString);
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;
}