8 Commits

Author SHA1 Message Date
7dc43ddd6f Add LastPasswordReset timestamp column, missing DB version bump 2024-11-26 21:21:35 -05:00
cd4e7ee159 Add Email column to Account table 2024-11-25 09:56:22 -08:00
ed9fe61faf Auth cookie refresh on PC_EXIT 2024-11-23 11:29:55 -08:00
55cf3f7102 Refactor login packet handler for more flexible auth (#298)
This PR enables auth cookies to be used simultaneously with plaintext paasswords sent in the cookie authID field.

* Hoist a bunch of checks from the login packet handler into helper functions.
* Rename the LoginType enum to AuthMethod and distinguish it from the iLoginType packet field (see comment in code for why these should be decoupled).
* If the provided token does not pass the cookie check and password auth is enabled, treat it as a plaintext password and authenticate if it is correct.
2024-11-17 05:21:37 +01:00
1543dac4e0 BUILD - Optimize Dockerfile by using alpine as base image (#277) 2024-10-28 21:26:25 -07:00
ae327cc104 Use FE2CL_..._AROUND, _AROUND_DEL packets (#295)
* Use FE2CL_..._AROUND, _AROUND_DEL packets
* Use increased buffer size for 728 and 1013 protocols
2024-10-28 20:49:34 -07:00
Juansecu
352fa8a133 BUILD - Expose ports used by the application server in Dockerfile 2024-10-09 19:13:54 -05:00
Juansecu
2096c3c3cc BUILD - Optimize Dockerfile by using alpine as base image 2024-06-08 21:55:03 -05:00
21 changed files with 461 additions and 147 deletions

View File

@@ -1,28 +1,33 @@
# build # build
FROM debian:stable-slim AS build FROM alpine:3 as build
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt-get -y update && apt-get install -y \ RUN apk update && apk upgrade && apk add \
linux-headers \
git \ git \
clang \ clang18 \
make \ make \
libsqlite3-dev sqlite-dev
COPY src ./src COPY src ./src
COPY vendor ./vendor COPY vendor ./vendor
COPY .git ./.git COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./ COPY Makefile CMakeLists.txt version.h.in ./
RUN sed -i 's/^CC=clang$/&-18/' Makefile
RUN sed -i 's/^CXX=clang++$/&-18/' Makefile
RUN make nosandbox -j$(nproc) RUN make nosandbox -j$(nproc)
# prod # prod
FROM debian:stable-slim FROM alpine:3
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt-get -y update && apt-get install -y \ RUN apk update && apk upgrade && apk add \
libsqlite3-dev libstdc++ \
sqlite-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion COPY --from=build /usr/src/app/bin/fusion /bin/fusion
COPY sql ./sql COPY sql ./sql
@@ -31,6 +36,6 @@ CMD ["/bin/fusion"]
EXPOSE 23000/tcp EXPOSE 23000/tcp
EXPOSE 23001/tcp EXPOSE 23001/tcp
EXPOSE 8001/tcp EXPOSE 8003/tcp
LABEL Name=openfusion Version=1.6.0 LABEL Name=openfusion Version=1.6.0

View File

@@ -115,6 +115,7 @@ CXXHDR=\
src/settings.hpp\ src/settings.hpp\
src/Transport.hpp\ src/Transport.hpp\
src/TableData.hpp\ src/TableData.hpp\
src/Bucket.hpp\
src/Chunking.hpp\ src/Chunking.hpp\
src/Buddies.hpp\ src/Buddies.hpp\
src/Groups.hpp\ src/Groups.hpp\

View File

@@ -18,8 +18,8 @@ acceptallcustomnames=true
# automatically create them? # automatically create them?
autocreateaccounts=true autocreateaccounts=true
# list of supported authentication methods (comma-separated) # list of supported authentication methods (comma-separated)
# password = allow login type 1 with plaintext passwords # password = allow logging in with plaintext passwords
# cookie = allow login type 2 with one-shot auth cookies # cookie = allow logging in with one-shot auth cookies
authmethods=password authmethods=password
# how often should everything be flushed to the database? # how often should everything be flushed to the database?
# the default is 4 minutes # the default is 4 minutes

8
sql/migration5.sql Normal file
View File

@@ -0,0 +1,8 @@
BEGIN TRANSACTION;
-- New Columns
ALTER TABLE Accounts ADD Email TEXT DEFAULT '' NOT NULL;
ALTER TABLE Accounts ADD LastPasswordReset INTEGER DEFAULT 0 NOT NULL;
-- Update DB Version
UPDATE Meta SET Value = 6 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;

View File

@@ -1,14 +1,16 @@
CREATE TABLE IF NOT EXISTS Accounts ( CREATE TABLE IF NOT EXISTS Accounts (
AccountID INTEGER NOT NULL, AccountID INTEGER NOT NULL,
Login TEXT NOT NULL UNIQUE COLLATE NOCASE, Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
Password TEXT NOT NULL, Password TEXT NOT NULL,
Selected INTEGER DEFAULT 1 NOT NULL, Selected INTEGER DEFAULT 1 NOT NULL,
AccountLevel INTEGER NOT NULL, AccountLevel INTEGER NOT NULL,
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
BannedUntil INTEGER DEFAULT 0 NOT NULL, BannedUntil INTEGER DEFAULT 0 NOT NULL,
BannedSince INTEGER DEFAULT 0 NOT NULL, BannedSince INTEGER DEFAULT 0 NOT NULL,
BanReason TEXT DEFAULT '' NOT NULL, BanReason TEXT DEFAULT '' NOT NULL,
Email TEXT DEFAULT '' NOT NULL,
LastPasswordReset INTEGER DEFAULT 0 NOT NULL,
PRIMARY KEY(AccountID AUTOINCREMENT) PRIMARY KEY(AccountID AUTOINCREMENT)
); );

40
src/Bucket.hpp Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <array>
#include <optional>
#include <assert.h>
template<class T, size_t N>
class Bucket {
std::array<T, N> buf;
size_t sz;
public:
Bucket() {
sz = 0;
}
void add(const T& item) {
assert(sz < N);
buf[sz++] = item;
}
std::optional<T> get(size_t idx) const {
if (idx < sz) {
return buf[idx];
}
return std::nullopt;
}
size_t size() const {
return sz;
}
bool isFull() const {
return sz == N;
}
void clear() {
sz = 0;
}
};

View File

@@ -1,7 +1,9 @@
#include "Chunking.hpp" #include "Chunking.hpp"
#include "Player.hpp"
#include "MobAI.hpp" #include "MobAI.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "Bucket.hpp"
#include <assert.h> #include <assert.h>
@@ -11,6 +13,12 @@ using namespace Chunking;
* The initial chunkPos value before a player is placed into the world. * The initial chunkPos value before a player is placed into the world.
*/ */
const ChunkPos Chunking::INVALID_CHUNK = {}; const ChunkPos Chunking::INVALID_CHUNK = {};
constexpr size_t MAX_PC_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sPCAppearanceData);
constexpr size_t MAX_NPC_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sNPCAppearanceData);
constexpr size_t MAX_SHINY_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sShinyAppearanceData);
constexpr size_t MAX_TRANSPORTATION_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sTransportationAppearanceData);
constexpr size_t MAX_IDS_PER_AROUND_DEL = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(int32_t);
constexpr size_t MAX_TRANSPORTATION_IDS_PER_AROUND_DEL = MAX_IDS_PER_AROUND_DEL - 1; // 1 less for eTT
std::map<ChunkPos, Chunk*> Chunking::chunks; std::map<ChunkPos, Chunk*> Chunking::chunks;
@@ -75,11 +83,80 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
deleteChunk(chunkPos); deleteChunk(chunkPos);
} }
template<class T, size_t N>
static void sendAroundPackets(const EntityRef recipient, std::vector<Bucket<T, N>>& buckets, uint32_t packetId) {
assert(recipient.kind == EntityKind::PLAYER);
uint8_t pktBuf[CN_PACKET_BODY_SIZE];
for (const auto& bucket : buckets) {
memset(pktBuf, 0, CN_PACKET_BODY_SIZE);
int count = bucket.size();
*((int32_t*)pktBuf) = count;
T* data = (T*)(pktBuf + sizeof(int32_t));
for (size_t i = 0; i < count; i++) {
data[i] = bucket.get(i).value();
}
recipient.sock->sendPacket(pktBuf, packetId, sizeof(int32_t) + (count * sizeof(T)));
}
}
template<size_t N>
static void sendAroundDelPackets(const EntityRef recipient, std::vector<Bucket<int32_t, N>>& buckets, uint32_t packetId) {
assert(recipient.kind == EntityKind::PLAYER);
uint8_t pktBuf[CN_PACKET_BODY_SIZE];
for (const auto& bucket : buckets) {
memset(pktBuf, 0, CN_PACKET_BODY_SIZE);
int count = bucket.size();
assert(count <= N);
size_t baseSize;
if (packetId == P_FE2CL_AROUND_DEL_TRANSPORTATION) {
sP_FE2CL_AROUND_DEL_TRANSPORTATION* pkt = (sP_FE2CL_AROUND_DEL_TRANSPORTATION*)pktBuf;
pkt->eTT = 3;
pkt->iCnt = count;
baseSize = sizeof(sP_FE2CL_AROUND_DEL_TRANSPORTATION);
} else {
*((int32_t*)pktBuf) = count;
baseSize = sizeof(int32_t);
}
int32_t* ids = (int32_t*)(pktBuf + baseSize);
for (size_t i = 0; i < count; i++) {
ids[i] = bucket.get(i).value();
}
recipient.sock->sendPacket(pktBuf, packetId, baseSize + (count * sizeof(int32_t)));
}
}
template<class T, size_t N>
static void bufferAppearanceData(std::vector<Bucket<T, N>>& buckets, const T& data) {
if (buckets.empty())
buckets.push_back({});
auto& bucket = buckets[buckets.size() - 1];
bucket.add(data);
if (bucket.isFull())
buckets.push_back({});
}
template<size_t N>
static void bufferIdForDisappearance(std::vector<Bucket<int32_t, N>>& buckets, int32_t id) {
if (buckets.empty())
buckets.push_back({});
auto& bucket = buckets[buckets.size() - 1];
bucket.add(id);
if (bucket.isFull())
buckets.push_back({});
}
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) { void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *ent = ref.getEntity(); Entity *ent = ref.getEntity();
bool alive = ent->isExtant(); bool alive = ent->isExtant();
// TODO: maybe optimize this, potentially using AROUND packets? std::vector<Bucket<sPCAppearanceData, MAX_PC_PER_AROUND>> pcAppearances;
std::vector<Bucket<sNPCAppearanceData, MAX_NPC_PER_AROUND>> npcAppearances;
std::vector<Bucket<sShinyAppearanceData, MAX_SHINY_PER_AROUND>> shinyAppearances;
std::vector<Bucket<sTransportationAppearanceData, MAX_TRANSPORTATION_PER_AROUND>> transportationAppearances;
for (Chunk *chunk : chnks) { for (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) { for (const EntityRef otherRef : chunk->entities) {
// skip oneself // skip oneself
@@ -95,7 +172,38 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
// notify this *player* of the existence of all visible Entities // notify this *player* of the existence of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) { if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
other->enterIntoViewOf(ref.sock); sPCAppearanceData pcData;
sNPCAppearanceData npcData;
sShinyAppearanceData eggData;
sTransportationAppearanceData busData;
switch(otherRef.kind) {
case EntityKind::PLAYER:
pcData = dynamic_cast<Player*>(other)->getAppearanceData();
bufferAppearanceData(pcAppearances, pcData);
break;
case EntityKind::SIMPLE_NPC:
npcData = dynamic_cast<BaseNPC*>(other)->getAppearanceData();
bufferAppearanceData(npcAppearances, npcData);
break;
case EntityKind::COMBAT_NPC:
npcData = dynamic_cast<CombatNPC*>(other)->getAppearanceData();
bufferAppearanceData(npcAppearances, npcData);
break;
case EntityKind::MOB:
npcData = dynamic_cast<Mob*>(other)->getAppearanceData();
bufferAppearanceData(npcAppearances, npcData);
break;
case EntityKind::EGG:
eggData = dynamic_cast<Egg*>(other)->getShinyAppearanceData();
bufferAppearanceData(shinyAppearances, eggData);
break;
case EntityKind::BUS:
busData = dynamic_cast<Bus*>(other)->getTransportationAppearanceData();
bufferAppearanceData(transportationAppearances, busData);
break;
default:
break;
}
} }
// for mobs, increment playersInView // for mobs, increment playersInView
@@ -105,13 +213,27 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
((Mob*)other)->playersInView++; ((Mob*)other)->playersInView++;
} }
} }
if (ref.kind == EntityKind::PLAYER) {
if (!pcAppearances.empty())
sendAroundPackets(ref, pcAppearances, P_FE2CL_PC_AROUND);
if (!npcAppearances.empty())
sendAroundPackets(ref, npcAppearances, P_FE2CL_NPC_AROUND);
if (!shinyAppearances.empty())
sendAroundPackets(ref, shinyAppearances, P_FE2CL_SHINY_AROUND);
if (!transportationAppearances.empty())
sendAroundPackets(ref, transportationAppearances, P_FE2CL_TRANSPORTATION_AROUND);
}
} }
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) { void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *ent = ref.getEntity(); Entity *ent = ref.getEntity();
bool alive = ent->isExtant(); bool alive = ent->isExtant();
// TODO: same as above std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> pcDisappearances;
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> npcDisappearances;
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> shinyDisappearances;
std::vector<Bucket<int32_t, MAX_TRANSPORTATION_IDS_PER_AROUND_DEL>> transportationDisappearances;
for (Chunk *chunk : chnks) { for (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) { for (const EntityRef otherRef : chunk->entities) {
// skip oneself // skip oneself
@@ -127,7 +249,29 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef re
// notify this *player* of the departure of all visible Entities // notify this *player* of the departure of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) { if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
other->disappearFromViewOf(ref.sock); int32_t id;
switch(otherRef.kind) {
case EntityKind::PLAYER:
id = dynamic_cast<Player*>(other)->iID;
bufferIdForDisappearance(pcDisappearances, id);
break;
case EntityKind::SIMPLE_NPC:
case EntityKind::COMBAT_NPC:
case EntityKind::MOB:
id = dynamic_cast<BaseNPC*>(other)->id;
bufferIdForDisappearance(npcDisappearances, id);
break;
case EntityKind::EGG:
id = dynamic_cast<Egg*>(other)->id;
bufferIdForDisappearance(shinyDisappearances, id);
break;
case EntityKind::BUS:
id = dynamic_cast<Bus*>(other)->id;
bufferIdForDisappearance(transportationDisappearances, id);
break;
default:
break;
}
} }
// for mobs, decrement playersInView // for mobs, decrement playersInView
@@ -137,6 +281,17 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef re
((Mob*)other)->playersInView--; ((Mob*)other)->playersInView--;
} }
} }
if (ref.kind == EntityKind::PLAYER) {
if (!pcDisappearances.empty())
sendAroundDelPackets(ref, pcDisappearances, P_FE2CL_AROUND_DEL_PC);
if (!npcDisappearances.empty())
sendAroundDelPackets(ref, npcDisappearances, P_FE2CL_AROUND_DEL_NPC);
if (!shinyDisappearances.empty())
sendAroundDelPackets(ref, shinyDisappearances, P_FE2CL_AROUND_DEL_SHINY);
if (!transportationDisappearances.empty())
sendAroundDelPackets(ref, transportationDisappearances, P_FE2CL_AROUND_DEL_TRANSPORTATION);
}
} }
static void emptyChunk(ChunkPos chunkPos) { static void emptyChunk(ChunkPos chunkPos) {

View File

@@ -126,15 +126,6 @@ static void eggStep(CNServer* serv, time_t currTime) {
} }
void Eggs::npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg) {
egg->iX = x;
egg->iY = y;
egg->iZ = z;
// client doesn't care about egg->iMapNum
egg->iShinyType = npc->iNPCType;
egg->iShiny_ID = npc->iNPC_ID;
}
static void eggPickup(CNSocket* sock, CNPacketData* data) { static void eggPickup(CNSocket* sock, CNPacketData* data) {
auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf; auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);

View File

@@ -15,5 +15,4 @@ namespace Eggs {
void init(); void init();
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration); void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
} }

