17 Commits

Author SHA1 Message Date
Juansecu
b3d8a47777 Merge 2096c3c3cc into 397700e909 2024-10-07 12:38:45 +03:00
CakeLancelot
397700e909 Update version numbers in README 2024-10-06 23:26:36 -05:00
d9b6aedd5b Fix nested ifdefs for windows 2024-10-06 20:58:20 -07:00
CakeLancelot
145113062b Update tdata reference to fix invalid label 2024-10-05 16:51:39 -05:00
d717c5d74d Update tdata reference for the car paths 2024-09-22 04:28:28 +02:00
a6eb0e2349 Auth Cookie Support (#285)
* Auth cookie support

* Add config option for auth cookie support

* Safe handling of TEGid/auth_id strings

* Fix bad size calculation due to pointer cast

* Expiration timestamp instead of valid bit

* Change setting to "allowed auth methods"

This allows plaintext password auth to be disabled altogether

* PR feedback
2024-09-17 20:41:48 -07:00
52833f7fb3 Fix CI/CD 2024-09-05 13:43:57 -04:00
CakeLancelot
3aed24de26 Update download links in Getting Started 2024-07-14 18:52:38 -05:00
Gent Semaj
17362b2ea6 Client synchronization improvements (#283)
* Client synchronization improvements

* Remove bad comment

* Remove guard on PC_TICK

* Fix delayed loading of nano skill icons

We actually don't need to wait for post-load to do the second nano book send.
That adds unnecessary delay. Moving it to right after `P_FE2CL_REP_PC_ENTER_SUCC`
does the trick and gives the client plenty of time to fetch the icons before
loading in-game.

* Don't send unnecessary nano book subsets pre-enter

* Fix comment
2024-06-27 08:19:54 -05:00
gsemaj
47dbc6d35e Notify if player must log out and back in for access change 2024-06-23 18:41:43 -07:00
Gent Semaj
b780f5ee60 Enable account level changing at runtime (#282)
* Enable account level change at runtime

* PR feedback
2024-06-23 20:25:46 -05:00
Gent Semaj
003186d97a Immediate email notifications (#281) 2024-06-23 10:32:22 -05:00
Gent Semaj
6d2f120305 Add missing trade offer packets (#280) 2024-06-22 18:20:59 -05:00
Juansecu
2096c3c3cc BUILD - Optimize Dockerfile by using alpine as base image 2024-06-08 21:55:03 -05:00
Juansecu
51615db230 CD - Add GitHub Action to push Docker image to Docker Hub (#275)
* CD - Add GitHub Action to push Docker image to Docker Hub

* BUILD - Reduce size of image by using slim version of Debian

* CI - Modify Push Docker Image action to build and push Docker image to linux/amd64 and linux/arm64 architectures
2024-05-15 10:17:33 -05:00
CakeLancelot
233d21ecd7 Fix copy/paste error in docker-compose.yml 2024-05-07 23:01:09 -05:00
Gent Semaj
54327b0c23 Docker improvements (#274) 2024-05-07 22:11:14 -05:00
25 changed files with 456 additions and 87 deletions

View File

@@ -1 +0,0 @@
version.h

View File

@@ -52,7 +52,7 @@ jobs:
Copy-Item -Path "config.ini" -Destination "bin" Copy-Item -Path "config.ini" -Destination "bin"
shell: pwsh shell: pwsh
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}' name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
path: bin path: bin
@@ -106,7 +106,7 @@ jobs:
} }
shell: pwsh shell: pwsh
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}' name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}'
path: bin path: bin
@@ -127,7 +127,7 @@ jobs:
GITDESC=$(git describe --tags) GITDESC=$(git describe --tags)
mkdir $GITDESC mkdir $GITDESC
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
path: ${{ env.ARTDIR }} path: ${{ env.ARTDIR }}
- name: Upload artifacts - name: Upload artifacts

38
.github/workflows/push-docker-image.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Push Docker Image
on:
release:
types: [published]
jobs:
push-docker-image:
name: Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
platforms:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@v4
- name: Retrieve major version
uses: winterjung/split@v2
id: split
with:
msg: ${{ github.ref_name }}
separator: .
- name: Log in to registry
uses: docker/login-action@v3
with:
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
- name: Build and push the Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest

View File

@@ -1,21 +1,37 @@
FROM debian:latest # 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 . ./ COPY src ./src
COPY vendor ./vendor
COPY .git ./.git
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 -j8 RUN make -j8
# tabledata should be copied from the host; # prod
# clone it there before building the container FROM alpine:3
#RUN git submodule update --init --recursive
CMD ["./bin/fusion"] WORKDIR /usr/src/app
LABEL Name=openfusion Version=0.0.1 RUN apk update && apk upgrade && apk add \
libstdc++ \
sqlite-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
COPY sql ./sql
CMD ["/bin/fusion"]
LABEL Name=openfusion Version=0.0.2

View File

@@ -13,13 +13,13 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
### Getting Started ### Getting Started
#### Method A: Installer (Easiest) #### Method A: Installer (Easiest)
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5-Installer.exe) - choose to run the file. 1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.6/OpenFusionClient-1.6-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect. 2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen. 3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3. 4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
#### Method B: Standalone .zip file #### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5.zip). 1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.6/OpenFusionClient-1.6.zip).
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install. 2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect. 3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen. 4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
@@ -28,7 +28,7 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux). Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
### Hosting a server ### Hosting a server
1. Grab `OpenFusionServer-1.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5). 1. Grab `OpenFusionServer-1.6-Original.zip` or `OpenFusionServer-1.6-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.6).
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server. 2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
3. Add a new server to the client's list: 3. Add a new server to the client's list:
1. For Description, enter anything you want. This is what will show up in the server list. 1. For Description, enter anything you want. This is what will show up in the server list.

