mirror of
				https://github.com/OpenFusionProject/OpenFusion.git
				synced 2025-10-26 22:30:05 +00:00 
			
		
		
		
	Compare commits
	
		
			15 Commits
		
	
	
		
			1.5.2
			...
			c4eb4a481b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c4eb4a481b | |||
| 810ccffd9e | |||
| 3c5eefd9c2 | |||
| c29899f2b9 | |||
| a38b14b79a | |||
| 52833f7fb3 | |||
|   | 3aed24de26 | ||
|   | 17362b2ea6 | ||
|   | 47dbc6d35e | ||
|   | b780f5ee60 | ||
|   | 003186d97a | ||
|   | 6d2f120305 | ||
|   | 51615db230 | ||
|   | 233d21ecd7 | ||
|   | 54327b0c23 | 
| @@ -1 +0,0 @@ | ||||
| version.h | ||||
							
								
								
									
										6
									
								
								.github/workflows/check-builds.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/check-builds.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -52,7 +52,7 @@ jobs: | ||||
|           Copy-Item -Path "config.ini" -Destination "bin" | ||||
|         shell: pwsh | ||||
|       - name: Upload build artifact | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}' | ||||
|           path: bin | ||||
| @@ -106,7 +106,7 @@ jobs: | ||||
|           } | ||||
|         shell: pwsh | ||||
|       - name: Upload build artifact | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}' | ||||
|           path: bin | ||||
| @@ -127,7 +127,7 @@ jobs: | ||||
|           GITDESC=$(git describe --tags) | ||||
|           mkdir $GITDESC | ||||
|           echo "ARTDIR=$GITDESC" >> $GITHUB_ENV | ||||
|       - uses: actions/download-artifact@v3 | ||||
|       - uses: actions/download-artifact@v4 | ||||
|         with: | ||||
|           path: ${{ env.ARTDIR }} | ||||
|       - name: Upload artifacts | ||||
|   | ||||
							
								
								
									
										38
									
								
								.github/workflows/push-docker-image.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/push-docker-image.yml
									
									
									
									
										vendored
									
									
										Normal 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 | ||||
							
								
								
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,4 +1,5 @@ | ||||
| FROM debian:latest | ||||
| # build | ||||
| FROM debian:stable-slim as build | ||||
|  | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| @@ -8,14 +9,24 @@ clang \ | ||||
| make \ | ||||
| libsqlite3-dev | ||||
|  | ||||
| COPY . ./ | ||||
| COPY src ./src | ||||
| COPY vendor ./vendor | ||||
| COPY .git ./.git | ||||
| COPY Makefile CMakeLists.txt version.h.in ./ | ||||
|  | ||||
| RUN make -j8 | ||||
|  | ||||
| # tabledata should be copied from the host; | ||||
| # clone it there before building the container | ||||
| #RUN git submodule update --init --recursive | ||||
| # prod | ||||
| FROM debian:stable-slim | ||||
|  | ||||
| CMD ["./bin/fusion"] | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| LABEL Name=openfusion Version=0.0.1 | ||||
| RUN apt-get -y update && apt-get install -y \ | ||||
| libsqlite3-dev | ||||
|  | ||||
| COPY --from=build /usr/src/app/bin/fusion /bin/fusion | ||||
| COPY sql ./sql | ||||
|  | ||||
| CMD ["/bin/fusion"] | ||||
|  | ||||
| LABEL Name=openfusion Version=0.0.2 | ||||
|   | ||||
| @@ -13,13 +13,13 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v | ||||
|  | ||||
| ### Getting Started | ||||
| #### 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.5.2/OpenFusionClient-1.5.2-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. | ||||
| 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. | ||||
|  | ||||
| #### 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.5.2/OpenFusionClient-1.5.2.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. | ||||
| 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. | ||||
| @@ -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). | ||||
|  | ||||
| ### 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.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5.2). | ||||
| 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: | ||||
|     1. For Description, enter anything you want. This is what will show up in the server list. | ||||
|   | ||||
| @@ -17,6 +17,8 @@ acceptallcustomnames=true | ||||
| # should attempts to log into non-existent accounts | ||||
| # automatically create them? | ||||
| autocreateaccounts=true | ||||
| # support logging in with auth cookies? | ||||
| useauthcookies=false | ||||
| # how often should everything be flushed to the database? | ||||
| # the default is 4 minutes | ||||
| dbsaveinterval=240 | ||||
|   | ||||
| @@ -6,6 +6,10 @@ services: | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./Dockerfile | ||||
|     volumes: | ||||
|       - ./config.ini:/usr/src/app/config.ini | ||||
|       - ./database.db:/usr/src/app/database.db | ||||
|       - ./tdata:/usr/src/app/tdata | ||||
|     ports: | ||||
|       - "23000:23000" | ||||
|       - "23001:23001" | ||||
|   | ||||
							
								
								
									
										19
									
								
								sql/migration4.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								sql/migration4.sql
									
									
									
									
									
										Normal 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, | ||||