View File

@@ -70,11 +70,7 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt); INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC? // TODO: Potentially decouple this from BaseNPC?
pkt.AppearanceData = { pkt.AppearanceData = getTransportationAppearanceData();
3, id, type,
x, y, z
};
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER); sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
} }
@@ -82,12 +78,22 @@ void Egg::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt); INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC? // TODO: Potentially decouple this from BaseNPC?
pkt.ShinyAppearanceData = { pkt.ShinyAppearanceData = getShinyAppearanceData();
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
}
sTransportationAppearanceData Bus::getTransportationAppearanceData() {
return sTransportationAppearanceData {
3, id, type,
x, y, z
};
}
sShinyAppearanceData Egg::getShinyAppearanceData() {
return sShinyAppearanceData {
id, type, 0, // client doesn't care about map num id, type, 0, // client doesn't care about map num
x, y, z x, y, z
}; };
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
} }
sNano* Player::getActiveNano() { sNano* Player::getActiveNano() {

View File

@@ -161,6 +161,8 @@ struct Egg : public BaseNPC {
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
sShinyAppearanceData getShinyAppearanceData();
}; };
struct Bus : public BaseNPC { struct Bus : public BaseNPC {
@@ -172,4 +174,6 @@ struct Bus : public BaseNPC {
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
sTransportationAppearanceData getTransportationAppearanceData();
}; };

View File

@@ -396,6 +396,14 @@ static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
static void exitGame(CNSocket* sock, CNPacketData* data) { static void exitGame(CNSocket* sock, CNPacketData* data) {
auto exitData = (sP_CL2FE_REQ_PC_EXIT*)data->buf; auto exitData = (sP_CL2FE_REQ_PC_EXIT*)data->buf;
// Refresh any auth cookie, in case "change character" was used
Player* plr = getPlayer(sock);
if (plr != nullptr) {
// 5 seconds should be enough to log in again
Database::refreshCookie(plr->accountId, 5);
}
INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response); INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
response.iID = exitData->iID; response.iID = exitData->iID;

View File

@@ -49,6 +49,7 @@ std::string U16toU8(char16_t* src, size_t max);
size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des
time_t getTime(); time_t getTime();
time_t getTimestamp(); time_t getTimestamp();
int timingSafeStrcmp(const char* a, const char* b);
void terminate(int); void terminate(int);
// The PROTOCOL_VERSION definition can be defined by the build system. // The PROTOCOL_VERSION definition can be defined by the build system.

View File

@@ -1,6 +1,8 @@
/* enum definitions from the client */ /* enum definitions from the client */
#pragma once #pragma once
#include "core/CNStructs.hpp"
// floats // floats
const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f; const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f;
const float CN_EP_RANK_1 = 0.8f; const float CN_EP_RANK_1 = 0.8f;
@@ -410,7 +412,13 @@ enum {
SEND_ANYCAST_NEW = 3, SEND_ANYCAST_NEW = 3,
SEND_BROADCAST = 4, SEND_BROADCAST = 4,
#if PROTOCOL_VERSION == 728
CN_PACKET_BUFFER_SIZE = 8192,
#elif PROTOCOL_VERSION == 1013
CN_PACKET_BUFFER_SIZE = 8192,
#else
CN_PACKET_BUFFER_SIZE = 4096, CN_PACKET_BUFFER_SIZE = 4096,
#endif
P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889 P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889
P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890 P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890

View File

@@ -235,7 +235,7 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
PACKET(P_FE2CL_REP_PC_EXIT_FAIL), PACKET(P_FE2CL_REP_PC_EXIT_FAIL),
PACKET(P_FE2CL_REP_PC_EXIT_SUCC), PACKET(P_FE2CL_REP_PC_EXIT_SUCC),
PACKET(P_FE2CL_PC_EXIT), PACKET(P_FE2CL_PC_EXIT),
PACKET(P_FE2CL_PC_AROUND), VAR_PACKET(P_FE2CL_PC_AROUND, iPCCnt, sPCAppearanceData),
PACKET(P_FE2CL_PC_MOVE), PACKET(P_FE2CL_PC_MOVE),
PACKET(P_FE2CL_PC_STOP), PACKET(P_FE2CL_PC_STOP),
PACKET(P_FE2CL_PC_JUMP), PACKET(P_FE2CL_PC_JUMP),
@@ -243,9 +243,9 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
PACKET(P_FE2CL_NPC_EXIT), PACKET(P_FE2CL_NPC_EXIT),
PACKET(P_FE2CL_NPC_MOVE), PACKET(P_FE2CL_NPC_MOVE),
PACKET(P_FE2CL_NPC_NEW), PACKET(P_FE2CL_NPC_NEW),
PACKET(P_FE2CL_NPC_AROUND), VAR_PACKET(P_FE2CL_NPC_AROUND, iNPCCnt, sNPCAppearanceData),
PACKET(P_FE2CL_AROUND_DEL_PC), VAR_PACKET(P_FE2CL_AROUND_DEL_PC, iPCCnt, int32_t),
PACKET(P_FE2CL_AROUND_DEL_NPC), VAR_PACKET(P_FE2CL_AROUND_DEL_NPC, iNPCCnt, int32_t),
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC), PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL), PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL),
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs_SUCC, iNPCCnt, sAttackResult), VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs_SUCC, iNPCCnt, sAttackResult),
@@ -387,8 +387,8 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
PACKET(P_FE2CL_TRANSPORTATION_EXIT), PACKET(P_FE2CL_TRANSPORTATION_EXIT),
PACKET(P_FE2CL_TRANSPORTATION_MOVE), PACKET(P_FE2CL_TRANSPORTATION_MOVE),
PACKET(P_FE2CL_TRANSPORTATION_NEW), PACKET(P_FE2CL_TRANSPORTATION_NEW),
PACKET(P_FE2CL_TRANSPORTATION_AROUND), VAR_PACKET(P_FE2CL_TRANSPORTATION_AROUND, iCnt, sTransportationAppearanceData),
PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION), VAR_PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION, iCnt, int32_t),
PACKET(P_FE2CL_REP_EP_RANK_LIST), PACKET(P_FE2CL_REP_EP_RANK_LIST),
PACKET(P_FE2CL_REP_EP_RANK_DETAIL), PACKET(P_FE2CL_REP_EP_RANK_DETAIL),
PACKET(P_FE2CL_REP_EP_RANK_PC_INFO), PACKET(P_FE2CL_REP_EP_RANK_PC_INFO),
@@ -404,8 +404,8 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
PACKET(P_FE2CL_SHINY_ENTER), PACKET(P_FE2CL_SHINY_ENTER),
PACKET(P_FE2CL_SHINY_EXIT), PACKET(P_FE2CL_SHINY_EXIT),
PACKET(P_FE2CL_SHINY_NEW), PACKET(P_FE2CL_SHINY_NEW),
PACKET(P_FE2CL_SHINY_AROUND), VAR_PACKET(P_FE2CL_SHINY_AROUND, iShinyCnt, sShinyAppearanceData),
PACKET(P_FE2CL_AROUND_DEL_SHINY), VAR_PACKET(P_FE2CL_AROUND_DEL_SHINY, iShinyCnt, int32_t),
PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL), PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL),
PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC), PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC),
PACKET(P_FE2CL_PC_MOVETRANSPORTATION), PACKET(P_FE2CL_PC_MOVETRANSPORTATION),