View File

@@ -17,6 +17,10 @@ acceptallcustomnames=true
# should attempts to log into non-existent accounts # should attempts to log into non-existent accounts
# automatically create them? # automatically create them?
autocreateaccounts=true autocreateaccounts=true
# list of supported authentication methods (comma-separated)
# password = allow login type 1 with plaintext passwords
# cookie = allow login type 2 with one-shot auth cookies
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
dbsaveinterval=240 dbsaveinterval=240

View File

@@ -6,6 +6,10 @@ services:
build: build:
context: . context: .
dockerfile: ./Dockerfile dockerfile: ./Dockerfile
volumes:
- ./config.ini:/usr/src/app/config.ini
- ./database.db:/usr/src/app/database.db
- ./tdata:/usr/src/app/tdata
ports: ports:
- "23000:23000" - "23000:23000"
- "23001:23001" - "23001:23001"

19
sql/migration4.sql Normal file
View File

@@ -0,0 +1,19 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- New table to store auth cookies
CREATE TABLE Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);
-- Update DB Version
UPDATE Meta SET Value = 5 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -158,4 +158,12 @@ CREATE TABLE IF NOT EXISTS RedeemedCodes(
Code TEXT NOT NULL, Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE, FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code) UNIQUE (PlayerID, Code)
) );
CREATE TABLE IF NOT EXISTS Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);

View File