|     Valid       INTEGER 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; | ||||
| @@ -143,7 +143,7 @@ CREATE TABLE IF NOT EXISTS EmailItems ( | ||||
|     UNIQUE (PlayerID, MsgIndex, Slot) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS RaceResults( | ||||
| CREATE TABLE IF NOT EXISTS RaceResults ( | ||||
|     EPID      INTEGER NOT NULL, | ||||
|     PlayerID  INTEGER NOT NULL, | ||||
|     Score     INTEGER NOT NULL, | ||||
| @@ -153,9 +153,17 @@ CREATE TABLE IF NOT EXISTS RaceResults( | ||||
|     FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS RedeemedCodes( | ||||
| CREATE TABLE IF NOT EXISTS RedeemedCodes ( | ||||
|     PlayerID    INTEGER NOT NULL, | ||||
|     Code        TEXT NOT NULL, | ||||
|     FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE, | ||||
|     UNIQUE (PlayerID, Code) | ||||
| ) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS Auth ( | ||||
|     AccountID   INTEGER NOT NULL, | ||||
|     Cookie      TEXT NOT NULL, | ||||
|     Valid       INTEGER DEFAULT 0 NOT NULL, | ||||
|     FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE, | ||||
|     UNIQUE (AccountID) | ||||
| ); | ||||
|   | ||||
| @@ -30,7 +30,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) { | ||||
| #pragma endregion | ||||
|  | ||||
| // Refresh buddy list | ||||
| void Buddies::refreshBuddyList(CNSocket* sock) { | ||||
| void Buddies::sendBuddyList(CNSocket* sock) { | ||||
|     Player* plr = PlayerManager::getPlayer(sock); | ||||
|     int buddyCnt = Database::getNumBuddies(plr); | ||||
|  | ||||
| @@ -277,15 +277,6 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) { | ||||
| // Getting buddy state | ||||
| static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) { | ||||
|     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); | ||||
|      | ||||
|   | ||||
| @@ -6,5 +6,5 @@ namespace Buddies { | ||||
|     void init(); | ||||
|  | ||||
|     // Buddy list | ||||
|     void refreshBuddyList(CNSocket* sock); | ||||
|     void sendBuddyList(CNSocket* sock); | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|     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) { | ||||
| @@ -1200,7 +1270,7 @@ static void registerCommand(std::string cmd, int requiredLevel, CommandHandler h | ||||
|  | ||||
| void CustomCommands::init() { | ||||
|     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("mss", 30, mssCommand, "edit Monkey Skyway routes"); | ||||
|     registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs"); | ||||
|   | ||||
| @@ -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::cout << logEmail << std::endl; | ||||
|     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() { | ||||
|   | ||||
| @@ -72,8 +72,8 @@ struct Player : public Entity, public ICombatant { | ||||
|     bool notify = false; | ||||
|     bool hidden = false; | ||||
|     bool unwarpable = false; | ||||
|     bool initialLoadDone = false; | ||||
|  | ||||
|     bool buddiesSynced = false; | ||||
|     int64_t buddyIDs[50] = {}; | ||||
|     bool isBuddyBlocked[50] = {}; | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  * 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 | ||||
|     Player *plr = getPlayer(sock); | ||||
|  | ||||
|     int16_t id = 0; | ||||
|     INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt); | ||||
|  | ||||
|     pkt.PCUID = plr->iID; | ||||
|     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) { | ||||
|         pkt.elementOffset = id; | ||||
|  | ||||
| @@ -212,6 +217,7 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) { | ||||
|  | ||||
|     response.iID = plr->iID; | ||||
|     response.uiSvrTime = getTime(); | ||||
|  | ||||
|     response.PCLoadData2CL.iUserLevel = plr->accountLevel; | ||||
|     response.PCLoadData2CL.iHP = plr->HP; | ||||
|     response.PCLoadData2CL.iLevel = plr->level; | ||||
| @@ -294,27 +300,21 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) { | ||||
|     sock->setFEKey(lm->FEKey); | ||||
|     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); | ||||
|  | ||||
|     // transmit MOTD after entering the game, so the client hopefully changes modes on time | ||||
|     Chat::sendServerMessage(sock, settings::MOTDSTRING); | ||||
|     sendNanoBook(sock, plr, false); | ||||
|  | ||||
|     // transfer ownership of Player object into the shard (still valid in this function though) | ||||
|     addPlayer(sock, plr); | ||||
|  | ||||
|     // check if there is an expiring vehicle | ||||
|     Items::checkItemExpire(sock, plr); | ||||
|  | ||||
|     // set player equip stats | ||||
|     Items::setItemStats(plr); | ||||
|  | ||||
|     Missions::failInstancedMissions(sock); | ||||
|  | ||||
|     sendNanoBookSubset(sock); | ||||
|  | ||||
|     // initial buddy sync | ||||
|     Buddies::refreshBuddyList(sock); | ||||
|  | ||||
|     for (auto& pair : players) | ||||
|         if (pair.second->notify) | ||||
|             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); | ||||
|     } | ||||
|  | ||||
|     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) { | ||||
|   | ||||
| @@ -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)); | ||||
| } | ||||
|  | ||||
| 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) { | ||||
|     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_ACCEPT, tradeOfferAccept); | ||||
|     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_CANCEL, tradeConfirmCancel); | ||||
|     REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem); | ||||
|   | ||||
| @@ -40,6 +40,7 @@ | ||||
|  | ||||
| // wrapper for U16toU8 | ||||
| #define ARRLEN(x) (sizeof(x)/sizeof(*x)) | ||||
| #define AUTOU8(x) std::string((char*)x, ARRLEN(x)) | ||||
| #define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))  | ||||
|  | ||||
| // TODO: rewrite U16toU8 & U8toU16 to not use codecvt | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| #include <string> | ||||
| #include <vector> | ||||
|  | ||||
| #define DATABASE_VERSION 4 | ||||
| #define DATABASE_VERSION 5 | ||||
|  | ||||
| namespace Database { | ||||
|  | ||||
| @@ -46,9 +46,17 @@ namespace Database { | ||||
|     void close(); | ||||
|  | ||||
|     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); | ||||
|  | ||||
|     void updateAccountLevel(int accountId, int accountLevel); | ||||
|  | ||||
|     // return true iff cookie is valid for the account. | ||||
|     // invalidates the stored cookie afterwards | ||||
|     bool checkCookie(int accountId, const char *cookie); | ||||
|  | ||||
|     // interface for the /ban command | ||||
|     bool banPlayer(int playerId, std::string& reason); | ||||
|     bool unbanPlayer(int playerId); | ||||
|   | ||||
| @@ -27,6 +27,32 @@ void Database::findAccount(Account* account, std::string login) { | ||||
|     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) { | ||||
|     std::lock_guard<std::mutex> lock(dbCrit); | ||||
|  | ||||
| @@ -52,6 +78,73 @@ int Database::addAccount(std::string login, std::string password) { | ||||
|     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 Valid = 1; | ||||
|         )"; | ||||
|  | ||||
|     const char* sql_invalidate = R"( | ||||
|         UPDATE Auth | ||||
|         SET Valid = 0 | ||||
|         WHERE AccountID = ?; | ||||
|         )"; | ||||
|  | ||||
|     sqlite3_stmt* stmt; | ||||
|  | ||||
|     sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL); | ||||
|     sqlite3_bind_int(stmt, 1, accountId); | ||||
|     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 consumeCookie(): " << sqlite3_errmsg(db) << std::endl; | ||||
|  | ||||
|     return match; | ||||
| } | ||||
|  | ||||
| void Database::updateSelected(int accountId, int slot) { | ||||
|     std::lock_guard<std::mutex> lock(dbCrit); | ||||
|  | ||||
|   | ||||
| @@ -105,57 +105,81 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) { | ||||
|  | ||||
| void CNLoginServer::login(CNSocket* sock, CNPacketData* data) { | ||||
|     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); | ||||
|     bool isCookieAuth = login->iLoginType == 2; | ||||
|  | ||||
|     std::string userLogin; | ||||
|     std::string userPassword; | ||||
|  | ||||
|     /* | ||||
|      * Sometimes the client sends garbage cookie data. | ||||
|      * Validate it as normal credentials instead of using a length check before falling back. | ||||
|      * The std::string -> char* -> std::string maneuver should remove any | ||||
|      * trailing garbage after the null terminator. | ||||
|      */ | ||||
|     if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) { | ||||
|         /* | ||||
|          * The std::string -> char* -> std::string maneuver should remove any | ||||
|          * trailing garbage after the null terminator. | ||||
|          */ | ||||
|     if (isCookieAuth) { | ||||
|         // username encoded in TEGid raw | ||||
|         userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str()); | ||||
|  | ||||
|         // N.B. clients that use web login without proper cookies | ||||
|         // send their passwords in the cookie field | ||||
|         userPassword = std::string(AUTOU8(login->szCookie_authid).c_str()); | ||||
|     } else { | ||||
|         userLogin = std::string(AUTOU16TOU8(login->szID).c_str()); | ||||
|         userPassword = 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 | ||||
|     // (not at the start or the end of the password field) | ||||
|     if (int(userPassword.find("\n")) > 0) | ||||
|         userPassword.erase(userPassword.find("\n"), 1); | ||||
|     if (!settings::USEAUTHCOOKIES) { | ||||
|         // use normal login flow | ||||
|         isCookieAuth = false; | ||||
|     } | ||||
|  | ||||
|     // check regex | ||||
|     if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) { | ||||
|     // check username regex | ||||
|     if (!CNLoginServer::isUsernameGood(userLogin)) { | ||||
|         // send a custom error message | ||||
|         INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg); | ||||
|         std::string text = "Invalid login or password\n"; | ||||
|         text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore\n"; | ||||
|         text += "Password has to be 8 - 32 characters long";           | ||||
|         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 = 15; | ||||
|         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); | ||||
|     } | ||||
|  | ||||
|     // check password regex if not cookie auth | ||||
|     if (!isCookieAuth && !CNLoginServer::isPasswordGood(userPassword)) { | ||||
|         // 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::findAccount(&findUser, userLogin); | ||||
|      | ||||
|     // account was not found | ||||
|     if (findUser.AccountID == 0) { | ||||
|         if (settings::AUTOCREATEACCOUNTS) | ||||
|         // don't auto-create an account if it's a cookie auth for whatever reason | ||||
|         if (settings::AUTOCREATEACCOUNTS && !isCookieAuth) | ||||
|             return newAccount(sock, userLogin, userPassword, login->iClientVerC); | ||||
|  | ||||
|         return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock); | ||||
|     } | ||||
|  | ||||
|     if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword)) | ||||
|         return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock); | ||||
|     if (isCookieAuth) { | ||||
|         const char *cookie = userPassword.c_str(); | ||||
|         if (!Database::checkCookie(findUser.AccountID, cookie)) | ||||
|             return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock); | ||||
|     } else { | ||||
|         // simple password check | ||||
|         if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword)) | ||||
|             return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock); | ||||
|     } | ||||
|  | ||||
|     // is the account banned | ||||
|     if (findUser.BannedUntil > getTimestamp()) { | ||||
| @@ -621,11 +645,14 @@ bool CNLoginServer::exitDuplicate(int accountId) { | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| bool CNLoginServer::isLoginDataGood(std::string login, std::string password) { | ||||
|     std::regex loginRegex("[a-zA-Z0-9_-]{4,32}"); | ||||
|     std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}"); | ||||
| bool CNLoginServer::isUsernameGood(std::string login) { | ||||
|     const std::regex loginRegex("[a-zA-Z0-9_-]{4,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) { | ||||
|   | ||||
| @@ -39,7 +39,8 @@ private: | ||||
|     static void changeName(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 isAccountInUse(int accountId); | ||||
|     static bool isCharacterNameGood(std::string Firstname, std::string Lastname); | ||||
|   | ||||
| @@ -13,6 +13,7 @@ bool settings::SANDBOX = true; | ||||
| int settings::LOGINPORT = 23000; | ||||
| bool settings::APPROVEALLNAMES = true; | ||||
| bool settings::AUTOCREATEACCOUNTS = true; | ||||
| bool settings::USEAUTHCOOKIES = false; | ||||
| int settings::DBSAVEINTERVAL = 240; | ||||
|  | ||||
| int settings::SHARDPORT = 23001; | ||||
| @@ -87,6 +88,7 @@ void settings::init() { | ||||
|     LOGINPORT = reader.GetInteger("login", "port", LOGINPORT); | ||||
|     APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES); | ||||
|     AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS); | ||||
|     USEAUTHCOOKIES = reader.GetBoolean("login", "useauthcookies", USEAUTHCOOKIES); | ||||
|     DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL); | ||||
|     SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT); | ||||
|     SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ namespace settings { | ||||
|     extern int LOGINPORT; | ||||
|     extern bool APPROVEALLNAMES; | ||||
|     extern bool AUTOCREATEACCOUNTS; | ||||
|     extern bool USEAUTHCOOKIES; | ||||
|     extern int DBSAVEINTERVAL; | ||||
|     extern int SHARDPORT; | ||||
|     extern std::string SHARDSERVERIP; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user