View File

@@ -5,7 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#define DATABASE_VERSION 5 #define DATABASE_VERSION 6
namespace Database { namespace Database {
@@ -56,6 +56,7 @@ namespace Database {
// return true if cookie is valid for the account. // return true if cookie is valid for the account.
// invalidates the stored cookie afterwards // invalidates the stored cookie afterwards
bool checkCookie(int accountId, const char *cookie); bool checkCookie(int accountId, const char *cookie);
void refreshCookie(int accountId, int durationSec);
// interface for the /ban command // interface for the /ban command
bool banPlayer(int playerId, std::string& reason); bool banPlayer(int playerId, std::string& reason);

View File

@@ -1,3 +1,5 @@
#include "core/CNStructs.hpp"
#include "db/internal.hpp" #include "db/internal.hpp"
#include "bcrypt/BCrypt.hpp" #include "bcrypt/BCrypt.hpp"
@@ -130,23 +132,46 @@ bool Database::checkCookie(int accountId, const char *tryCookie) {
return false; return false;
} }
/* bool match = (timingSafeStrcmp(cookie, tryCookie) == 0);
* since cookies are immediately invalidated, we don't need to be concerned about
* timing-related side channel attacks, so strcmp is fine here
*/
bool match = (strcmp(cookie, tryCookie) == 0);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL); /*
sqlite3_bind_int(stmt, 1, accountId); * Only invalidate the cookie if it was correct. This prevents
rc = sqlite3_step(stmt); * replay attacks without enabling DOS attacks on accounts.
sqlite3_finalize(stmt); */
if (rc != SQLITE_DONE) if (match) {
std::cout << "[WARN] Database fail on checkCookie(): " << sqlite3_errmsg(db) << std::endl; sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on checkCookie(): " << sqlite3_errmsg(db) << std::endl;
}
return match; return match;
} }
void Database::refreshCookie(int accountId, int durationSec) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Auth
SET Expires = ?
WHERE AccountID = ?;
)";
int expires = getTimestamp() + durationSec;
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, expires);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on refreshCookie(): " << sqlite3_errmsg(db) << std::endl;
}
void Database::updateSelected(int accountId, int slot) { void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);