@@ -30,7 +30,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) {
#pragma endregion #pragma endregion
// Refresh buddy list // Refresh buddy list
void Buddies::refreshBuddyList(CNSocket* sock) { void Buddies::sendBuddyList(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
int buddyCnt = Database::getNumBuddies(plr); int buddyCnt = Database::getNumBuddies(plr);
@@ -278,15 +278,6 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) { static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
/*
* If the buddy list wasn't synced a second time yet, sync it.
* Not sure why we have to do it again for the client not to trip up.
*/
if (!plr->buddiesSynced) {
refreshBuddyList(sock);
plr->buddiesSynced = true;
}
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
for (int slot = 0; slot < 50; slot++) { for (int slot = 0; slot < 50; slot++) {

View File

@@ -6,5 +6,5 @@ namespace Buddies {
void init(); void init();
// Buddy list // Buddy list
void refreshBuddyList(CNSocket* sock); void sendBuddyList(CNSocket* sock);
} }

View File

@@ -83,7 +83,77 @@ static void helpCommand(std::string full, std::vector<std::string>& args, CNSock
} }
static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Chat::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel)); if (args.size() < 2) {
Chat::sendServerMessage(sock, "Usage: /access <id> [new_level]");
Chat::sendServerMessage(sock, "Use . for id to select yourself");
return;
}
char *tmp;
Player* self = PlayerManager::getPlayer(sock);
int selfAccess = self->accountLevel;
Player* player;
if (args[1].compare(".") == 0) {
player = self;
} else {
int id = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid player ID " + args[1]);
return;
}
player = PlayerManager::getPlayerFromID(id);
if (player == nullptr) {
Chat::sendServerMessage(sock, "Could not find player with ID " + std::to_string(id));
return;
}
// Messing with other players requires a baseline access of 30
if (player != self && selfAccess > 30) {
Chat::sendServerMessage(sock, "Can't check or change other players access levels (insufficient privileges)");
return;
}
}
std::string playerName = PlayerManager::getPlayerName(player);
int currentAccess = player->accountLevel;
if (args.size() < 3) {
// just check
Chat::sendServerMessage(sock, playerName + " has access level " + std::to_string(currentAccess));
return;
}
// Can't change the access level of someone with stronger privileges
// N.B. lower value = stronger privileges
if (currentAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can't change this player's access level (insufficient privileges)");
return;
}
int newAccess = std::strtol(args[2].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid access level " + args[2]);
return;
}
// Can only assign an access level weaker than yours
if (newAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can only assign privileges weaker than your own");
return;
}
player->accountLevel = newAccess;
// Save to database
int accountId = Database::getAccountIdForPlayer(player->iID);
Database::updateAccountLevel(accountId, newAccess);
std::string msg = "Changed access level for " + playerName + " from " + std::to_string(currentAccess) + " to " + std::to_string(newAccess);
if (newAccess <= 50 && currentAccess > 50)
msg += " (they must log out and back in for some commands to be enabled)";
Chat::sendServerMessage(sock, msg);
} }
static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -1200,7 +1270,7 @@ static void registerCommand(std::string cmd, int requiredLevel, CommandHandler h
void CustomCommands::init() { void CustomCommands::init() {
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands"); registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
registerCommand("access", 100, accessCommand, "print your access level"); registerCommand("access", 100, accessCommand, "check or change access levels");
registerCommand("instance", 30, instanceCommand, "print or change your current instance"); registerCommand("instance", 30, instanceCommand, "print or change your current instance");
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes"); registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs"); registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");

View File

@@ -325,6 +325,13 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody; std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
std::cout << logEmail << std::endl; std::cout << logEmail << std::endl;
dump.push_back(logEmail); dump.push_back(logEmail);
// notification to recipient if online
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
if (recipient != nullptr)
{
emailUpdateCheck(recipient, nullptr);
}
} }
void Email::init() { void Email::init() {

View File

@@ -72,8 +72,8 @@ struct Player : public Entity, public ICombatant {
bool notify = false; bool notify = false;
bool hidden = false; bool hidden = false;
bool unwarpable = false; bool unwarpable = false;
bool initialLoadDone = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {}; int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {}; bool isBuddyBlocked[50] = {};

View File

@@ -155,16 +155,21 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z) {
* Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted * Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted
* for in these packets, even if the player hasn't unlocked them. * for in these packets, even if the player hasn't unlocked them.
*/ */
static void sendNanoBookSubset(CNSocket *sock) { static void sendNanoBook(CNSocket *sock, Player *plr, bool resizeOnly) {
#ifdef ACADEMY #ifdef ACADEMY
Player *plr = getPlayer(sock);
int16_t id = 0; int16_t id = 0;
INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt); INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt);
pkt.PCUID = plr->iID; pkt.PCUID = plr->iID;
pkt.bookSize = NANO_COUNT; pkt.bookSize = NANO_COUNT;
if (resizeOnly) {
// triggers nano array resizing without
// actually sending nanos
sock->sendPacket(pkt, P_FE2CL_REP_NANO_BOOK_SUBSET);
return;
}
while (id < NANO_COUNT) { while (id < NANO_COUNT) {
pkt.elementOffset = id; pkt.elementOffset = id;
@@ -212,6 +217,7 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
response.iID = plr->iID; response.iID = plr->iID;
response.uiSvrTime = getTime(); response.uiSvrTime = getTime();
response.PCLoadData2CL.iUserLevel = plr->accountLevel; response.PCLoadData2CL.iUserLevel = plr->accountLevel;
response.PCLoadData2CL.iHP = plr->HP; response.PCLoadData2CL.iHP = plr->HP;
response.PCLoadData2CL.iLevel = plr->level; response.PCLoadData2CL.iLevel = plr->level;
@@ -294,27 +300,21 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
sock->setFEKey(lm->FEKey); sock->setFEKey(lm->FEKey);
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on
// Academy builds receive nanos in a separate packet. An initial one with the size of the
// nano book needs to be sent before PC_ENTER_SUCC so the client can resize its nano arrays,
// and then proper packets with the nanos included must be sent after, while the game is loading.
sendNanoBook(sock, plr, true);
sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC); sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
// transmit MOTD after entering the game, so the client hopefully changes modes on time sendNanoBook(sock, plr, false);
Chat::sendServerMessage(sock, settings::MOTDSTRING);
// transfer ownership of Player object into the shard (still valid in this function though) // transfer ownership of Player object into the shard (still valid in this function though)
addPlayer(sock, plr); addPlayer(sock, plr);
// check if there is an expiring vehicle
Items::checkItemExpire(sock, plr);
// set player equip stats // set player equip stats
Items::setItemStats(plr); Items::setItemStats(plr);
Missions::failInstancedMissions(sock);
sendNanoBookSubset(sock);
// initial buddy sync
Buddies::refreshBuddyList(sock);
for (auto& pair : players) for (auto& pair : players)
if (pair.second->notify) if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined."); Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined.");
@@ -377,6 +377,17 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO); sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
} }
if (!plr->initialLoadDone) {
// these should be called only once, but not until after
// first load-in or else the client may ignore the packets
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
Missions::failInstancedMissions(sock); // auto-fail missions
Buddies::sendBuddyList(sock); // buddy list
Items::checkItemExpire(sock, plr); // vehicle expiration
plr->initialLoadDone = true;
}
} }
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) { static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {

View File

@@ -186,6 +186,36 @@ static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL)); otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
} }
static void tradeOfferCancel(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL));
}
static void tradeOfferAbort(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT));
}
static void tradeConfirm(CNSocket* sock, CNPacketData* data) { static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf; sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
@@ -430,6 +460,8 @@ void Trading::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL, tradeOfferCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT, tradeOfferAbort);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);