View File

@@ -222,6 +222,17 @@ time_t getTimestamp() {
return (time_t)value.count(); return (time_t)value.count();
} }
// timing safe strcmp implementation for e.g. cookie validation
int timingSafeStrcmp(const char* a, const char* b) {
int diff = 0;
while (*a && *b) {
diff |= *a++ ^ *b++;
}
diff |= *a;
diff |= *b;
return diff;
}
// convert integer timestamp (in s) to FF systime struct // convert integer timestamp (in s) to FF systime struct
sSYSTEMTIME timeStampToStruct(uint64_t time) { sSYSTEMTIME timeStampToStruct(uint64_t time) {

View File

@@ -109,11 +109,17 @@ void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
std::string userLogin; std::string userLogin;
std::string userToken; // could be password or auth cookie std::string userToken; // could be password or auth cookie
/*
* In this context, "cookie auth" just means the credentials were sent
* in the szCookie fields instead of szID and szPassword.
*/
bool isCookieAuth = login->iLoginType == USE_COOKIE_FIELDS;
/* /*
* The std::string -> char* -> std::string maneuver should remove any * The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator. * trailing garbage after the null terminator.
*/ */
if (login->iLoginType == (int32_t)LoginType::COOKIE) { if (isCookieAuth) {
userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str()); userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str());
userToken = std::string(AUTOU8(login->szCookie_authid).c_str()); userToken = std::string(AUTOU8(login->szCookie_authid).c_str());
} else { } else {
@@ -121,99 +127,41 @@ void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
userToken = std::string(AUTOU16TOU8(login->szPassword).c_str()); userToken = std::string(AUTOU16TOU8(login->szPassword).c_str());
} }
// check username regex if (!CNLoginServer::checkUsername(sock, userLogin)) {
if (!CNLoginServer::isUsernameGood(userLogin)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock); return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
} }
// we only interpret the token as a cookie if cookie login was used and it's allowed.
// otherwise we interpret it as a password, and this maintains compatibility with
// the auto-login trick used on older clients
bool isCookieAuth = login->iLoginType == (int32_t)LoginType::COOKIE
&& CNLoginServer::isLoginTypeAllowed(LoginType::COOKIE);
// password login checks
if (!isCookieAuth) { if (!isCookieAuth) {
// bail if password auth isn't allowed // password was sent in plaintext
if (!CNLoginServer::isLoginTypeAllowed(LoginType::PASSWORD)) { if (!CNLoginServer::checkPassword(sock, userToken)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Password login disabled\n";
text += "This server has disabled logging in with plaintext passwords.\n";
text += "Please contact an admin for assistance.";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 12;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// check regex
if (!CNLoginServer::isPasswordGood(userToken)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid password\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock); return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
} }
} }
Database::Account findUser = {}; Database::Account findUser = {};
Database::findAccount(&findUser, userLogin); Database::findAccount(&findUser, userLogin);
// account was not found // account was not found
if (findUser.AccountID == 0) { if (findUser.AccountID == 0) {
// don't auto-create an account if it's a cookie auth for whatever reason /*
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth) * Don't auto-create accounts if it's a cookie login.
* It'll either be a bad cookie or a plaintext password sent by auto-login;
* either way, we only want to allow auto-creation if the user explicitly entered their credentials.
*/
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth) {
return newAccount(sock, userLogin, userToken, login->iClientVerC); return newAccount(sock, userLogin, userToken, login->iClientVerC);
}
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock); return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
} }
if (isCookieAuth) { // make sure either a valid cookie or password was sent
const char *cookie = userToken.c_str(); if (!CNLoginServer::checkToken(sock, findUser, userToken, isCookieAuth)) {
if (!Database::checkCookie(findUser.AccountID, cookie)) return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
} else {
// simple password check
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userToken))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
} }
// is the account banned if (CNLoginServer::checkBan(sock, findUser)) {
if (findUser.BannedUntil > getTimestamp()) { return; // don't send fail packet
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
// ceiling devision
int64_t remainingDays = (findUser.BannedUntil-getTimestamp()) / 86400 + ((findUser.BannedUntil - getTimestamp()) % 86400 != 0);
std::string text = "Your account has been banned. \nReason: ";
text += findUser.BanReason;
text += "\nBan expires in " + std::to_string(remainingDays) + " day";
if (remainingDays > 1)
text += "s";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 99999999;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// don't send fail packet
return;
} }
/* /*
@@ -659,12 +607,12 @@ bool CNLoginServer::exitDuplicate(int accountId) {
return false; return false;
} }
bool CNLoginServer::isUsernameGood(std::string login) { bool CNLoginServer::isUsernameGood(std::string& login) {
const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}"); const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
return (std::regex_match(login, loginRegex)); return (std::regex_match(login, loginRegex));
} }
bool CNLoginServer::isPasswordGood(std::string password) { bool CNLoginServer::isPasswordGood(std::string& password) {
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}"); const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(password, passwordRegex)); return (std::regex_match(password, passwordRegex));
} }
@@ -680,16 +628,107 @@ bool CNLoginServer::isCharacterNameGood(std::string Firstname, std::string Lastn
return (std::regex_match(Firstname, firstnamecheck) && std::regex_match(Lastname, lastnamecheck)); return (std::regex_match(Firstname, firstnamecheck) && std::regex_match(Lastname, lastnamecheck));
} }
bool CNLoginServer::isLoginTypeAllowed(LoginType loginType) { bool CNLoginServer::isAuthMethodAllowed(AuthMethod authMethod) {
// the config file specifies "comma-separated" but tbh we don't care // the config file specifies "comma-separated" but tbh we don't care
switch (loginType) { switch (authMethod) {
case LoginType::PASSWORD: case AuthMethod::PASSWORD:
return settings::AUTHMETHODS.find("password") != std::string::npos; return settings::AUTHMETHODS.find("password") != std::string::npos;
case LoginType::COOKIE: case AuthMethod::COOKIE:
return settings::AUTHMETHODS.find("cookie") != std::string::npos; return settings::AUTHMETHODS.find("cookie") != std::string::npos;
default: default:
break; break;
} }
return false; return false;
} }
bool CNLoginServer::checkPassword(CNSocket* sock, std::string& password) {
// check password auth allowed
if (!CNLoginServer::isAuthMethodAllowed(AuthMethod::PASSWORD)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Password login disabled\n";
text += "This server has disabled logging in with plaintext passwords.\n";
text += "Please contact an admin for assistance.";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 12;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return false;
}
// check regex
if (!CNLoginServer::isPasswordGood(password)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid password\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return false;
}
return true;
}
bool CNLoginServer::checkUsername(CNSocket* sock, std::string& username) {
// check username regex
if (!CNLoginServer::isUsernameGood(username)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return false;
}
return true;
}
bool CNLoginServer::checkToken(CNSocket* sock, Database::Account& account, std::string& token, bool isCookieAuth) {
// check for valid cookie first
if (isCookieAuth && CNLoginServer::isAuthMethodAllowed(AuthMethod::COOKIE)) {
const char *cookie = token.c_str();
if (Database::checkCookie(account.AccountID, cookie)) {
return true;
}
}
// cookie check disabled or failed; check to see if it's a plaintext password
if (CNLoginServer::isAuthMethodAllowed(AuthMethod::PASSWORD)
&& CNLoginServer::isPasswordCorrect(account.Password, token)) {
return true;
}
return false;
}
bool CNLoginServer::checkBan(CNSocket* sock, Database::Account& account) {
// check if the account is banned
if (account.BannedUntil > getTimestamp()) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
// ceiling devision
int64_t remainingDays = (account.BannedUntil-getTimestamp()) / 86400 + ((account.BannedUntil - getTimestamp()) % 86400 != 0);
std::string text = "Your account has been banned. \nReason: ";
text += account.BanReason;
text += "\nBan expires in " + std::to_string(remainingDays) + " day";
if (remainingDays > 1)
text += "s";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 99999999;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return true;
}
return false;
}
#pragma endregion #pragma endregion

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "core/Core.hpp"
#include "db/Database.hpp"
#include "Player.hpp" #include "Player.hpp"
@@ -23,7 +24,9 @@ enum class LoginError {
UPDATED_EUALA_REQUIRED = 9 UPDATED_EUALA_REQUIRED = 9
}; };
enum class LoginType { #define USE_COOKIE_FIELDS 2
enum class AuthMethod {
PASSWORD = 1, PASSWORD = 1,
COOKIE = 2 COOKIE = 2
}; };
@@ -44,12 +47,17 @@ private:
static void changeName(CNSocket* sock, CNPacketData* data); static void changeName(CNSocket* sock, CNPacketData* data);
static void duplicateExit(CNSocket* sock, CNPacketData* data); static void duplicateExit(CNSocket* sock, CNPacketData* data);
static bool isUsernameGood(std::string login); static bool isUsernameGood(std::string& login);
static bool isPasswordGood(std::string password); static bool isPasswordGood(std::string& password);
static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword); static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword);
static bool isAccountInUse(int accountId); static bool isAccountInUse(int accountId);
static bool isCharacterNameGood(std::string Firstname, std::string Lastname); static bool isCharacterNameGood(std::string Firstname, std::string Lastname);
static bool isLoginTypeAllowed(LoginType loginType); static bool isAuthMethodAllowed(AuthMethod authMethod);
static bool checkUsername(CNSocket* sock, std::string& username);
static bool checkPassword(CNSocket* sock, std::string& password);
static bool checkToken(CNSocket* sock, Database::Account& account, std::string& token, bool isCookieAuth);
static bool checkBan(CNSocket* sock, Database::Account& account);
static void newAccount(CNSocket* sock, std::string userLogin, std::string userPassword, int32_t clientVerC); static void newAccount(CNSocket* sock, std::string userLogin, std::string userPassword, int32_t clientVerC);
// returns true if success // returns true if success
static bool exitDuplicate(int accountId); static bool exitDuplicate(int accountId);

View File

@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <stdint.h>
#include <string> #include <string>
#include <time.h>
namespace settings { namespace settings {
extern int VERBOSITY; extern int VERBOSITY;