View File

@@ -40,6 +40,7 @@
// wrapper for U16toU8 // wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x)) #define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU8(x) std::string((char*)x, ARRLEN(x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x)) #define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt // TODO: rewrite U16toU8 & U8toU16 to not use codecvt

View File

@@ -5,7 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#define DATABASE_VERSION 4 #define DATABASE_VERSION 5
namespace Database { namespace Database {
@@ -46,9 +46,17 @@ namespace Database {
void close(); void close();
void findAccount(Account* account, std::string login); void findAccount(Account* account, std::string login);
// returns ID, 0 if something failed
// return ID, 0 if something failed
int getAccountIdForPlayer(int playerId);
int addAccount(std::string login, std::string password); int addAccount(std::string login, std::string password);
void updateAccountLevel(int accountId, int accountLevel);
// return true if cookie is valid for the account.
// invalidates the stored cookie afterwards
bool checkCookie(int accountId, const char *cookie);
// interface for the /ban command // interface for the /ban command
bool banPlayer(int playerId, std::string& reason); bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId); bool unbanPlayer(int playerId);

View File

@@ -27,6 +27,32 @@ void Database::findAccount(Account* account, std::string login) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
int Database::getAccountIdForPlayer(int playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT AccountID
FROM Players
WHERE PlayerID = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
std::cout << "[WARN] Database: couldn't get account id for player " << playerId << std::endl;
sqlite3_finalize(stmt);
return 0;
}
int accountId = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return accountId;
}
int Database::addAccount(std::string login, std::string password) { int Database::addAccount(std::string login, std::string password) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -52,6 +78,75 @@ int Database::addAccount(std::string login, std::string password) {
return sqlite3_last_insert_rowid(db); return sqlite3_last_insert_rowid(db);
} }
void Database::updateAccountLevel(int accountId, int accountLevel) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
AccountLevel = ?
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountLevel);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateAccountLevel(): " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
bool Database::checkCookie(int accountId, const char *tryCookie) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql_get = R"(
SELECT Cookie
FROM Auth
WHERE AccountID = ? AND Expires > ?;
)";
const char* sql_invalidate = R"(
UPDATE Auth
SET Expires = 0
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
sqlite3_bind_int(stmt, 2, getTimestamp());
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return false;
}
const char *cookie = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (strlen(cookie) != strlen(tryCookie)) {
sqlite3_finalize(stmt);
return false;
}
/*
* 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_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;
}
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

@@ -105,57 +105,95 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) {
void CNLoginServer::login(CNSocket* sock, CNPacketData* data) { void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
auto login = (sP_CL2LS_REQ_LOGIN*)data->buf; auto login = (sP_CL2LS_REQ_LOGIN*)data->buf;
// TODO: implement better way of sending credentials
std::string userLogin((char*)login->szCookie_TEGid);
std::string userPassword((char*)login->szCookie_authid);
/* std::string userLogin;
* Sometimes the client sends garbage cookie data. std::string userToken; // could be password or auth cookie
* Validate it as normal credentials instead of using a length check before falling back.
*/
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
/* /*
* 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) {
userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str());
userToken = std::string(AUTOU8(login->szCookie_authid).c_str());
} else {
userLogin = std::string(AUTOU16TOU8(login->szID).c_str()); userLogin = std::string(AUTOU16TOU8(login->szID).c_str());
userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str()); userToken = std::string(AUTOU16TOU8(login->szPassword).c_str());
} }
// the client inserts a "\n" in the password if you press enter key in the middle of the password // check username regex
// (not at the start or the end of the password field) if (!CNLoginServer::isUsernameGood(userLogin)) {
if (int(userPassword.find("\n")) > 0)
userPassword.erase(userPassword.find("\n"), 1);
// check regex
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
// send a custom error message // send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg); INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login or password\n"; 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\n"; text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg)); U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 15; msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE); sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock // 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) {
// bail if password auth isn't allowed
if (!CNLoginServer::isLoginTypeAllowed(LoginType::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);
// 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);
}
}
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) {
if (settings::AUTOCREATEACCOUNTS) // don't auto-create an account if it's a cookie auth for whatever reason
return newAccount(sock, userLogin, userPassword, login->iClientVerC); if (settings::AUTOCREATEACCOUNTS && !isCookieAuth)
return newAccount(sock, userLogin, userToken, login->iClientVerC);
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock); return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
} }
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword)) if (isCookieAuth) {
const char *cookie = userToken.c_str();
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 // is the account banned
if (findUser.BannedUntil > getTimestamp()) { if (findUser.BannedUntil > getTimestamp()) {
@@ -621,11 +659,14 @@ bool CNLoginServer::exitDuplicate(int accountId) {
return false; return false;
} }
bool CNLoginServer::isLoginDataGood(std::string login, std::string password) { bool CNLoginServer::isUsernameGood(std::string login) {
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}"); const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}"); return (std::regex_match(login, loginRegex));
}
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex)); bool CNLoginServer::isPasswordGood(std::string password) {
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(password, passwordRegex));
} }
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) { bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) {
@@ -638,4 +679,17 @@ bool CNLoginServer::isCharacterNameGood(std::string Firstname, std::string Lastn
std::regex lastnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)"); std::regex lastnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)");
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) {
// the config file specifies "comma-separated" but tbh we don't care
switch (loginType) {
case LoginType::PASSWORD:
return settings::AUTHMETHODS.find("password") != std::string::npos;
case LoginType::COOKIE:
return settings::AUTHMETHODS.find("cookie") != std::string::npos;
default:
break;
}
return false;
}
#pragma endregion #pragma endregion

View File

@@ -23,6 +23,11 @@ enum class LoginError {
UPDATED_EUALA_REQUIRED = 9 UPDATED_EUALA_REQUIRED = 9
}; };
enum class LoginType {
PASSWORD = 1,
COOKIE = 2
};
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static // WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
class CNLoginServer : public CNServer { class CNLoginServer : public CNServer {
private: private:
@@ -39,10 +44,12 @@ 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 isLoginDataGood(std::string login, std::string password); static bool isUsernameGood(std::string login);
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 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

@@ -13,6 +13,7 @@ bool settings::SANDBOX = true;
int settings::LOGINPORT = 23000; int settings::LOGINPORT = 23000;
bool settings::APPROVEALLNAMES = true; bool settings::APPROVEALLNAMES = true;
bool settings::AUTOCREATEACCOUNTS = true; bool settings::AUTOCREATEACCOUNTS = true;
std::string settings::AUTHMETHODS = "password";
int settings::DBSAVEINTERVAL = 240; int settings::DBSAVEINTERVAL = 240;
int settings::SHARDPORT = 23001; int settings::SHARDPORT = 23001;
@@ -87,6 +88,7 @@ void settings::init() {
LOGINPORT = reader.GetInteger("login", "port", LOGINPORT); LOGINPORT = reader.GetInteger("login", "port", LOGINPORT);
APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES); APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES);
AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS); AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS);
AUTHMETHODS = reader.Get("login", "authmethods", AUTHMETHODS);
DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL); DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL);
SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT); SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT);
SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP); SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP);

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <stdint.h>
#include <string> #include <string>
namespace settings { namespace settings {
@@ -8,12 +9,13 @@ namespace settings {
extern int LOGINPORT; extern int LOGINPORT;
extern bool APPROVEALLNAMES; extern bool APPROVEALLNAMES;
extern bool AUTOCREATEACCOUNTS; extern bool AUTOCREATEACCOUNTS;
extern std::string AUTHMETHODS;
extern int DBSAVEINTERVAL; extern int DBSAVEINTERVAL;
extern int SHARDPORT; extern int SHARDPORT;
extern std::string SHARDSERVERIP; extern std::string SHARDSERVERIP;
extern bool LOCALHOSTWORKAROUND; extern bool LOCALHOSTWORKAROUND;
extern bool ANTICHEAT; extern bool ANTICHEAT;
extern time_t TIMEOUT; extern int64_t TIMEOUT;
extern int VIEWDISTANCE; extern int VIEWDISTANCE;
extern bool SIMULATEMOBS; extern bool SIMULATEMOBS;
extern int SPAWN_X; extern int SPAWN_X;

2
tdata

Submodule tdata updated: 8c98c83682...bdb611b092

View File

@@ -22,13 +22,14 @@
#endif #endif
#include <errno.h> #include <errno.h>
#if defined(_WIN32) || defined(_WIN64)
// On windows we need to generate random bytes differently.
#if defined(_WIN32) && !defined(_WIN64) #if defined(_WIN32) && !defined(_WIN64)
typedef __int32 ssize_t; typedef __int32 ssize_t;
#elif defined(_WIN32) && defined(_WIN64) #elif defined(_WIN32) && defined(_WIN64)
typedef __int64 ssize_t; typedef __int64 ssize_t;
#endif #endif
#if defined(_WIN32) || defined(_WIN64)
// On windows we need to generate random bytes differently.
#define BCRYPT_HASHSIZE 60 #define BCRYPT_HASHSIZE 60
#include "bcrypt.h" #include "bcrypt.h"