├── .cargo └── config.toml ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── rpcn.cfg ├── rustfmt.toml ├── scoreboards.cfg ├── server_redirs.cfg ├── servers.cfg ├── src ├── main.rs ├── server.rs └── server │ ├── client.rs │ ├── client │ ├── cmd_account.rs │ ├── cmd_admin.rs │ ├── cmd_friend.rs │ ├── cmd_misc.rs │ ├── cmd_room.rs │ ├── cmd_room_gui.rs │ ├── cmd_score.rs │ ├── cmd_server.rs │ ├── cmd_session.rs │ ├── cmd_tus.rs │ ├── notifications.rs │ └── ticket.rs │ ├── database.rs │ ├── database │ ├── db_score.rs │ └── db_tus.rs │ ├── game_tracker.rs │ ├── gui_room_manager.rs │ ├── room_manager.rs │ ├── score_cache.rs │ ├── stream_extractor.rs │ ├── stream_extractor │ ├── fb_helpers.rs │ ├── np2_structs.fbs │ └── np2_structs_generated.rs │ ├── udp_server.rs │ └── utils.rs └── ticket_public.pem /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # 64 bit MSVC 2 | [target.x86_64-pc-windows-msvc] 3 | rustflags = [ 4 | "-C", "link-arg=/STACK:8000000" 5 | ] 6 | 7 | # 64 bit Mingw 8 | [target.x86_64-pc-windows-gnu] 9 | rustflags = [ 10 | "-C", "link-arg=-Wl,--stack,8000000" 11 | ] 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: Swatinem/rust-cache@v2 11 | - name: Build 12 | run: cargo build --release --verbose 13 | - name: Run tests 14 | run: cargo test --release --verbose 15 | - run: mkdir artifact 16 | - run: cp target/release/rpcn artifact/rpcn 17 | - run: cp rpcn.cfg artifact/rpcn.cfg 18 | - run: cp servers.cfg artifact/servers.cfg 19 | - run: cp server_redirs.cfg artifact/server_redirs.cfg 20 | - run: cp scoreboards.cfg artifact/scoreboards.cfg 21 | - name: Create artifact 22 | run: cd artifact && tar -zcvf ../rpcn-linux.tar.gz ./ && cd .. 23 | - name: Upload artifact 24 | uses: actions/upload-artifact@v4.0.0 25 | with: 26 | name: rpcn 27 | path: rpcn-linux.tar.gz 28 | compression-level: 9 29 | - name: Release 30 | uses: softprops/action-gh-release@v1 31 | if: startsWith(github.ref, 'refs/tags/') 32 | with: 33 | files: rpcn-linux.tar.gz 34 | 35 | build-win: 36 | runs-on: windows-latest 37 | steps: 38 | - uses: actions/checkout@v1 39 | - uses: Swatinem/rust-cache@v2 40 | - name: Build 41 | run: cargo build --release --verbose 42 | - name: Run tests 43 | run: cargo test --release --verbose 44 | - run: mkdir artifact-win 45 | - run: copy target/release/rpcn.exe artifact-win/rpcn.exe 46 | - run: copy rpcn.cfg artifact-win/rpcn.cfg 47 | - run: copy servers.cfg artifact-win/servers.cfg 48 | - run: copy server_redirs.cfg artifact-win/server_redirs.cfg 49 | - run: copy scoreboards.cfg artifact-win/scoreboards.cfg 50 | - name: Create artifact 51 | run: Compress-Archive -Path artifact-win/* -Destination rpcn-win.zip 52 | - name: Upload artifact 53 | uses: actions/upload-artifact@v4.0.0 54 | with: 55 | name: rpcn-win 56 | path: rpcn-win.zip 57 | compression-level: 9 58 | - name: Release 59 | uses: softprops/action-gh-release@v1 60 | if: startsWith(github.ref, 'refs/tags/') 61 | with: 62 | files: rpcn-win.zip 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # Other 9 | /.vscode/** 10 | /db/** 11 | ticket_private.pem 12 | key.pem 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.4.2] - 2025-02-21 9 | 10 | ### Fixed 11 | 12 | - Fixed Notification to target of RequestSignalingInfos sending the wrong IP information 13 | - CookiePLMonster: Fixed scoreboard ordering for Super Hang-On 14 | 15 | ### Misc 16 | 17 | - Updated dependencies 18 | 19 | 20 | ## [1.4.1] - 2025-02-12 21 | 22 | ### Fixed 23 | 24 | - Use original config to hash new accounts 25 | 26 | 27 | ## [1.4.0] - 2025-02-11 28 | 29 | ### Added 30 | 31 | - Implemented support for private slots and room passwords 32 | - Added hint for game name in game tracker 33 | - Added a cache to game tracker 34 | - Added support for IPv6 for signaling 35 | - Added support for groups 36 | - Added a command to reset client's state without disconnecting 37 | 38 | ### Changed 39 | 40 | - Removed target from logging 41 | - Stopped printing error if connection stops because of TCP connection termination 42 | - Got rid of unnecessary Arcs around atomics in game tracker 43 | - Cleaned up logging 44 | 45 | ### Fixed 46 | 47 | - Signaling information is now given out to the person joining a room in the room response and in the notification that a user has joined instead of separately 48 | - Fixed Presence not being announced to friends when first set 49 | 50 | ### Misc 51 | 52 | - Updated dependencies to all latest major versions 53 | 54 | 55 | ## [1.3.0] - 2024-09-19 56 | 57 | ### Added 58 | 59 | - Added a configuration setting letting you set admins rights for specific usernames(set on launch or at account creation) 60 | - Added commands to support old GUI API (CreateRoomGUI, JoinRoomGUI, LeaveRoomGUI, GetRoomListGUI, SetRoomSearchFlagGUI, GetRoomSearchFlagGUI, SetRoomInfoGUI, GetRoomInfoGUI, QuickMatchGUI, SearchJoinRoomGUI) 61 | 62 | ### Changed 63 | 64 | - Changed the default ranking limit(number of scores ranked per table) from 100 to 250 65 | - Ticket issuer ID was changed to 0x100(from 0x33333333) as Tony Hawk: SHRED was found to be checking this value 66 | 67 | ### Fixed 68 | 69 | - Allow all messages but invites to be sent to non-friends 70 | - Fixed score cache last insertion/update time not being updated on insert 71 | - Fixed SceNpCommunicationId to reflect the sub_id 72 | - Fixed startIndex in Matching2 search requests not being interpreted and being set wrong in the reply 73 | - Fixed user_rooms not being appropriately updated when removed from the room forcefully(ie room destroyed) 74 | - Fixed SCE_NP_MATCHING2_ROOMMEMBER_FLAG_ATTR_OWNER not being set for the member becoming the new owner if the succession didn't go through the list 75 | - Ninetime: added servers for all Arcsys games so they should work now(BBCT, BBCSE, BBCPE, BBCF, P4A, P4AU, UNIEL) and for a few other games(Outrun Online Arcade, SCV, KOF13) 76 | - CookiePLMonster: Fixed scoreboards ordering/rules for RR7, Crazy Taxi, GTHD, Daytona USA, Wrecked, Hotline Miami 2 77 | 78 | ### Misc 79 | 80 | - Updated dependencies 81 | 82 | 83 | ## [1.2.4] - 2024-03-23 84 | 85 | ### Fixed 86 | 87 | - Fixed delete TUS data queries 88 | 89 | 90 | ## [1.2.3] - 2024-03-20 91 | 92 | ### Fixed 93 | 94 | - Fixed some TUS queries 95 | 96 | ### Misc 97 | 98 | - Updated dependencies 99 | 100 | 101 | ## [1.2.2] - 2024-03-11 102 | 103 | ### Fixed 104 | 105 | - Fixed some TUS queries 106 | 107 | ### Misc 108 | 109 | - Updated dependencies 110 | 111 | 112 | ## [1.2.1] - 2024-02-25 113 | 114 | ### Fixed 115 | 116 | - Fixed some invalid SQL requests in db_tus.rs 117 | - Fixed RETURNING queries(UPSERT aborts on constraint fail) 118 | - Ninetime: Added a server for DBZ Battle of Z Invitation mode 119 | 120 | 121 | ## [1.2.0] - 2024-02-23 122 | 123 | ### Added 124 | 125 | - Added a configuration file for scoreboards 126 | 127 | ### Misc 128 | 129 | - Version change triggered by a protocol change on rpcs3's side 130 | 131 | 132 | ## [1.1.0] - 2024-02-04 133 | 134 | ### Added 135 | 136 | - Added a notification to signal the target in RequestSignalingInfos to help connectivity 137 | 138 | 139 | ## [1.0.3] - 2024-01-30 140 | 141 | ### Fixed 142 | 143 | - Added owner checks to SetRoomDataInternal and SetRoomDataExternal and rpcn now only sends notifications on actual modification 144 | 145 | 146 | ## [1.0.2] - 2024-01-29 147 | 148 | ### Fixed 149 | 150 | - Add flush() after write_all() to ensure all data is sent 151 | 152 | 153 | ## [1.0.1] - 2024-01-29 154 | 155 | ### Fixed 156 | 157 | - Fixed GetScoreData accidentally returning a u64 for size of data 158 | 159 | 160 | ## [1.0.0] - 2024-01-14 161 | 162 | ### Added 163 | 164 | - Implemented SetUserInfo 165 | - Implemented GetRoomMemberBinAttr 166 | - Added proper public/private slot values 167 | - Added a cleanup mechanism for accounts that have never been logged on and are older than a month 168 | 169 | ### Fixed 170 | 171 | - Added FOREIGN_KEY constraints to TUS tables to ensure sanity 172 | - Added a mechanism to ensure cleanup is done properly before letting the user back in the server 173 | 174 | ### Misc 175 | 176 | - Updated dependencies 177 | - Migrated all score tables into one unified table 178 | - Added indexes for faster lookup to the SQL tables 179 | 180 | 181 | ## [0.9.2] - 2024-01-09 182 | 183 | ### Fixed 184 | 185 | - Forced stack size to 8MB on Windows for parity with Linux 186 | 187 | 188 | ## [0.9.1] - 2024-01-05 189 | 190 | ### Fixed 191 | 192 | - Add brackets around Message-ID sent with emails 193 | 194 | 195 | ## [0.9.0] - 2024-01-04 196 | 197 | ### Added 198 | 199 | - Presence support 200 | 201 | ### Fixed 202 | 203 | - Added Message-ID to emails sent to help with some SMTP relays 204 | 205 | ### Misc 206 | 207 | - Updated Flatbuffers to v23.5.26 208 | - Updated hyper to v1.1 209 | - Refactored data hierarchy for Client 210 | - Minor code cleanups 211 | 212 | 213 | ## [0.8.2] - 2023-12-30 214 | 215 | ### Fixed 216 | 217 | - Fixed tus_add_and_get_vuser_variable db query 218 | 219 | 220 | ## [0.8.1] - 2023-12-29 221 | 222 | ### Fixed 223 | 224 | - Fixed TUS data query failing because of info NOT NULL constraint fail 225 | 226 | 227 | ## [0.8.0] - 2023-12-28 228 | 229 | ### Added 230 | 231 | - Added full TUS(Title User Storage) API support. 232 | - Added GetNetworkTime support. 233 | - Added cleanup of unused data files from score/tus on startup(note that they are not cleaned while the server is running as otherwise I can't guarantee atomicity of the db query + file access). 234 | 235 | ### Changed 236 | 237 | - Improved code parsing by adding some wrappers. 238 | - Improved friend queries by storing both user id and username of friends on login. 239 | - Improved some database queries 240 | 241 | ### Fixed 242 | 243 | - Fixed users getting stuck in logged in state if the thread panics by moving logging out procedures to Client Drop impl. 244 | 245 | ### Misc 246 | 247 | - Added worlds for Playstation Home to config 248 | - Added server_redirs.cfg that contains DeS example -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rpcn" 3 | version = "1.4.2" 4 | authors = ["RipleyTom "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | num-traits = "0.2" 9 | num-derive = "0.4" 10 | rand = "0.8" 11 | rust-argon2 = "2.1" 12 | flatbuffers = "24.3.25" 13 | parking_lot = "0.12" 14 | libsqlite3-sys = "0.30" 15 | rusqlite = { version = "0.32", features = [ "bundled" ] } 16 | r2d2 = "0.8" 17 | r2d2_sqlite = "0.25" 18 | tokio = { version = "1.18", features = [ "full" ] } 19 | tokio-rustls = "0.26" 20 | rustls-pemfile = "2.2" 21 | socket2 = { version = "0.5", features = [ "all" ] } 22 | lettre = "0.11" 23 | tracing = "0.1" 24 | tracing-subscriber = "0.3" 25 | openssl = { version = "0.10", features = [ "vendored" ] } 26 | hyper = { version = "1.1", features = [ "full" ] } 27 | hyper-util = { version = "0.1", features = ["full"] } 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest as builder 2 | WORKDIR /usr/src/rpcn 3 | COPY . . 4 | RUN cargo install --path . 5 | 6 | FROM debian:bullseye-slim 7 | WORKDIR /rpcn 8 | RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* 9 | RUN openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem -subj '/CN=localhost' 10 | COPY --from=builder /usr/local/cargo/bin/rpcn /usr/local/bin/rpcn 11 | COPY --from=builder /usr/src/rpcn/*.cfg ./ 12 | 13 | CMD ["rpcn"] 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPCN 2 | 3 | RPCN is a server that implements multiplayer functionality for RPCS3. 4 | It implements rooms which permit matchmaking, scoreboards, title user storage(ie cloud saves), etc. 5 | All the settings and their descriptions are in rpcn.cfg. 6 | 7 | # FAQ 8 | 9 | ## RPCN complains about "Failed to open certificate cert.pem" 10 | 11 | RPCN needs a certificate and its corresponding private key to be generated for the encrypted TLS connections to clients, 12 | you can use OpenSSL for this: 13 | ``` 14 | openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem 15 | ``` 16 | 17 | You can, optionally and disabled by default, generate a key to sign generated tickets: 18 | ``` 19 | openssl ecparam -name secp224k1 -genkey -noout -out ticket_private.pem 20 | openssl ec -in ticket_private.pem -pubout -out ticket_public.pem 21 | ``` 22 | 23 | ## Will RPCN work with real PS3s? 24 | 25 | No. 26 | 27 | # Special Thanks 28 | 29 | A special thanks to the various authors of the following libraries that RPCN is using: 30 | - [Rusqlite](https://github.com/rusqlite/rusqlite) 31 | Perfect library if you plan to use SQLite with rust. The author has been incredibly helpful in diagnosing SQLite issues, thanks! 32 | - [Tokio](https://github.com/tokio-rs/tokio) 33 | The king of async for Rust. 34 | - [Flatbuffers](https://github.com/google/flatbuffers) 35 | This library has been pretty essential to the development of RPCN. Great serialization library, strict yet flexible! 36 | 37 | And all the other libraries I'm forgetting(check Cargo.toml)! 38 | Also thanks to everyone that contributed directly or indirectly to RPCN! 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | volumes: 4 | rpcnstorage: 5 | name: rpcnstorage 6 | driver: local 7 | driver_opts: 8 | device: /home/$USER/rpcn #use your preferred storage path here. This will NOT auto create the directory, must exist prior. 9 | o: bind 10 | type: none 11 | 12 | services: 13 | rpcn: 14 | container_name: rpcn 15 | image: rpcn:latest 16 | build: 17 | context: . 18 | dockerfile: Dockerfile 19 | volumes: 20 | - rpcnstorage:/rpcn 21 | network_mode: host #Only works with Linux systems currently 22 | -------------------------------------------------------------------------------- /rpcn.cfg: -------------------------------------------------------------------------------- 1 | # This setting determines if missing psn server ids are created internally. 2 | # It is strongly recommended to leave this to true unless you want to limit your server to a specific game and the server id for this 3 | # game is already in the database. 4 | CreateMissing=true 5 | # Determines the verbosity of the logging. 6 | # Valid values are Trace, Debug, Info, Warn, Error. 7 | Verbosity=Info 8 | # Host & Port of the server. 9 | Host=0.0.0.0 10 | Port=31313 11 | # This host is only used for the signaling part of RPCN 12 | HostIPv6=:: 13 | # This determines if emails are validated(ie if an email is sent to verify the email and if a token is required). 14 | # If you want to run your custom server to play with a few people it's probably not necessary. 15 | # Sadly recommended for public servers if you want to be able to ban people. 16 | EmailValidated=false 17 | # Settings for the email server, if EmailHost is empty an unencrypted localhost:25 connection is used and login&password is ignored. 18 | # Those settings are unused if EmailValidated is set to false 19 | EmailHost= 20 | EmailLogin= 21 | EmailPassword= 22 | # Determines if tickets are signed 23 | # If enabled ticket_private.key must contain the private key to sign tickets 24 | SignTickets=false 25 | SignTicketsDigest=SHA224 26 | # Runs a minimal web server to display stats 27 | StatServer=false 28 | StatServerHost=0.0.0.0 29 | StatServerPort=31314 30 | StatServerCacheLife=1 31 | # List of admin usernames, separated by a comma("AdminsList=yournickname,friendnickname") 32 | # Make sure to create those accounts before making the server public 33 | AdminsList= 34 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 200 2 | hard_tabs = true -------------------------------------------------------------------------------- /scoreboards.cfg: -------------------------------------------------------------------------------- 1 | #Syntax is communication_id|board_ids|rank_limit|update_mode|sort_mode|upload_num_limit|upload_size_limit 2 | #board_ids can be multiple board ids separated by a comma 3 | #update_mode values: 4 | #NORMAL_UPDATE 5 | #FORCE_UPDATE 6 | #sort_mode values: 7 | #DESCENDING_ORDER 8 | #ASCENDING_ORDER 9 | 10 | #Ridge Racer 7 11 | NPWR00001_00|0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 12 | NPWR00001_00|352,353,354,355|250|FORCE_UPDATE|DESCENDING_ORDER|10|6000000 13 | NPWR00001_01|0,1|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 14 | #Gran Turismo HD Concept (JP) 15 | NPWR00010_00|1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,65,66|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 16 | #Gran Turismo HD Concept (US) 17 | NPWR00064_00|1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,65,66|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 18 | #Gran Turismo HD Concept (Asia) 19 | NPWR00065_00|1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,65,66|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 20 | #Gran Turismo HD Concept (EU) 21 | NPWR00091_00|1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,65,66|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 22 | #GTI Club 23 | NPWR00284_00|0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 24 | #MEGA MAN 9 25 | NPWR00386_00|0,1,2,3,4,5,6,7,8,9,10,11,12,14|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 26 | NPWR00386_00|13|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 27 | #LUMINES SuperNova 28 | NPWR00394_00|50,150|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 29 | NPWR00394_00|0,2,30,40,42,44,46,100,102,130,140,142,144,146|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 30 | #MEGA MAN 10 31 | NPWR00880_00|0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 32 | NPWR00880_00|17|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 33 | #Sonic the Hedgehog 4: Episode I 34 | NPWR00954_00|0,1,2,6,7,8,12,13,14,19,24,25,26,30,31,32,36,37,38,43,48,49,50,54,55,56,60,61,62,67,72,73,74,78,79,80,84,85,86,91,97,102,104,106,108,110,112,114|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 35 | NPWR00954_00|3,4,5,9,10,11,15,16,17,22,27,28,29,33,34,35,39,40,41,46,51,52,53,57,58,59,63,64,65,70,75,76,77,81,82,83,87,88,89,94,100,103,105,107,109,111,113,115|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 36 | #HOARD 37 | NPWR01374_00|20,21,22,23,24,25,26,27,28,42|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 38 | NPWR01374_00|0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,29,30,31,32,33,34|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 39 | #Crazy Taxi 40 | NPWR01440_00|9,10,11,13,14,15,16,17,18,19,21,22,23|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 41 | #PAC-MAN Championship Edition DX+ 42 | NPWR01441_00|44,45,47,48,49,50,51,52,53,55,56,58,59,60,61,62,63,64,65,444,445,447,448,455,456|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 43 | NPWR01441_00|0,1,3,4,5,6,7,8,11,12,14,15,16,17,18,19,20,21,22,23,25,26,27,28,29,30,33,36,37,38,39,40,41,411,412,414,415,422,423,425,426|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 44 | #Alex Kidd in Miracle World 45 | NPWR01539_00|0,1,2,10,11,12,20,21,22|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 46 | #The Revenge of Shinobi 47 | NPWR01544_00|0,1,2|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 48 | NPWR01544_00|10,11,12,20,21,22|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 49 | #Wonder Boy in Monster World 50 | NPWR01546_00|0,1,2,10,11,12,20,21,22|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 51 | #Monster World IV 52 | NPWR01547_00|0,1,2,10,11,12,20,21,22|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 53 | #Super Hang-On 54 | NPWR01548_00|0,1,2,10,11,12,20,21,22,30,31,32,40,41,42|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 55 | #Wizardry: Prisoners of the Lost City (Confirmed commented line does NOT behave the same on PSN, so those show the most party wipes/deaths/lost chars) 56 | NPWR01555_00|0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65|250|FORCE_UPDATE|DESCENDING_ORDER|10|6000000 57 | #NPWR01555_00|3,4,5|250|FORCE_UPDATE|ASCENDING_ORDER|10|6000000 58 | #Daytona USA 59 | NPWR01815_00|1,2,3|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 60 | #Wonder Boy in Monster Land 61 | NPWR01843_00|10,11,12,30,31,32|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 62 | NPWR01843_00|0,1,2,20,21,22|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 63 | #Wrecked: Revenge Revisited 64 | NPWR01856_00|0,1,2,3,4,5,6,7,8,9,10,11,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,42,43,44,45,46,47|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 65 | #Sonic the Hedgehog 4: Episode II 66 | NPWR02484_00|0,1,2,6,7,8,12,13,14,18,19,20,24,25,26,30,31,32,36,37,38,42,43,44,48,49,50,54,55,56,60,61,62,66,67,68,72,73,74,78,79,80,84,85,86,90,91,92,96,97,98,102,103,104,108,109,110,114,115,116,120,121,122,126,127,128,132,133,134,138,139,140,144,145,146,150,151,152,156,157,158,162,163,164,168,169,170,174,175,176,180,181,182,186,187,188,192,193,194,198,199,200,204,205,206,210,211,212,216,218,220,222,224,226,228,230,232,234,236,238,240,242|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 67 | NPWR02484_00|3,4,5,9,10,11,15,16,17,21,22,23,27,28,29,33,34,35,39,40,41,45,46,47,51,52,53,57,58,59,63,64,65,69,70,71,75,76,77,81,82,83,87,88,89,93,94,95,99,100,101,105,106,107,111,112,113,117,118,119,123,124,125,129,130,131,135,136,137,141,142,143,147,148,149,153,154,155,159,160,161,165,166,167,171,172,173,177,178,179,183,184,185,189,190,191,195,196,197,201,202,203,207,208,209,213,214,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 68 | #Retro/Grade (Lowest score) 69 | NPWR02771_00|0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 70 | NPWR02771_00|183,184|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 71 | #Sonic CD 72 | NPWR02783_00|0|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 73 | NPWR02783_00|1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 74 | #Hotline Miami 2: Wrong Number 75 | NPWR06254_00|2,3,6,7,10,11,14,15,18,19,22,23,26,27,30,31,34,35,38,39,42,43,46,47,50,51,54,55,58,59,62,63,66,67,70,71,74,75,78,79,82,83,86,87,90,91,94,95,98,99,102,103|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 76 | #DuckTales: Remastered 77 | NPWR04539_00|4|250|NORMAL_UPDATE|ASCENDING_ORDER|10|6000000 78 | NPWR04539_00|0,1,2,3|250|NORMAL_UPDATE|DESCENDING_ORDER|10|6000000 79 | -------------------------------------------------------------------------------- /server_redirs.cfg: -------------------------------------------------------------------------------- 1 | #DeS (redir to EU) 2 | NPWR00245_00=>NPWR01249_00 3 | NPWR00881_00=>NPWR01249_00 4 | #JoJo ASB (redir to US, doesn't work :x) 5 | #NPWR03545(jp) NPWR05840(us) 6 | -------------------------------------------------------------------------------- /servers.cfg: -------------------------------------------------------------------------------- 1 | #This file describes servers to be created for games more complex than 1 server / 1 world 2 | #The format is communication_id|server|world|lobbies(separated by comma) 3 | #Tekken 6(tested) 4 | NPWR00482_00|1|1| 5 | NPWR00482_00|1|2| 6 | NPWR00482_00|1|3| 7 | NPWR00482_00|1|4| 8 | #MGS:PW 9 | NPWR01848_00|1|1| 10 | NPWR01848_00|1|2| 11 | NPWR01848_00|1|3| 12 | #DBZ:Battle of Z 13 | NPWR04847_00|1|1| 14 | NPWR04847_00|1|2| 15 | NPWR04847_00|1|3| 16 | NPWR04847_00|1|4| 17 | #Puyo Puyo 18 | NPWR04945_00|1|1| 19 | NPWR04945_00|1|2| 20 | NPWR04945_00|1|3| 21 | NPWR04945_00|1|4| 22 | NPWR04945_00|1|5| 23 | NPWR04945_00|1|6| 24 | #Sword Art Online: Lost Song 25 | NPWR08257_00|1|1| 26 | NPWR08257_00|1|2| 27 | NPWR08257_00|1|3| 28 | NPWR08257_00|1|4| 29 | NPWR08257_00|1|5| 30 | #Naruto Shippuuden: Narutimate Storm Generation (untested) 31 | NPWR01886_00|1|1| 32 | NPWR01886_00|1|2| 33 | #Naruto Shippuden: Ultimate Ninja Storm Revolution (untested) 34 | NPWR06010_00|1|1| 35 | NPWR06010_00|1|2| 36 | #JoJo's Bizarre Adventure - All-Star Battle (untested) 37 | NPWR05840_00|1|1| 38 | NPWR05840_00|1|2| 39 | #Gladiator VS (untested) 40 | NPWR02544_00|1|1| 41 | NPWR02544_00|1|2| 42 | #Virtua Tennis 4 (untested) 43 | NPWR01224_00|1|1| 44 | NPWR01224_00|1|2| 45 | #Armored Core V (untested) 46 | NPWR03050_00|1|1| 47 | NPWR03050_00|1|2| 48 | #Nascar '15 (untested) 49 | NPWR09007_00|1|1| 50 | NPWR09007_00|1|2| 51 | #.hack//Versus (untested) 52 | NPWR01946_00|1|1| 53 | NPWR01946_00|1|2| 54 | #Naruto Shippuden: Ultimate Ninja Storm 2 55 | NPWR00637_00|1|1| 56 | NPWR00637_00|1|2| 57 | #機動戦士ガンダム エクストリームバーサス フルブースト 58 | NPWR04269_00|1|1| 59 | NPWR04269_00|1|2| 60 | NPWR04269_00|1|3| 61 | NPWR04269_00|1|4| 62 | #Saint Seiya: Brave Soldiers 63 | NPWR04207_00|1|1| 64 | NPWR04207_00|1|2| 65 | NPWR04207_00|1|3| 66 | NPWR04207_00|1|4| 67 | #Saint Seiya: Soldiers' Soul 68 | NPWR08977_00|1|1| 69 | NPWR08977_00|1|2| 70 | NPWR08977_00|1|3| 71 | NPWR08977_00|1|4| 72 | #PlayStation Home 73 | NPWR00432_00|1|65537| 74 | NPWR00432_00|1|65538| 75 | NPWR00432_00|1|65539| 76 | NPWR00432_00|1|65540| 77 | NPWR00432_00|1|65541| 78 | NPWR00432_00|1|65542| 79 | NPWR00432_00|1|65543| 80 | NPWR00432_00|1|65544| 81 | NPWR00432_00|1|65545| 82 | NPWR00432_00|1|65546| 83 | #Metal Slug 3 84 | NPWR08648_00|1|65537| 85 | #BBCT 86 | NPWR00656_00|1|1| 87 | NPWR00656_00|1|2| 88 | #BBCS 89 | NPWR01202_00|1|1| 90 | NPWR01202_00|1|2| 91 | #BBCSE 92 | NPWR02565_00|1|1| 93 | NPWR02565_00|1|2| 94 | #BBCP 95 | NPWR04033_00|1|1| 96 | NPWR04033_00|1|2| 97 | NPWR04033_00|1|3| 98 | #BBCPE 99 | NPWR07753_00|1|1| 100 | NPWR07753_00|1|2| 101 | NPWR07753_00|1|3| 102 | #BBCF 103 | NPWR10301_00|1|1| 104 | NPWR10301_00|1|2| 105 | NPWR10301_00|1|3| 106 | #P4A 107 | NPWR02915_00|1|1| 108 | NPWR02915_00|1|2| 109 | NPWR02915_00|1|3| 110 | #P4AU 111 | NPWR06200_00|1|1| 112 | NPWR06200_00|1|2| 113 | NPWR06200_00|1|3| 114 | #UNIEL 115 | NPWR06112_00|1|1| 116 | NPWR06112_00|1|2| 117 | #Outrun Online Arcade 118 | NPWR00458_00|1|1| 119 | NPWR00458_00|1|2| 120 | NPWR00458_00|1|3| 121 | #SoulCalibur V 122 | NPWR01738_00|1|1| 123 | NPWR01738_00|1|2| 124 | #King of Fighters XIII 125 | NPWR02645_00|1|1| 126 | NPWR02645_00|1|2| 127 | #AH3 128 | NPWR01464_00|1|1| 129 | NPWR01464_00|1|2| 130 | #AH3LM!!!!! 131 | NPWR06231_00|1|1| 132 | NPWR06231_00|1|2| 133 | 134 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::convert::TryInto; 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::str::FromStr; 7 | 8 | mod server; 9 | use server::client::{Client, ComId, COMMUNICATION_ID_SIZE}; 10 | use server::Server; 11 | 12 | use openssl::ec::EcKey; 13 | use openssl::hash::MessageDigest; 14 | use openssl::pkey::{PKey, Private}; 15 | 16 | pub struct TicketSignInfo { 17 | pub digest: MessageDigest, 18 | pub key: PKey, 19 | } 20 | 21 | pub struct Config { 22 | create_missing: bool, // Creates servers/worlds/lobbies if the client queries for ones but there are none or specific id queries 23 | verbosity: tracing::Level, 24 | host_ipv4: String, 25 | host_ipv6: String, 26 | port: String, 27 | email_validated: bool, // Requires email validation 28 | email_host: String, 29 | email_login: String, 30 | email_password: String, 31 | banned_domains: HashSet, 32 | server_redirs: HashMap, 33 | ticket_signature_info: Option, 34 | stat_server_host_and_port: Option<(String, String)>, 35 | stat_server_timeout: u32, 36 | admins_list: Vec, 37 | } 38 | 39 | impl Config { 40 | pub fn new() -> Config { 41 | Config { 42 | create_missing: true, 43 | verbosity: tracing::Level::INFO, 44 | host_ipv4: "0.0.0.0".to_string(), 45 | host_ipv6: "0:0:0:0:0:0:0:0".to_string(), 46 | port: "31313".to_string(), 47 | email_validated: false, 48 | email_host: String::new(), 49 | email_login: String::new(), 50 | email_password: String::new(), 51 | banned_domains: HashSet::new(), 52 | server_redirs: HashMap::new(), 53 | ticket_signature_info: None, 54 | stat_server_host_and_port: None, 55 | stat_server_timeout: 0, 56 | admins_list: Vec::new(), 57 | } 58 | } 59 | 60 | pub fn load_config_file(&mut self) -> Result<(), std::io::Error> { 61 | let mut file = File::open("rpcn.cfg")?; 62 | let mut buf_file = String::new(); 63 | file.read_to_string(&mut buf_file)?; 64 | 65 | let config_data: HashMap<&str, &str> = buf_file 66 | .lines() 67 | .filter_map(|l| { 68 | if l.trim().is_empty() || l.trim().chars().nth(0).unwrap() == '#' { 69 | return None; 70 | } 71 | 72 | let name_and_value: Vec<&str> = l.trim().splitn(2, '=').collect(); 73 | if name_and_value.len() != 2 { 74 | return None; 75 | } 76 | Some((name_and_value[0], name_and_value[1])) 77 | }) 78 | .collect(); 79 | 80 | let set_bool = |name: &str, d_bool: &mut bool| { 81 | if let Some(data) = config_data.get(name) { 82 | match data { 83 | s if s.eq_ignore_ascii_case("true") => *d_bool = true, 84 | s if s.eq_ignore_ascii_case("false") => *d_bool = false, 85 | s => println!("Invalid value(<{}>) for configuration entry <{}>, defaulting to <{}>", s, name, *d_bool), 86 | } 87 | } else { 88 | println!("Configuration entry for <{}> was not found, defaulting to <{}>", name, d_bool); 89 | } 90 | }; 91 | 92 | let set_string = |name: &str, d_str: &mut String| { 93 | if let Some(data) = config_data.get(name) { 94 | *d_str = String::from(*data); 95 | } else { 96 | println!("Configuration entry for <{}> was not found, defaulting to <{}>", name, d_str); 97 | } 98 | }; 99 | 100 | let set_u32 = |name: &str, d_u32: &mut u32| { 101 | if let Some(data) = config_data.get(name) { 102 | match data.parse::() { 103 | Ok(v) => *d_u32 = v, 104 | Err(_) => println!("Invalid value(<{}>) for configuration entry <{}>, defaulting to <{}>", data, name, *d_u32), 105 | } 106 | } else { 107 | println!("Configuration entry for <{}> was not found, defaulting to <{}>", name, d_u32); 108 | } 109 | }; 110 | 111 | let set_verbosity = |d_verbosity: &mut tracing::Level| { 112 | if let Some(data) = config_data.get("Verbosity") { 113 | if let Ok(level) = tracing::Level::from_str(data) { 114 | *d_verbosity = level; 115 | } else { 116 | println!("Config value given for Verbosity(<{}>) is invalid, defaulting to <{}>!", data, d_verbosity); 117 | } 118 | } else { 119 | println!("Configuration entry for Verbosity was not found, defaulting to <{}>", d_verbosity); 120 | } 121 | }; 122 | 123 | let set_admins_list = |d_list: &mut Vec| { 124 | if let Some(data) = config_data.get("AdminsList") { 125 | if data.is_empty() { 126 | return; 127 | } 128 | 129 | let admins_list: Vec = data.split(',').map(|a| a.trim().to_string()).collect(); 130 | 131 | if admins_list.iter().map(|username| Client::is_valid_username(username)).any(|r| !r) { 132 | println!("AdminsList contains an invalid username, the setting will be ignored!"); 133 | } else { 134 | *d_list = admins_list; 135 | } 136 | } else { 137 | println!("Configuration entry for AdminsList was not found, leaving it empty"); 138 | } 139 | }; 140 | 141 | // Unused for now 142 | // let set_u32 = |name: &str, d_u32: &mut u32| { 143 | // if let Some(data) = config_data.get(name) { 144 | // match data.parse::() { 145 | // Ok(parsed) => *d_u32 = parsed, 146 | // Err(e) => println!("Failed to parse value for <{}>, defaulting to <{}>: {}", name, d_u32, e), 147 | // } 148 | // } else { 149 | // println!("Configuration entry for <{}> was not found, defaulting to <{}>", name, d_u32); 150 | // } 151 | // }; 152 | 153 | set_bool("CreateMissing", &mut self.create_missing); 154 | set_verbosity(&mut self.verbosity); 155 | set_bool("EmailValidated", &mut self.email_validated); 156 | set_string("EmailHost", &mut self.email_host); 157 | set_string("EmailLogin", &mut self.email_login); 158 | set_string("EmailPassword", &mut self.email_password); 159 | set_string("Host", &mut self.host_ipv4); 160 | set_string("HostIPv6", &mut self.host_ipv6); 161 | set_string("Port", &mut self.port); 162 | set_string("EmailHost", &mut self.email_host); 163 | set_string("EmailLogin", &mut self.email_login); 164 | set_string("EmailPassword", &mut self.email_password); 165 | 166 | let mut sign_tickets = false; 167 | set_bool("SignTickets", &mut sign_tickets); 168 | 169 | if sign_tickets { 170 | let ticket_key = Config::load_ticket_private_key(); 171 | if let Err(ref e) = ticket_key { 172 | println!("Error loading the ticket private key:\n{}", e); 173 | } 174 | let ticket_key = ticket_key.ok(); 175 | 176 | let mut ticket_digest_str = String::new(); 177 | set_string("SignTicketsDigest", &mut ticket_digest_str); 178 | let ticket_digest = MessageDigest::from_name(&ticket_digest_str); 179 | if ticket_digest.is_none() { 180 | println!("SignTicketsDigest value <{}> is invalid!", ticket_digest_str); 181 | } 182 | 183 | if ticket_key.is_none() || ticket_digest.is_none() { 184 | println!("Ticket signing is enabled but it's missing digest/key, disabling ticket signing!"); 185 | } else { 186 | self.ticket_signature_info = Some(TicketSignInfo { 187 | digest: ticket_digest.unwrap(), 188 | key: ticket_key.unwrap(), 189 | }); 190 | } 191 | } 192 | 193 | let mut run_stat_server = false; 194 | set_bool("StatServer", &mut run_stat_server); 195 | 196 | if run_stat_server { 197 | let mut stat_server_host = String::new(); 198 | let mut stat_server_port = String::new(); 199 | set_string("StatServerHost", &mut stat_server_host); 200 | set_string("StatServerPort", &mut stat_server_port); 201 | 202 | set_u32("StatServerCacheLife", &mut self.stat_server_timeout); 203 | 204 | if stat_server_host.is_empty() || stat_server_port.is_empty() { 205 | println!("Stat server is enabled but it's missing host/port information, disabling it!"); 206 | } else { 207 | self.stat_server_host_and_port = Some((stat_server_host, stat_server_port)); 208 | } 209 | } 210 | 211 | set_admins_list(&mut self.admins_list); 212 | 213 | Ok(()) 214 | } 215 | 216 | pub fn is_create_missing(&self) -> bool { 217 | self.create_missing 218 | } 219 | 220 | pub fn is_email_validated(&self) -> bool { 221 | self.email_validated 222 | } 223 | 224 | pub fn get_verbosity(&self) -> &tracing::Level { 225 | &self.verbosity 226 | } 227 | 228 | pub fn get_host_ipv4(&self) -> &String { 229 | &self.host_ipv4 230 | } 231 | 232 | pub fn get_host_ipv6(&self) -> &String { 233 | &self.host_ipv6 234 | } 235 | 236 | pub fn get_port(&self) -> &String { 237 | &self.port 238 | } 239 | 240 | pub fn get_email_auth(&self) -> (String, String, String) { 241 | (self.email_host.clone(), self.email_login.clone(), self.email_password.clone()) 242 | } 243 | 244 | pub fn load_domains_banlist(&mut self) { 245 | if let Ok(mut file_emails) = File::open("domains_banlist.txt") { 246 | let mut buf_file = String::new(); 247 | let _ = file_emails.read_to_string(&mut buf_file); 248 | self.banned_domains = buf_file.lines().map(|x| x.trim().to_ascii_lowercase()).collect(); 249 | } 250 | } 251 | pub fn is_banned_domain(&self, domain: &str) -> bool { 252 | self.banned_domains.contains(domain) 253 | } 254 | 255 | pub fn load_server_redirections(&mut self) { 256 | if let Ok(mut file_redirs) = File::open("server_redirs.cfg") { 257 | let mut buf_file = String::new(); 258 | let _ = file_redirs.read_to_string(&mut buf_file); 259 | self.server_redirs = buf_file 260 | .lines() 261 | .filter_map(|line| { 262 | let parsed: Vec<&[u8]> = line.trim().split("=>").map(|x| x.trim()).map(|x| x.as_bytes()).collect(); 263 | if line.is_empty() || line.chars().nth(0).unwrap() == '#' || parsed.len() != 2 || parsed[0].len() != COMMUNICATION_ID_SIZE || parsed[1].len() != COMMUNICATION_ID_SIZE { 264 | None 265 | } else { 266 | Some((parsed[0].try_into().unwrap(), parsed[1].try_into().unwrap())) 267 | } 268 | }) 269 | .collect(); 270 | } 271 | } 272 | 273 | pub fn get_server_redirection(&self, com_id: ComId) -> ComId { 274 | match self.server_redirs.get(&com_id) { 275 | Some(redir) => *redir, 276 | None => com_id, 277 | } 278 | } 279 | 280 | pub fn get_ticket_signing_info(&self) -> &Option { 281 | &self.ticket_signature_info 282 | } 283 | 284 | pub fn get_stat_server_binds(&self) -> &Option<(String, String)> { 285 | &self.stat_server_host_and_port 286 | } 287 | 288 | pub fn get_stat_server_timeout(&self) -> u32 { 289 | self.stat_server_timeout 290 | } 291 | 292 | pub fn get_admins_list(&self) -> &Vec { 293 | &self.admins_list 294 | } 295 | 296 | fn load_ticket_private_key() -> Result, String> { 297 | let mut private_key_file = File::open("ticket_private.pem").map_err(|e| format!("Failed to open ticket_private.pem: {}", e))?; 298 | let mut private_key_raw = Vec::new(); 299 | private_key_file.read_to_end(&mut private_key_raw).map_err(|e| format!("Failed to read ticket_private.pem: {}", e))?; 300 | let ec_key = EcKey::private_key_from_pem(&private_key_raw).map_err(|e| format!("Failed to read private key from the file: {}", e))?; 301 | PKey::from_ec_key(ec_key).map_err(|e| format!("Failed to convert EC key to PKey: {}", e)) 302 | } 303 | } 304 | 305 | fn main() { 306 | println!("RPCN v{}", env!("CARGO_PKG_VERSION")); 307 | 308 | let mut config = Config::new(); 309 | if let Err(e) = config.load_config_file() { 310 | println!("An error happened reading the config file rpcn.cfg: {}\nDefault values will be used for every settings!", e); 311 | } 312 | 313 | config.load_domains_banlist(); 314 | config.load_server_redirections(); 315 | 316 | let subscriber = tracing_subscriber::FmtSubscriber::builder() 317 | .with_max_level(*config.get_verbosity()) 318 | .without_time() 319 | .with_target(false) 320 | .with_ansi(true) 321 | .finish(); 322 | tracing::subscriber::set_global_default(subscriber).expect("Setting default subscriber failed!"); 323 | 324 | let serv = Server::new(config); 325 | if let Err(e) = serv { 326 | println!("Failed to create server: {}", e); 327 | return; 328 | } 329 | let mut serv = serv.unwrap(); 330 | 331 | if let Err(e) = serv.start() { 332 | println!("Server terminated with error: {}", e); 333 | } else { 334 | println!("Server terminated normally"); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::fs::File; 3 | use std::io::{self, BufReader}; 4 | use std::net::ToSocketAddrs; 5 | use std::sync::Arc; 6 | 7 | use rustls_pemfile::{certs, pkcs8_private_keys}; 8 | use tokio::io::AsyncWriteExt; 9 | use tokio::net::TcpListener; 10 | use tokio::runtime; 11 | use tokio::sync::watch; 12 | use tokio_rustls::rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; 13 | use tokio_rustls::rustls::server::ServerConfig; 14 | use tokio_rustls::TlsAcceptor; 15 | use tracing::{error, info, warn}; 16 | 17 | use parking_lot::RwLock; 18 | 19 | use socket2::{SockRef, TcpKeepalive}; 20 | 21 | pub mod client; 22 | use client::{Client, ClientSharedInfo, PacketType, SharedData, TerminateWatch, HEADER_SIZE}; 23 | mod database; 24 | mod game_tracker; 25 | use game_tracker::GameTracker; 26 | mod gui_room_manager; 27 | use gui_room_manager::GuiRoomManager; 28 | mod room_manager; 29 | use room_manager::RoomManager; 30 | mod score_cache; 31 | use score_cache::ScoresCache; 32 | mod udp_server; 33 | mod utils; 34 | use crate::Config; 35 | 36 | #[allow(non_snake_case, dead_code)] 37 | mod stream_extractor; 38 | 39 | const PROTOCOL_VERSION: u32 = 26; 40 | 41 | pub struct Server { 42 | config: Arc>, 43 | db_pool: r2d2::Pool, 44 | gui_room_manager: Arc>, 45 | room_manager: Arc>, 46 | client_infos: Arc>>, 47 | score_cache: Arc, 48 | game_tracker: Arc, 49 | cleanup_duty: Arc>>, 50 | } 51 | 52 | impl Server { 53 | pub fn new(config: Config) -> Result { 54 | let config = Arc::new(RwLock::new(config)); 55 | 56 | let db_pool = Server::initialize_database(config.read().get_admins_list())?; 57 | let score_cache = Server::initialize_score(db_pool.get().map_err(|e| format!("Failed to get a database connection: {}", e))?)?; 58 | Server::initialize_tus_data_handler()?; 59 | 60 | Server::cleanup_database(db_pool.get().map_err(|e| format!("Failed to get a database connection: {}", e))?)?; 61 | 62 | Server::clean_score_data(db_pool.get().map_err(|e| format!("Failed to get a database connection: {}", e))?)?; 63 | Server::clean_tus_data(db_pool.get().map_err(|e| format!("Failed to get a database connection: {}", e))?)?; 64 | 65 | let gui_room_manager = Arc::new(RwLock::new(GuiRoomManager::new())); 66 | let room_manager = Arc::new(RwLock::new(RoomManager::new())); 67 | let client_infos = Arc::new(RwLock::new(HashMap::new())); 68 | let game_tracker = Arc::new(GameTracker::new()); 69 | let cleanup_duty = Arc::new(RwLock::new(HashSet::new())); 70 | 71 | Ok(Server { 72 | config, 73 | db_pool, 74 | gui_room_manager, 75 | room_manager, 76 | client_infos, 77 | score_cache, 78 | game_tracker, 79 | cleanup_duty, 80 | }) 81 | } 82 | 83 | #[cfg(any( 84 | doc, 85 | target_os = "android", 86 | target_os = "dragonfly", 87 | target_os = "freebsd", 88 | target_os = "fuchsia", 89 | target_os = "illumos", 90 | target_os = "linux", 91 | target_os = "netbsd", 92 | target_vendor = "apple", 93 | ))] 94 | fn set_socket_keepalive(stream: &tokio::net::TcpStream) -> Result<(), std::io::Error> { 95 | let socket_ref = SockRef::from(stream); 96 | socket_ref.set_tcp_keepalive( 97 | &TcpKeepalive::new() 98 | .with_time(std::time::Duration::new(30, 0)) 99 | .with_interval(std::time::Duration::new(30, 0)) 100 | .with_retries(4), 101 | ) 102 | } 103 | 104 | #[cfg(target_os = "windows")] 105 | fn set_socket_keepalive(stream: &tokio::net::TcpStream) -> Result<(), std::io::Error> { 106 | let socket_ref = SockRef::from(stream); 107 | socket_ref.set_tcp_keepalive(&TcpKeepalive::new().with_time(std::time::Duration::new(30, 0)).with_interval(std::time::Duration::new(30, 0))) 108 | } 109 | 110 | pub fn start(&mut self) -> io::Result<()> { 111 | // Parse host address 112 | let str_addr = self.config.read().get_host_ipv4().clone() + ":" + self.config.read().get_port(); 113 | let mut addr = str_addr.to_socket_addrs().map_err(|e| io::Error::new(e.kind(), format!("{} is not a valid address", &str_addr)))?; 114 | let addr = addr 115 | .next() 116 | .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, format!("{} is not a valid address", &str_addr)))?; 117 | 118 | // Setup TLS 119 | let f_cert = File::open("cert.pem").map_err(|e| io::Error::new(e.kind(), "Failed to open certificate cert.pem"))?; 120 | let f_key = std::fs::File::open("key.pem").map_err(|e| io::Error::new(e.kind(), "Failed to open private key key.pem"))?; 121 | let certif = certs(&mut BufReader::new(&f_cert)) 122 | .collect::>, io::Error>>() 123 | .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "cert.pem is invalid"))?; 124 | let mut private_key = pkcs8_private_keys(&mut BufReader::new(&f_key)) 125 | .collect::>, io::Error>>() 126 | .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "key.pem is invalid"))?; 127 | if certif.is_empty() || private_key.is_empty() { 128 | return Err(io::Error::new(io::ErrorKind::InvalidInput, "key.pem doesn't contain a PKCS8 encoded private key!")); 129 | } 130 | 131 | let server_config = ServerConfig::builder() 132 | //.with_safe_default_protocol_versions() 133 | .with_no_client_auth() 134 | .with_single_cert(certif, private_key.remove(0).into()) 135 | .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Failed to setup certificate"))?; 136 | 137 | // Setup Tokio 138 | let runtime = runtime::Builder::new_multi_thread().enable_all().build()?; 139 | let handle = runtime.handle().clone(); 140 | let acceptor = TlsAcceptor::from(Arc::new(server_config)); 141 | 142 | let mut servinfo_vec = vec![PacketType::ServerInfo as u8]; 143 | servinfo_vec.extend(&0u16.to_le_bytes()); 144 | servinfo_vec.extend(&(4 + HEADER_SIZE).to_le_bytes()); 145 | servinfo_vec.extend(&0u64.to_le_bytes()); 146 | servinfo_vec.extend(&PROTOCOL_VERSION.to_le_bytes()); 147 | let servinfo_vec: Arc> = Arc::new(servinfo_vec); 148 | 149 | let fut_server = async { 150 | let (term_send, term_recv) = watch::channel(false); 151 | let mut term_watch = TerminateWatch::new(term_recv, term_send); 152 | 153 | self.start_udp_server(term_watch.clone()).await?; 154 | self.start_stat_server(term_watch.clone(), self.game_tracker.clone()).await?; 155 | 156 | let listener = TcpListener::bind(&addr).await.map_err(|e| io::Error::new(e.kind(), format!("Error binding to <{}>: {}", &addr, e)))?; 157 | info!("Now waiting for connections on <{}>", &addr); 158 | 159 | 'main_loop: loop { 160 | tokio::select! { 161 | accept_result = listener.accept() => { 162 | if let Err(e) = accept_result { 163 | warn!("Accept failed with: {}", e); 164 | continue 'main_loop; 165 | } 166 | let (stream, peer_addr) = accept_result.unwrap(); 167 | 168 | { 169 | if let Err(e) = Server::set_socket_keepalive(&stream) { 170 | error!("set_tcp_keepalive() failed with: {}", e); 171 | } 172 | } 173 | 174 | info!("New client from {}", peer_addr); 175 | let acceptor = acceptor.clone(); 176 | let config = self.config.clone(); 177 | let db_pool = self.db_pool.clone(); 178 | let shared = SharedData::new(self.gui_room_manager.clone(), self.room_manager.clone(), self.client_infos.clone(), self.score_cache.clone(), self.game_tracker.clone(), self.cleanup_duty.clone()); 179 | let servinfo_vec = servinfo_vec.clone(); 180 | let term_watch = term_watch.clone(); 181 | let fut_client = async move { 182 | let mut stream = acceptor.accept(stream).await?; 183 | stream.write_all(&servinfo_vec).await?; 184 | let (mut client, mut tls_reader) = Client::new(config, stream, db_pool, shared, term_watch).await; 185 | client.process(&mut tls_reader).await; 186 | Ok(()) as io::Result<()> 187 | }; 188 | handle.spawn(fut_client); 189 | } 190 | _ = term_watch.recv.changed() => { 191 | break 'main_loop; 192 | } 193 | } 194 | } 195 | 196 | Ok(()) 197 | }; 198 | let res = runtime.block_on(fut_server); 199 | runtime.shutdown_timeout(std::time::Duration::from_secs(120)); 200 | res 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/server/client/cmd_account.rs: -------------------------------------------------------------------------------- 1 | // Account Management Commands 2 | 3 | use std::sync::atomic::{AtomicU64, Ordering}; 4 | 5 | use crate::server::client::*; 6 | use crate::server::database::DbError; 7 | 8 | use lettre::{message::Mailbox, Message, SmtpTransport, Transport}; 9 | 10 | fn strip_email(email: &str) -> String { 11 | let check_email = email.to_ascii_lowercase(); 12 | let tokens: Vec<&str> = check_email.split('@').collect(); 13 | let alias_split: Vec<&str> = tokens[0].split('+').collect(); 14 | format!("{}@{}", alias_split[0], tokens[1]) 15 | } 16 | 17 | static EMAIL_ID_DISPENSER: AtomicU64 = AtomicU64::new(1); 18 | 19 | impl Client { 20 | pub fn is_admin(&self) -> bool { 21 | self.client_info.admin 22 | } 23 | 24 | #[allow(dead_code)] 25 | pub fn is_stat_agent(&self) -> bool { 26 | self.client_info.stat_agent 27 | } 28 | 29 | fn send_email(&self, email_to_send: Message) -> Result<(), lettre::transport::smtp::Error> { 30 | let (host, login, password) = self.config.read().get_email_auth(); 31 | 32 | let smtp_client; 33 | if host.is_empty() { 34 | smtp_client = SmtpTransport::unencrypted_localhost(); 35 | } else { 36 | let mut smtp_client_builder = SmtpTransport::relay(&host)?; 37 | 38 | if !login.is_empty() { 39 | smtp_client_builder = smtp_client_builder 40 | .credentials(Credentials::new(login, password)) 41 | .authentication(vec![Mechanism::Plain]) 42 | .hello_name(lettre::transport::smtp::extension::ClientId::Domain("np.rpcs3.net".to_string())); 43 | } 44 | 45 | smtp_client = smtp_client_builder.build(); 46 | } 47 | 48 | smtp_client.send(&email_to_send)?; 49 | Ok(()) 50 | } 51 | 52 | fn send_email_template(&self, email_addr: &str, email_username: &str, subject: &str, body: String) -> Result<(), String> { 53 | let message_id = format!("<{}.{}@rpcs3.net>", Client::get_timestamp_nanos(), EMAIL_ID_DISPENSER.fetch_add(1, Ordering::SeqCst)); 54 | 55 | let email_to_send = Message::builder() 56 | .to(Mailbox::new( 57 | Some(email_username.to_owned()), 58 | email_addr.parse().map_err(|e| format!("Error parsing email({}): {}", email_addr, e))?, 59 | )) 60 | .from("RPCN ".parse().unwrap()) 61 | .subject(subject) 62 | .header(lettre::message::header::ContentType::TEXT_PLAIN) 63 | .message_id(Some(message_id)) 64 | .body(body) 65 | .unwrap(); 66 | self.send_email(email_to_send).map_err(|e| format!("SMTP error: {}", e)) 67 | } 68 | 69 | fn send_token_mail(&self, email_addr: &str, npid: &str, token: &str) -> Result<(), String> { 70 | self.send_email_template(email_addr, npid, "Your token for RPCN", format!("Your token for username {} is:\n{}", npid, token)) 71 | } 72 | 73 | fn send_reset_token_mail(&self, email_addr: &str, npid: &str, reset_token: &str) -> Result<(), String> { 74 | self.send_email_template( 75 | email_addr, 76 | npid, 77 | "Your password reset code for RPCN", 78 | format!("Your password reset code for username {} is:\n{}\n\nNote that this code can only be used once!", npid, reset_token), 79 | ) 80 | } 81 | 82 | pub async fn login(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 83 | let login = data.get_string(false); 84 | let password = data.get_string(false); 85 | let token = data.get_string(true); 86 | let friend_userids: HashMap; 87 | 88 | if data.error() { 89 | warn!("Error while extracting data from Login command"); 90 | return Err(ErrorType::Malformed); 91 | } 92 | 93 | let timestamp; 94 | { 95 | let db = Database::new(self.get_database_connection()?); 96 | 97 | match db.check_user(&login, &password, &token, self.config.read().is_email_validated()) { 98 | Ok(user_data) => { 99 | { 100 | let cleanup_duty = self.shared.cleanup_duty.read(); 101 | if cleanup_duty.contains(&user_data.user_id) { 102 | return Err(ErrorType::LoginAlreadyLoggedIn); 103 | } 104 | } 105 | 106 | let mut client_infos = self.shared.client_infos.write(); 107 | 108 | if client_infos.contains_key(&user_data.user_id) { 109 | return Err(ErrorType::LoginAlreadyLoggedIn); 110 | } 111 | 112 | let rels = db.get_relationships(user_data.user_id).map_err(|_| ErrorType::DbFail)?; 113 | 114 | // Authentified beyond this point 115 | 116 | // Update last login time 117 | db.update_login_time(user_data.user_id).map_err(|_| ErrorType::DbFail)?; 118 | 119 | // Get friends infos 120 | self.authentified = true; 121 | self.client_info.npid = login; 122 | self.client_info.online_name = user_data.online_name.clone(); 123 | self.client_info.avatar_url = user_data.avatar_url.clone(); 124 | self.client_info.user_id = user_data.user_id; 125 | self.client_info.token = user_data.token.clone(); 126 | self.client_info.admin = user_data.admin; 127 | self.client_info.stat_agent = user_data.stat_agent; 128 | self.client_info.banned = user_data.banned; 129 | reply.extend(user_data.online_name.as_bytes()); 130 | reply.push(0); 131 | reply.extend(user_data.avatar_url.as_bytes()); 132 | reply.push(0); 133 | reply.extend(&self.client_info.user_id.to_le_bytes()); 134 | 135 | let dump_usernames = |reply: &mut Vec, v_usernames: &Vec<(i64, String)>| { 136 | reply.extend(&(v_usernames.len() as u32).to_le_bytes()); 137 | for (_userid, username) in v_usernames { 138 | reply.extend(username.as_bytes()); 139 | reply.push(0); 140 | } 141 | }; 142 | 143 | let dump_usernames_and_status = 144 | |reply: &mut Vec, v_usernames: &Vec<(i64, String)>, client_infos: &parking_lot::lock_api::RwLockWriteGuard>| { 145 | reply.extend(&(v_usernames.len() as u32).to_le_bytes()); 146 | 'users_loop: for (user_id, username) in v_usernames { 147 | reply.extend(username.as_bytes()); 148 | reply.push(0); 149 | if let Some(client_info) = client_infos.get(user_id) { 150 | reply.push(1); // status 151 | if let Some(ref presence_info) = client_info.friend_info.read().presence { 152 | presence_info.dump(reply); 153 | continue 'users_loop; 154 | } 155 | } else { 156 | reply.push(0); // status 157 | } 158 | ClientSharedPresence::dump_empty(reply); 159 | } 160 | }; 161 | 162 | timestamp = Client::get_timestamp_nanos(); 163 | 164 | dump_usernames_and_status(reply, &rels.friends, &client_infos); 165 | dump_usernames(reply, &rels.friend_requests); 166 | dump_usernames(reply, &rels.friend_requests_received); 167 | dump_usernames(reply, &rels.blocked); 168 | 169 | friend_userids = rels.friends.iter().map(|v| (*v).clone()).collect(); 170 | 171 | info!("Authentified as {}", &self.client_info.npid); 172 | client_infos.insert(self.client_info.user_id, ClientSharedInfo::new(friend_userids.clone(), self.channel_sender.clone())); 173 | 174 | self.shared.game_tracker.increase_num_users(); 175 | } 176 | Err(e) => { 177 | return Err(match e { 178 | DbError::Empty => ErrorType::LoginInvalidUsername, 179 | DbError::WrongPass => ErrorType::LoginInvalidPassword, 180 | DbError::WrongToken => ErrorType::LoginInvalidToken, 181 | _ => ErrorType::LoginError, 182 | }); 183 | } 184 | } 185 | } 186 | 187 | if self.authentified { 188 | // Notify friends that user has come Online 189 | let notif = Client::create_friend_status_notification(&self.client_info.npid, timestamp, true); 190 | self.send_notification(¬if, &friend_userids.keys().copied().collect()).await; 191 | Ok(ErrorType::NoError) 192 | } else { 193 | Err(ErrorType::LoginError) 194 | } 195 | } 196 | 197 | pub fn is_valid_username(npid: &str) -> bool { 198 | npid.len() >= 3 && npid.len() <= 16 && npid.chars().all(|x| x.is_ascii_alphanumeric() || x == '-' || x == '_') 199 | } 200 | 201 | pub fn create_account(&self, data: &mut StreamExtractor) -> Result { 202 | let npid = data.get_string(false); 203 | let password = data.get_string(false); 204 | let online_name = data.get_string(false); 205 | let avatar_url = data.get_string(false); 206 | let email = data.get_string(false); 207 | 208 | if data.error() { 209 | warn!("Error while extracting data from Create command"); 210 | return Err(ErrorType::Malformed); 211 | } 212 | 213 | if !Client::is_valid_username(&npid) { 214 | warn!("Error validating NpId"); 215 | return Err(ErrorType::InvalidInput); 216 | } 217 | 218 | if !Client::is_valid_username(&online_name) { 219 | warn!("Error validating Online Name"); 220 | return Err(ErrorType::InvalidInput); 221 | } 222 | 223 | let email = email.trim().to_string(); 224 | 225 | if email.parse::().is_err() || email.contains(' ') { 226 | warn!("Invalid email provided: {}", email); 227 | return Err(ErrorType::InvalidInput); 228 | } 229 | 230 | let check_email = strip_email(&email); 231 | 232 | if self.config.read().is_email_validated() { 233 | let tokens: Vec<&str> = check_email.split('@').collect(); 234 | if self.config.read().is_banned_domain(tokens[1]) { 235 | warn!("Attempted to use banned domain: {}", email); 236 | return Err(ErrorType::CreationBannedEmailProvider); 237 | } 238 | } 239 | 240 | let is_admin = self.config.read().get_admins_list().contains(&npid); 241 | 242 | match Database::new(self.get_database_connection()?).add_user(&npid, &password, &online_name, &avatar_url, &email, &check_email, is_admin) { 243 | Ok(token) => { 244 | info!("Successfully created account {}", &npid); 245 | if self.config.read().is_email_validated() { 246 | if let Err(e) = self.send_token_mail(&email, &npid, &token) { 247 | error!("Error sending email: {}", e); 248 | } 249 | } 250 | 251 | // this is not an error, we disconnect the client after account creation, successful or not 252 | Err(ErrorType::NoError) 253 | } 254 | Err(e) => { 255 | warn!("Account creation failed(npid: {})", &npid); 256 | Err(match e { 257 | DbError::ExistingUsername => ErrorType::CreationExistingUsername, 258 | DbError::ExistingEmail => ErrorType::CreationExistingEmail, 259 | _ => ErrorType::CreationError, 260 | }) 261 | } 262 | } 263 | } 264 | 265 | pub fn resend_token(&self, data: &mut StreamExtractor) -> Result { 266 | if !self.config.read().is_email_validated() { 267 | return Err(ErrorType::Invalid); 268 | } 269 | 270 | let login = data.get_string(false); 271 | let password = data.get_string(false); 272 | 273 | if data.error() { 274 | warn!("Error while extracting data from SendToken command"); 275 | return Err(ErrorType::Malformed); 276 | } 277 | 278 | { 279 | let db = Database::new(self.get_database_connection()?); 280 | 281 | if let Ok(user_data) = db.check_user(&login, &password, "", false) { 282 | // Let's check that the email hasn't been sent in the last 24 hours 283 | let last_token_sent_timestamp = db.get_token_sent_time(user_data.user_id).map_err(|_| { 284 | error!("Unexpected error querying last token sent time"); 285 | ErrorType::DbFail 286 | })?; 287 | 288 | // check that a token email hasn't been sent in the last 24 hours 289 | if let Some(last_token_sent_timestamp) = last_token_sent_timestamp { 290 | if (Client::get_timestamp_seconds() - last_token_sent_timestamp) < (24 * 60 * 60) { 291 | warn!("User {} attempted to get token again too soon!", login); 292 | return Err(ErrorType::TooSoon); 293 | } 294 | } 295 | 296 | if let Err(e) = self.send_token_mail(&user_data.email, &login, &user_data.token) { 297 | error!("Error sending email: {}", e); 298 | Err(ErrorType::EmailFail) 299 | } else { 300 | // Update last token sent time 301 | if db.set_token_sent_time(user_data.user_id).is_err() { 302 | error!("Unexpected error setting token sent time"); 303 | } 304 | Err(ErrorType::NoError) 305 | } 306 | } else { 307 | Err(ErrorType::LoginError) 308 | } 309 | } 310 | } 311 | 312 | pub fn send_reset_token(&self, data: &mut StreamExtractor) -> Result { 313 | if !self.config.read().is_email_validated() { 314 | return Err(ErrorType::Invalid); 315 | } 316 | 317 | let username = data.get_string(false); 318 | let email = data.get_string(false); 319 | if data.error() { 320 | warn!("Error while extracting data from SendResetToken command"); 321 | return Err(ErrorType::Malformed); 322 | } 323 | 324 | let db = Database::new(self.get_database_connection()?); 325 | let email_check = strip_email(&email); 326 | 327 | if let Ok((user_id, email_to_use)) = db.check_email(&username, &email_check) { 328 | let last_pass_token_sent_timestamp = db.get_reset_password_token_sent_time(user_id).map_err(|_| { 329 | error!("Unexpected error querying last password token timestamp"); 330 | ErrorType::DbFail 331 | })?; 332 | 333 | // check that a reset token email hasn't been sent in the last 24 hours 334 | if let Some(last_pass_token_sent_timestamp) = last_pass_token_sent_timestamp { 335 | if (Client::get_timestamp_seconds() - last_pass_token_sent_timestamp) < (24 * 60 * 60) { 336 | warn!("User {} attempted to get password reset token again too soon!", username); 337 | return Err(ErrorType::TooSoon); 338 | } 339 | } 340 | 341 | // Generate a new token and update the user entry 342 | let token = db.update_password_token(user_id).map_err(|_| { 343 | error!("Unexpected error updating reset password token"); 344 | ErrorType::DbFail 345 | })?; 346 | 347 | if let Err(e) = self.send_reset_token_mail(&email_to_use, &username, &token) { 348 | error!("Error sending email: {}", e); 349 | Err(ErrorType::EmailFail) 350 | } else { 351 | // Update last token sent time 352 | db.set_reset_password_token_sent_time(user_id).map_err(|_| { 353 | error!("Unexpected error setting new reset password token timestamp"); 354 | ErrorType::DbFail 355 | })?; 356 | Err(ErrorType::NoError) 357 | } 358 | } else { 359 | Err(ErrorType::LoginError) 360 | } 361 | } 362 | 363 | pub fn reset_password(&self, data: &mut StreamExtractor) -> Result { 364 | if !self.config.read().is_email_validated() { 365 | return Err(ErrorType::Invalid); 366 | } 367 | 368 | let username = data.get_string(false); 369 | let token = data.get_string(false); 370 | let new_password = data.get_string(false); 371 | 372 | if data.error() { 373 | warn!("Error while extracting data from ResetPassword command"); 374 | return Err(ErrorType::Malformed); 375 | } 376 | 377 | { 378 | let db = Database::new(self.get_database_connection()?); 379 | 380 | if let Ok(user_id) = db.check_reset_token(&username, &token) { 381 | if db.update_user_password(user_id, &token, &new_password).is_err() { 382 | error!("Unexpected error updating user password!"); 383 | Err(ErrorType::DbFail) 384 | } else { 385 | Err(ErrorType::NoError) 386 | } 387 | } else { 388 | Err(ErrorType::LoginError) 389 | } 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/server/client/cmd_admin.rs: -------------------------------------------------------------------------------- 1 | // Admin Commands 2 | 3 | use crate::server::client::*; 4 | 5 | impl Client { 6 | pub fn req_admin_update_domain_bans(&self) -> Result { 7 | if !self.is_admin() { 8 | return Err(ErrorType::Invalid); 9 | } 10 | 11 | self.config.write().load_domains_banlist(); 12 | Ok(ErrorType::NoError) 13 | } 14 | 15 | pub fn req_admin_terminate_server(&self) -> Result { 16 | if !self.is_admin() { 17 | return Err(ErrorType::Invalid); 18 | } 19 | 20 | // Client always holds both a Receiver and an Arc to sender so it should always succeed 21 | self.terminate_watch.send.lock().send(true).unwrap(); 22 | Ok(ErrorType::NoError) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/server/client/cmd_friend.rs: -------------------------------------------------------------------------------- 1 | // Friend/Block Commands 2 | 3 | use crate::server::client::notifications::NotificationType; 4 | use crate::server::client::*; 5 | use crate::server::database::{DbError, FriendStatus}; 6 | 7 | impl Client { 8 | pub async fn add_friend(&mut self, data: &mut StreamExtractor) -> Result { 9 | let friend_npid = data.get_string(false); 10 | if data.error() { 11 | warn!("Error while extracting data from AddFriend command"); 12 | return Err(ErrorType::Malformed); 13 | } 14 | 15 | let (friend_user_id, status_friend); 16 | { 17 | let db = Database::new(self.get_database_connection()?); 18 | 19 | let friend_user_id_res = db.get_user_id(&friend_npid); 20 | if friend_user_id_res.is_err() { 21 | return Ok(ErrorType::NotFound); 22 | } 23 | 24 | friend_user_id = friend_user_id_res.unwrap(); 25 | if friend_user_id == self.client_info.user_id { 26 | return Ok(ErrorType::InvalidInput); 27 | } 28 | 29 | let rel_status = db.get_rel_status(self.client_info.user_id, friend_user_id); 30 | match rel_status { 31 | Ok((mut status_user, s_friend)) => { 32 | status_friend = s_friend; 33 | // If there is a block, either from current user or person added as friend, query is invalid 34 | if (status_user & (FriendStatus::Blocked as u8)) != 0 || (status_friend & (FriendStatus::Blocked as u8)) != 0 { 35 | return Ok(ErrorType::Blocked); 36 | } 37 | // If user has already requested friendship or is a friend 38 | if (status_user & (FriendStatus::Friend as u8)) != 0 { 39 | return Ok(ErrorType::AlreadyFriend); 40 | } 41 | // Finally just add friendship flag! 42 | status_user |= FriendStatus::Friend as u8; 43 | if let Err(e) = db.set_rel_status(self.client_info.user_id, friend_user_id, status_user, status_friend) { 44 | error!("Unexpected error happened setting relationship status with preexisting status: {:?}", e); 45 | return Err(ErrorType::DbFail); 46 | } 47 | } 48 | Err(DbError::Empty) => { 49 | status_friend = 0; 50 | if let Err(e) = db.set_rel_status(self.client_info.user_id, friend_user_id, FriendStatus::Friend as u8, 0) { 51 | error!("Unexpected error happened creating relationship status: {:?}", e); 52 | return Err(ErrorType::DbFail); 53 | } 54 | } 55 | Err(e) => { 56 | error!("Unexpected result querying relationship status: {:?}", e); 57 | return Err(ErrorType::DbFail); 58 | } 59 | } 60 | } 61 | 62 | // Send notifications as needed 63 | if (status_friend & FriendStatus::Friend as u8) != 0 { 64 | // If user accepted friend request from friend send FriendNew notification 65 | // Update signaling infos 66 | let friend_online: bool; 67 | { 68 | let client_infos = self.shared.client_infos.read(); 69 | if let Some(user_info) = client_infos.get(&self.client_info.user_id) { 70 | let mut user_fi = user_info.friend_info.write(); 71 | user_fi.friends.insert(friend_user_id, friend_npid.clone()); 72 | } 73 | if let Some(other_user_info) = client_infos.get(&friend_user_id) { 74 | let mut other_user_fi = other_user_info.friend_info.write(); 75 | other_user_fi.friends.insert(self.client_info.user_id, self.client_info.npid.clone()); 76 | friend_online = true; 77 | } else { 78 | friend_online = false; 79 | } 80 | } 81 | 82 | // Send notification to user that he has a new friend! 83 | let self_n = Client::create_new_friend_notification(&friend_npid, friend_online); 84 | self.self_notification(&self_n); 85 | if friend_online { 86 | // Send notification to friend that he has a new friend! 87 | let friend_n = Client::create_new_friend_notification(&self.client_info.npid, true); 88 | self.send_single_notification(&friend_n, friend_user_id).await; 89 | } 90 | } else { 91 | // Else send friend the FriendQuery notification 92 | let mut n_msg: Vec = Vec::new(); 93 | n_msg.extend(self.client_info.npid.as_bytes()); 94 | n_msg.push(0); 95 | let friend_n = Client::create_notification(NotificationType::FriendQuery, &n_msg); 96 | self.send_single_notification(&friend_n, friend_user_id).await; 97 | } 98 | 99 | Ok(ErrorType::NoError) 100 | } 101 | 102 | pub async fn remove_friend(&mut self, data: &mut StreamExtractor) -> Result { 103 | let friend_npid = data.get_string(false); 104 | if data.error() { 105 | warn!("Error while extracting data from RemoveFriend command"); 106 | return Err(ErrorType::Malformed); 107 | } 108 | 109 | let friend_user_id; 110 | { 111 | let db = Database::new(self.get_database_connection()?); 112 | 113 | let friend_user_id_res = db.get_user_id(&friend_npid); 114 | if friend_user_id_res.is_err() { 115 | return Ok(ErrorType::NotFound); 116 | } 117 | 118 | friend_user_id = friend_user_id_res.unwrap(); 119 | if friend_user_id == self.client_info.user_id { 120 | return Ok(ErrorType::InvalidInput); 121 | } 122 | 123 | let rel_status = db.get_rel_status(self.client_info.user_id, friend_user_id); 124 | match rel_status { 125 | Ok((mut status_user, mut status_friend)) => { 126 | // Check that some friendship relationship exist 127 | if ((status_user & (FriendStatus::Friend as u8)) | (status_friend & (FriendStatus::Friend as u8))) == 0 { 128 | return Ok(ErrorType::NotFound); 129 | } 130 | // Remove friendship flag from relationship 131 | status_user &= !(FriendStatus::Friend as u8); 132 | status_friend &= !(FriendStatus::Friend as u8); 133 | if let Err(e) = db.set_rel_status(self.client_info.user_id, friend_user_id, status_user, status_friend) { 134 | error!("Unexpected error happened setting relationship status with preexisting status: {:?}", e); 135 | return Err(ErrorType::DbFail); 136 | } 137 | } 138 | Err(DbError::Empty) => { 139 | return Ok(ErrorType::NotFound); 140 | } 141 | Err(e) => { 142 | error!("Unexpected result querying relationship status: {:?}", e); 143 | return Err(ErrorType::DbFail); 144 | } 145 | } 146 | } 147 | 148 | // Send notifications as needed 149 | // Send to user 150 | let mut n_msg: Vec = Vec::new(); 151 | n_msg.extend(friend_npid.as_bytes()); 152 | n_msg.push(0); 153 | let self_n = Client::create_notification(NotificationType::FriendLost, &n_msg); 154 | self.self_notification(&self_n); 155 | // Send to ex-friend too 156 | n_msg.clear(); 157 | n_msg.extend(self.client_info.npid.as_bytes()); 158 | n_msg.push(0); 159 | let friend_n = Client::create_notification(NotificationType::FriendLost, &n_msg); 160 | self.send_single_notification(&friend_n, friend_user_id).await; 161 | // Update signaling infos 162 | let client_infos = self.shared.client_infos.read(); 163 | if let Some(client_info) = client_infos.get(&self.client_info.user_id) { 164 | let mut user_fi = client_info.friend_info.write(); 165 | user_fi.friends.remove(&friend_user_id); 166 | } 167 | if let Some(other_user_info) = client_infos.get(&friend_user_id) { 168 | let mut other_user_fi = other_user_info.friend_info.write(); 169 | other_user_fi.friends.remove(&self.client_info.user_id); 170 | } 171 | 172 | Ok(ErrorType::NoError) 173 | } 174 | 175 | pub fn add_block(&mut self, _data: &mut StreamExtractor, _reply: &mut Vec) -> Result { 176 | // TODO 177 | Ok(ErrorType::NoError) 178 | } 179 | 180 | pub fn remove_block(&mut self, _data: &mut StreamExtractor, _reply: &mut Vec) -> Result { 181 | // TODO 182 | Ok(ErrorType::NoError) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/server/client/cmd_misc.rs: -------------------------------------------------------------------------------- 1 | use crate::server::client::*; 2 | 3 | const SCE_NP_BASIC_PRESENCE_TITLE_SIZE_MAX: usize = 128; 4 | const SCE_NP_BASIC_PRESENCE_EXTENDED_STATUS_SIZE_MAX: usize = 192; 5 | const SCE_NP_BASIC_PRESENCE_COMMENT_SIZE_MAX: usize = 64; 6 | const SCE_NP_BASIC_MAX_PRESENCE_SIZE: usize = 128; 7 | 8 | #[allow(dead_code)] 9 | #[allow(non_camel_case_types)] 10 | #[repr(u16)] 11 | enum MessageMainType { 12 | SCE_NP_BASIC_MESSAGE_MAIN_TYPE_DATA_ATTACHMENT = 0, 13 | SCE_NP_BASIC_MESSAGE_MAIN_TYPE_GENERAL = 1, 14 | SCE_NP_BASIC_MESSAGE_MAIN_TYPE_ADD_FRIEND = 2, 15 | SCE_NP_BASIC_MESSAGE_MAIN_TYPE_INVITE = 3, 16 | } 17 | 18 | impl Client { 19 | pub async fn req_signaling_infos(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 20 | let npid = data.get_string(false); 21 | if data.error() || npid.len() > 16 { 22 | warn!("Error while extracting data from RequestSignalingInfos command"); 23 | return Err(ErrorType::Malformed); 24 | } 25 | 26 | let user_id = Database::new(self.get_database_connection()?).get_user_id(&npid); 27 | if user_id.is_err() { 28 | return Ok(ErrorType::NotFound); 29 | } 30 | let user_id = user_id.unwrap(); 31 | 32 | let (caller_sig_infos, op_sig_infos); 33 | { 34 | let client_infos = self.shared.client_infos.read(); 35 | 36 | caller_sig_infos = client_infos.get(&self.client_info.user_id).unwrap().signaling_info.read().clone(); 37 | 38 | let client_info = client_infos.get(&user_id); 39 | if client_info.is_none() { 40 | return Ok(ErrorType::NotFound); 41 | } 42 | let client_info = client_info.unwrap(); 43 | 44 | op_sig_infos = client_info.signaling_info.read().clone(); 45 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 46 | 47 | if caller_sig_infos.addr_p2p_ipv4.0 == op_sig_infos.addr_p2p_ipv4.0 { 48 | Client::add_data_packet(reply, &Client::build_signaling_addr(&mut builder, &op_sig_infos.local_addr_p2p, 3658)); 49 | info!("Requesting signaling infos for {} => (local) {:?}:{}", &npid, &op_sig_infos.local_addr_p2p, 3658); 50 | // Don't send notification for local addresses as connectivity shouldn't be an issue 51 | return Ok(ErrorType::NoError); 52 | } else { 53 | match (caller_sig_infos.addr_p2p_ipv6, op_sig_infos.addr_p2p_ipv6) { 54 | (Some(_), Some((op_ipv6, op_port_ipv6))) => { 55 | Client::add_data_packet(reply, &Client::build_signaling_addr(&mut builder, &op_ipv6, op_port_ipv6)); 56 | info!("Requesting signaling infos for {} => (extern ipv6) {:?}:{}", &npid, &op_ipv6, op_port_ipv6); 57 | } 58 | _ => { 59 | Client::add_data_packet(reply, &Client::build_signaling_addr(&mut builder, &op_sig_infos.addr_p2p_ipv4.0, op_sig_infos.addr_p2p_ipv4.1)); 60 | info!( 61 | "Requesting signaling infos for {} => (extern ipv4) {:?}:{}", 62 | &npid, &op_sig_infos.addr_p2p_ipv4.0, op_sig_infos.addr_p2p_ipv4.1 63 | ); 64 | } 65 | } 66 | } 67 | } 68 | 69 | // Send a notification to the target to help with connectivity 70 | let mut notif_builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 71 | let npid_str = notif_builder.create_string(&self.client_info.npid); 72 | let addr = match (caller_sig_infos.addr_p2p_ipv6, op_sig_infos.addr_p2p_ipv6) { 73 | (Some((caller_ipv6, caller_port_ipv6)), Some(_)) => Client::make_signaling_addr(&mut notif_builder, &caller_ipv6, caller_port_ipv6), 74 | _ => Client::make_signaling_addr(&mut notif_builder, &caller_sig_infos.addr_p2p_ipv4.0, caller_sig_infos.addr_p2p_ipv4.1), 75 | }; 76 | 77 | let matching_addr = MatchingSignalingInfo::create( 78 | &mut notif_builder, 79 | &MatchingSignalingInfoArgs { 80 | npid: Some(npid_str), 81 | addr: Some(addr), 82 | }, 83 | ); 84 | 85 | notif_builder.finish(matching_addr, None); 86 | let vec_notif = notif_builder.finished_data().to_vec(); 87 | 88 | let mut n_msg: Vec = Vec::with_capacity(size_of::() + vec_notif.len()); 89 | Client::add_data_packet(&mut n_msg, &vec_notif); 90 | let friend_n = Client::create_notification(NotificationType::SignalingHelper, &n_msg); 91 | self.send_single_notification(&friend_n, user_id).await; 92 | Ok(ErrorType::NoError) 93 | } 94 | 95 | pub fn req_ticket(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 96 | let service_id = data.get_string(false); 97 | let cookie = data.get_rawdata(); 98 | 99 | if data.error() { 100 | warn!("Error while extracting data from RequestTicket command"); 101 | return Err(ErrorType::Malformed); 102 | } 103 | 104 | info!("Requested a ticket for <{}>", service_id); 105 | 106 | if let Some(ref mut cur_service_id) = self.current_game.1 { 107 | if *cur_service_id != service_id { 108 | self.shared.game_tracker.decrease_count_ticket(cur_service_id); 109 | *cur_service_id = service_id.clone(); 110 | self.shared.game_tracker.increase_count_ticket(&service_id); 111 | } 112 | } else { 113 | self.current_game.1 = Some(service_id.clone()); 114 | self.shared.game_tracker.increase_count_ticket(&service_id); 115 | } 116 | 117 | let ticket; 118 | { 119 | let config = self.config.read(); 120 | let sign_info = config.get_ticket_signing_info(); 121 | ticket = Ticket::new(self.client_info.user_id as u64, &self.client_info.npid, &service_id, cookie, sign_info); 122 | } 123 | let ticket_blob = ticket.generate_blob(); 124 | 125 | Client::add_data_packet(reply, &ticket_blob); 126 | 127 | Ok(ErrorType::NoError) 128 | } 129 | 130 | pub async fn send_message(&mut self, data: &mut StreamExtractor) -> Result { 131 | let sendmessage_req = data.get_flatbuffer::(); 132 | if data.error() || sendmessage_req.is_err() { 133 | warn!("Error while extracting data from SendMessage command"); 134 | return Err(ErrorType::Malformed); 135 | } 136 | let sendmessage_req = sendmessage_req.unwrap(); 137 | 138 | let message = Client::validate_and_unwrap(sendmessage_req.message())?; 139 | let npids = Client::validate_and_unwrap(sendmessage_req.npids())?; 140 | 141 | // Get all the IDs 142 | let mut ids = HashSet::new(); 143 | { 144 | let db = Database::new(self.get_database_connection()?); 145 | for npid in &npids { 146 | match db.get_user_id(npid) { 147 | Ok(id) => { 148 | ids.insert(id); 149 | } 150 | Err(_) => { 151 | warn!("Requested to send a message to invalid npid: {}", npid); 152 | return Ok(ErrorType::InvalidInput); 153 | } 154 | } 155 | } 156 | } 157 | 158 | // Can't message self 159 | if ids.is_empty() || ids.contains(&self.client_info.user_id) { 160 | warn!("Requested to send a message to empty set or self!"); 161 | return Ok(ErrorType::InvalidInput); 162 | } 163 | 164 | // Ensure all recipients are friends for invite messages(is this necessary?) 165 | let msg = flatbuffers::root::(message.bytes()).map_err(|e| { 166 | warn!("MessageDetails was malformed: {}", e); 167 | ErrorType::Malformed 168 | })?; 169 | if msg.mainType() == MessageMainType::SCE_NP_BASIC_MESSAGE_MAIN_TYPE_INVITE as u16 { 170 | let client_infos = self.shared.client_infos.read(); 171 | let client_fi = client_infos.get(&self.client_info.user_id).unwrap().friend_info.read(); 172 | if !client_fi.friends.keys().copied().collect::>().is_superset(&ids) { 173 | warn!("Requested to send a message to a non-friend!"); 174 | return Ok(ErrorType::InvalidInput); 175 | } 176 | } 177 | 178 | // Finally send the notifications 179 | let mut n_msg: Vec = Vec::new(); 180 | n_msg.extend(self.client_info.npid.as_bytes()); 181 | n_msg.push(0); 182 | n_msg.extend(&(message.len() as u32).to_le_bytes()); 183 | n_msg.extend(message); 184 | let notif = Client::create_notification(NotificationType::MessageReceived, &n_msg); 185 | self.send_notification(¬if, &ids).await; 186 | 187 | Ok(ErrorType::NoError) 188 | } 189 | 190 | pub fn get_network_time(&self, reply: &mut Vec) -> Result { 191 | reply.extend(Client::get_psn_timestamp().to_le_bytes()); 192 | Ok(ErrorType::NoError) 193 | } 194 | 195 | pub async fn set_presence(&mut self, data: &mut StreamExtractor) -> Result { 196 | let (com_id, pr_req) = self.get_com_and_fb::(data)?; 197 | let title = Client::validate_and_unwrap(pr_req.title())?; 198 | 199 | let notify: Option<(ClientSharedPresence, HashSet)>; 200 | 201 | { 202 | let client_infos = self.shared.client_infos.read(); 203 | let mut friend_info = client_infos.get(&self.client_info.user_id).unwrap().friend_info.write(); 204 | 205 | if let Some(ref mut presence_info) = friend_info.presence { 206 | if presence_info.communication_id != com_id 207 | || presence_info.title != title 208 | || pr_req.status().is_some_and(|status| status != presence_info.status) 209 | || pr_req.comment().is_some_and(|comment| comment != presence_info.comment) 210 | || pr_req.data().is_some_and(|data| data.bytes() != presence_info.data) 211 | { 212 | presence_info.communication_id = com_id; 213 | presence_info.title = title.to_string(); 214 | presence_info.title.truncate(SCE_NP_BASIC_PRESENCE_TITLE_SIZE_MAX - 1); 215 | 216 | self.shared.game_tracker.add_gamename_hint(&com_id, title); 217 | 218 | if let Some(status) = pr_req.status() { 219 | presence_info.status = status.to_string(); 220 | presence_info.status.truncate(SCE_NP_BASIC_PRESENCE_EXTENDED_STATUS_SIZE_MAX - 1); 221 | } 222 | 223 | if let Some(comment) = pr_req.comment() { 224 | presence_info.comment = comment.to_string(); 225 | presence_info.status.truncate(SCE_NP_BASIC_PRESENCE_COMMENT_SIZE_MAX - 1); 226 | } 227 | 228 | if let Some(data) = pr_req.data() { 229 | presence_info.data = data.bytes().to_vec(); 230 | presence_info.data.truncate(SCE_NP_BASIC_MAX_PRESENCE_SIZE); 231 | } 232 | 233 | notify = Some((presence_info.clone(), friend_info.friends.keys().copied().collect())); 234 | } else { 235 | notify = None; 236 | } 237 | } else { 238 | let new_presence = ClientSharedPresence::new(com_id, title, pr_req.status(), pr_req.comment(), pr_req.data().map(|v| v.bytes())); 239 | friend_info.presence = Some(new_presence.clone()); 240 | notify = Some((new_presence, friend_info.friends.keys().copied().collect())); 241 | } 242 | } 243 | 244 | if let Some((presence, user_ids)) = notify { 245 | let mut n_msg: Vec = Vec::new(); 246 | n_msg.extend(self.client_info.npid.as_bytes()); 247 | n_msg.push(0); 248 | presence.dump(&mut n_msg); 249 | let notif = Client::create_notification(NotificationType::FriendPresenceChanged, &n_msg); 250 | self.send_notification(¬if, &user_ids).await; 251 | } 252 | 253 | Ok(ErrorType::NoError) 254 | } 255 | 256 | pub async fn reset_state(&mut self) -> Result { 257 | Client::clean_user_state(self).await; 258 | Ok(ErrorType::NoError) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/server/client/cmd_room.rs: -------------------------------------------------------------------------------- 1 | // Room Commands 2 | 3 | use crate::server::client::*; 4 | 5 | impl Client { 6 | pub fn req_create_room(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 7 | let (com_id, create_req) = self.get_com_and_fb::(data)?; 8 | 9 | let server_id = Database::new(self.get_database_connection()?) 10 | .get_corresponding_server(&com_id, create_req.worldId(), create_req.lobbyId()) 11 | .map_err(|_| { 12 | warn!( 13 | "Attempted to use invalid worldId/lobbyId for comId {}: {}/{}", 14 | &com_id_to_string(&com_id), 15 | create_req.worldId(), 16 | create_req.lobbyId() 17 | ); 18 | ErrorType::InvalidInput 19 | })?; 20 | 21 | let signaling_info = if let Some(sig_info) = self.shared.client_infos.read().get(&self.client_info.user_id) { 22 | sig_info.signaling_info.read().clone() 23 | } else { 24 | warn!("User created a room with no signaling info!"); 25 | ClientSharedSignalingInfo::new() 26 | }; 27 | 28 | let resp = self.shared.room_manager.write().create_room(&com_id, &create_req, &self.client_info, server_id, signaling_info); 29 | 30 | match resp { 31 | Err(ErrorType::Malformed) => Err(ErrorType::Malformed), 32 | Err(e) => Ok(e), 33 | Ok(resp) => { 34 | Client::add_data_packet(reply, &resp); 35 | Ok(ErrorType::NoError) 36 | } 37 | } 38 | } 39 | 40 | pub async fn req_join_room(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 41 | let (com_id, join_req) = self.get_com_and_fb::(data)?; 42 | let room_id = join_req.roomId(); 43 | 44 | let (response, notifications); 45 | { 46 | let mut room_manager = self.shared.room_manager.write(); 47 | if !room_manager.room_exists(&com_id, room_id) { 48 | warn!("User requested to join a room that doesn't exist!"); 49 | return Ok(ErrorType::RoomMissing); 50 | } 51 | 52 | let users = { 53 | let room = room_manager.get_room(&com_id, room_id); 54 | room.get_room_users() 55 | }; 56 | 57 | if users.iter().any(|x| *x.1 == self.client_info.user_id) { 58 | warn!("User tried to join a room he was already a member of!"); 59 | return Ok(ErrorType::RoomAlreadyJoined); 60 | } 61 | 62 | let signaling_info = if let Some(sig_info) = self.shared.client_infos.read().get(&self.client_info.user_id) { 63 | sig_info.signaling_info.read().clone() 64 | } else { 65 | warn!("User joined a room with no signaling info!"); 66 | ClientSharedSignalingInfo::new() 67 | }; 68 | 69 | let resp = room_manager.join_room(&com_id, &join_req, &self.client_info, signaling_info); 70 | match resp { 71 | Err(ErrorType::Malformed) => return Err(ErrorType::Malformed), 72 | Err(e) => return Ok(e), 73 | Ok((resp, noti)) => { 74 | (response, notifications) = (resp, noti); 75 | } 76 | } 77 | } 78 | 79 | Client::add_data_packet(reply, &response); 80 | 81 | for (notification_data, user_ids) in notifications.into_iter().flatten() { 82 | let mut n_msg = Vec::with_capacity(notification_data.len() + size_of::()); 83 | Client::add_data_packet(&mut n_msg, ¬ification_data); 84 | let notif = Client::create_notification(NotificationType::UserJoinedRoom, &n_msg); 85 | self.send_notification(¬if, &user_ids).await; 86 | } 87 | 88 | Ok(ErrorType::NoError) 89 | } 90 | 91 | pub async fn leave_room(&self, com_id: &ComId, room_id: u64, opt_data: Option<&PresenceOptionData<'_>>, event_cause: EventCause) -> ErrorType { 92 | let (destroyed, users, user_data); 93 | { 94 | let mut room_manager = self.shared.room_manager.write(); 95 | if !room_manager.room_exists(com_id, room_id) { 96 | return ErrorType::RoomMissing; 97 | } 98 | 99 | let room = room_manager.get_room(com_id, room_id); 100 | let member_id = room.get_member_id(self.client_info.user_id); 101 | if let Err(e) = member_id { 102 | return e; 103 | } 104 | 105 | // We get this in advance in case the room is not destroyed 106 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 107 | let wip_user_data = room.get_room_member_update_info(&mut builder, member_id.unwrap(), event_cause.clone(), opt_data); 108 | builder.finish(wip_user_data, None); 109 | user_data = builder.finished_data().to_vec(); 110 | 111 | let res = room_manager.leave_room(com_id, room_id, self.client_info.user_id); 112 | if let Err(e) = res { 113 | return e; 114 | } 115 | let (destroyed_toa, users_toa) = res.unwrap(); 116 | destroyed = destroyed_toa; 117 | users = users_toa; 118 | } 119 | 120 | if destroyed { 121 | // Notify other room users that the room has been destroyed 122 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 123 | let opt_data = dc_opt_data(&mut builder, opt_data); 124 | let room_update = RoomUpdateInfo::create( 125 | &mut builder, 126 | &RoomUpdateInfoArgs { 127 | eventCause: event_cause as u8, 128 | errorCode: 0, 129 | optData: Some(opt_data), 130 | }, 131 | ); 132 | builder.finish(room_update, None); 133 | let room_update_data = builder.finished_data().to_vec(); 134 | 135 | let mut n_msg: Vec = Vec::new(); 136 | n_msg.extend(&room_id.to_le_bytes()); 137 | Client::add_data_packet(&mut n_msg, &room_update_data); 138 | 139 | let notif = Client::create_notification(NotificationType::RoomDestroyed, &n_msg); 140 | self.send_notification(¬if, &users).await; 141 | } else { 142 | // Notify other room users that someone left the room 143 | let mut n_msg: Vec = Vec::new(); 144 | n_msg.extend(&room_id.to_le_bytes()); 145 | Client::add_data_packet(&mut n_msg, &user_data); 146 | 147 | let notif = Client::create_notification(NotificationType::UserLeftRoom, &n_msg); 148 | self.send_notification(¬if, &users).await; 149 | } 150 | 151 | ErrorType::NoError 152 | } 153 | 154 | pub async fn req_leave_room(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 155 | let (com_id, leave_req) = self.get_com_and_fb::(data)?; 156 | 157 | let res = self.leave_room(&com_id, leave_req.roomId(), Some(&leave_req.optData().unwrap()), EventCause::LeaveAction).await; 158 | reply.extend(&leave_req.roomId().to_le_bytes()); 159 | Ok(res) 160 | } 161 | pub fn req_search_room(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 162 | let (com_id, search_req) = self.get_com_and_fb::(data)?; 163 | 164 | let resp = self.shared.room_manager.read().search_room(&com_id, &search_req); 165 | Client::add_data_packet(reply, &resp); 166 | Ok(ErrorType::NoError) 167 | } 168 | pub fn req_get_roomdata_external_list(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 169 | let (com_id, getdata_req) = self.get_com_and_fb::(data)?; 170 | 171 | let resp = self.shared.room_manager.read().get_roomdata_external_list(&com_id, &getdata_req); 172 | 173 | Client::add_data_packet(reply, &resp); 174 | 175 | Ok(ErrorType::NoError) 176 | } 177 | 178 | pub fn req_set_roomdata_external(&mut self, data: &mut StreamExtractor) -> Result { 179 | let (com_id, setdata_req) = self.get_com_and_fb::(data)?; 180 | 181 | if let Err(e) = self.shared.room_manager.write().set_roomdata_external(&com_id, &setdata_req, self.client_info.user_id) { 182 | Ok(e) 183 | } else { 184 | Ok(ErrorType::NoError) 185 | } 186 | } 187 | pub fn req_get_roomdata_internal(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 188 | let (com_id, getdata_req) = self.get_com_and_fb::(data)?; 189 | 190 | let resp = self.shared.room_manager.read().get_roomdata_internal(&com_id, &getdata_req); 191 | if let Err(e) = resp { 192 | return Ok(e); 193 | } 194 | 195 | let resp = resp.unwrap(); 196 | Client::add_data_packet(reply, &resp); 197 | Ok(ErrorType::NoError) 198 | } 199 | pub async fn req_set_roomdata_internal(&mut self, data: &mut StreamExtractor) -> Result { 200 | let (com_id, setdata_req) = self.get_com_and_fb::(data)?; 201 | 202 | let room_id = setdata_req.roomId(); 203 | let res = self.shared.room_manager.write().set_roomdata_internal(&com_id, &setdata_req, self.client_info.user_id); 204 | 205 | match res { 206 | Ok(Some((users, notif_data))) => { 207 | let mut n_msg: Vec = Vec::new(); 208 | n_msg.extend(&room_id.to_le_bytes()); 209 | Client::add_data_packet(&mut n_msg, ¬if_data); 210 | let notif = Client::create_notification(NotificationType::UpdatedRoomDataInternal, &n_msg); 211 | self.send_notification(¬if, &users).await; 212 | self.self_notification(¬if); 213 | Ok(ErrorType::NoError) 214 | } 215 | Ok(None) => Ok(ErrorType::NoError), 216 | Err(e) => Ok(e), 217 | } 218 | } 219 | 220 | pub async fn req_get_roommemberdata_internal(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 221 | let (com_id, getdata_req) = self.get_com_and_fb::(data)?; 222 | 223 | let resp = self.shared.room_manager.read().get_roommemberdata_internal(&com_id, &getdata_req); 224 | 225 | if let Err(e) = resp { 226 | return Ok(e); 227 | } 228 | let resp = resp.unwrap(); 229 | 230 | Client::add_data_packet(reply, &resp); 231 | Ok(ErrorType::NoError) 232 | } 233 | 234 | pub async fn req_set_roommemberdata_internal(&mut self, data: &mut StreamExtractor) -> Result { 235 | let (com_id, setdata_req) = self.get_com_and_fb::(data)?; 236 | 237 | let room_id = setdata_req.roomId(); 238 | let res = self.shared.room_manager.write().set_roommemberdata_internal(&com_id, &setdata_req, self.client_info.user_id); 239 | 240 | match res { 241 | Ok((users, notif_data)) => { 242 | let mut n_msg: Vec = Vec::new(); 243 | n_msg.extend(&room_id.to_le_bytes()); 244 | Client::add_data_packet(&mut n_msg, ¬if_data); 245 | let notif = Client::create_notification(NotificationType::UpdatedRoomMemberDataInternal, &n_msg); 246 | self.send_notification(¬if, &users).await; 247 | self.self_notification(¬if); 248 | Ok(ErrorType::NoError) 249 | } 250 | Err(e) => Ok(e), 251 | } 252 | } 253 | 254 | pub fn req_ping_room_owner(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 255 | let com_id = self.get_com_id_with_redir(data); 256 | let room_id = data.get::(); 257 | if data.error() { 258 | warn!("Error while extracting data from PingRoomOwner command"); 259 | return Err(ErrorType::Malformed); 260 | } 261 | 262 | let infos = self.shared.room_manager.read().get_room_infos(&com_id, room_id); 263 | if let Err(e) = infos { 264 | return Ok(e); 265 | } 266 | let (server_id, world_id, _) = infos.unwrap(); 267 | 268 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 269 | let resp = GetPingInfoResponse::create( 270 | &mut builder, 271 | &GetPingInfoResponseArgs { 272 | serverId: server_id, 273 | worldId: world_id, 274 | roomId: room_id, 275 | rtt: 20000, 276 | }, 277 | ); 278 | 279 | builder.finish(resp, None); 280 | let finished_data = builder.finished_data().to_vec(); 281 | Client::add_data_packet(reply, &finished_data); 282 | 283 | Ok(ErrorType::NoError) 284 | } 285 | 286 | pub async fn req_send_room_message(&mut self, data: &mut StreamExtractor) -> Result { 287 | let (com_id, msg_req) = self.get_com_and_fb::(data)?; 288 | 289 | let room_id = msg_req.roomId(); 290 | let (notif, member_id, users); 291 | let mut dst_vec: Vec = Vec::new(); 292 | { 293 | let room_manager = self.shared.room_manager.read(); 294 | if !room_manager.room_exists(&com_id, room_id) { 295 | warn!("User requested to send a message to a room that doesn't exist!"); 296 | return Ok(ErrorType::RoomMissing); 297 | } 298 | { 299 | let room = room_manager.get_room(&com_id, room_id); 300 | let m_id = room.get_member_id(self.client_info.user_id); 301 | if m_id.is_err() { 302 | warn!("User requested to send a message to a room that he's not a member of!"); 303 | return Ok(ErrorType::Unauthorized); 304 | } 305 | member_id = m_id.unwrap(); 306 | users = room.get_room_users(); 307 | } 308 | 309 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 310 | 311 | if let Some(dst) = msg_req.dst() { 312 | for i in 0..dst.len() { 313 | dst_vec.push(dst.get(i)); 314 | } 315 | } 316 | let dst = Some(builder.create_vector(&dst_vec)); 317 | 318 | let mut npid = None; 319 | if (msg_req.option() & 0x01) != 0 { 320 | npid = Some(builder.create_string(&self.client_info.npid)); 321 | } 322 | let mut online_name = None; 323 | if (msg_req.option() & 0x02) != 0 { 324 | online_name = Some(builder.create_string(&self.client_info.online_name)); 325 | } 326 | let mut avatar_url = None; 327 | if (msg_req.option() & 0x04) != 0 { 328 | avatar_url = Some(builder.create_string(&self.client_info.avatar_url)); 329 | } 330 | 331 | let src_user_info = UserInfo::create( 332 | &mut builder, 333 | &UserInfoArgs { 334 | npId: npid, 335 | onlineName: online_name, 336 | avatarUrl: avatar_url, 337 | }, 338 | ); 339 | 340 | let msg_vec: Vec; 341 | if let Some(msg) = msg_req.msg() { 342 | msg_vec = msg.iter().collect(); 343 | } else { 344 | msg_vec = Vec::new(); 345 | } 346 | let msg = Some(builder.create_vector(&msg_vec)); 347 | 348 | let resp = RoomMessageInfo::create( 349 | &mut builder, 350 | &RoomMessageInfoArgs { 351 | filtered: false, 352 | castType: msg_req.castType(), 353 | dst, 354 | srcMember: Some(src_user_info), 355 | msg, 356 | }, 357 | ); 358 | builder.finish(resp, None); 359 | let finished_data = builder.finished_data().to_vec(); 360 | 361 | let mut n_msg: Vec = Vec::new(); 362 | n_msg.extend(&room_id.to_le_bytes()); 363 | n_msg.extend(&member_id.to_le_bytes()); 364 | Client::add_data_packet(&mut n_msg, &finished_data); 365 | notif = Client::create_notification(NotificationType::RoomMessageReceived, &n_msg); 366 | } 367 | 368 | match msg_req.castType() { 369 | 1 => { 370 | // SCE_NP_MATCHING2_CASTTYPE_BROADCAST 371 | let user_ids: HashSet = users.iter().filter_map(|x| if *x.1 != self.client_info.user_id { Some(*x.1) } else { None }).collect(); 372 | self.send_notification(¬if, &user_ids).await; 373 | self.self_notification(¬if); 374 | } 375 | 2 | 3 => { 376 | // SCE_NP_MATCHING2_CASTTYPE_UNICAST & SCE_NP_MATCHING2_CASTTYPE_MULTICAST 377 | let mut found_self = false; 378 | let user_ids: HashSet = users 379 | .iter() 380 | .filter_map(|x| { 381 | if !dst_vec.iter().any(|dst| *dst == *x.0) { 382 | None 383 | } else if *x.1 != self.client_info.user_id { 384 | Some(*x.1) 385 | } else { 386 | found_self = true; 387 | None 388 | } 389 | }) 390 | .collect(); 391 | self.send_notification(¬if, &user_ids).await; 392 | if found_self { 393 | self.self_notification(¬if); 394 | }; 395 | } 396 | 4 => { 397 | // SCE_NP_MATCHING2_CASTTYPE_MULTICAST_TEAM 398 | return Ok(ErrorType::Unsupported); 399 | } 400 | _ => { 401 | warn!("Invalid broadcast type in send_room_message!"); 402 | return Err(ErrorType::InvalidInput); // This shouldn't happen, closing connection 403 | } 404 | } 405 | 406 | Ok(ErrorType::NoError) 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/server/client/cmd_room_gui.rs: -------------------------------------------------------------------------------- 1 | // Room Commands for old GUI api 2 | 3 | use crate::server::{client::*, gui_room_manager::GuiRoomId}; 4 | 5 | impl Client { 6 | pub fn req_create_room_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 7 | let (com_id, create_req) = self.get_com_and_fb::(data)?; 8 | 9 | let resp = self.shared.gui_room_manager.write().create_room_gui(&com_id, &create_req, &self.client_info); 10 | Client::add_data_packet(reply, &resp); 11 | Ok(ErrorType::NoError) 12 | } 13 | 14 | pub async fn req_join_room_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 15 | let join_req = self.get_fb::(data)?; 16 | 17 | let room_id = GuiRoomManager::fb_vec_to_room_id(join_req.id())?; 18 | let res = self.shared.gui_room_manager.write().join_room_gui(&room_id, &self.client_info); 19 | match res { 20 | Ok((resp, notif_users, notif_data)) => { 21 | let mut n_msg: Vec = Vec::with_capacity(notif_data.len() + size_of::()); 22 | Client::add_data_packet(&mut n_msg, ¬if_data); 23 | let notif = Client::create_notification(NotificationType::MemberJoinedRoomGUI, &n_msg); 24 | self.send_notification(¬if, ¬if_users).await; 25 | 26 | Client::add_data_packet(reply, &resp); 27 | Ok(ErrorType::NoError) 28 | } 29 | Err(e) => Ok(e), 30 | } 31 | } 32 | 33 | pub async fn leave_room_gui(&mut self, room_id: &GuiRoomId) -> Result, ErrorType> { 34 | let (resp, notif_ids, notifs) = self.shared.gui_room_manager.write().leave_room_gui(room_id, &self.client_info)?; 35 | 36 | for (notif_type, notif_data) in ¬ifs { 37 | let mut n_msg: Vec = Vec::with_capacity(notif_data.len() + size_of::()); 38 | Client::add_data_packet(&mut n_msg, notif_data); 39 | let notif_vec = Client::create_notification(*notif_type, &n_msg); 40 | self.send_notification(¬if_vec, ¬if_ids).await; 41 | } 42 | 43 | Ok(resp) 44 | } 45 | 46 | pub async fn req_leave_room_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 47 | let leave_req = self.get_fb::(data)?; 48 | let room_id = GuiRoomManager::fb_vec_to_room_id(leave_req.id())?; 49 | 50 | match self.leave_room_gui(&room_id).await { 51 | Ok(data) => Client::add_data_packet(reply, &data), 52 | Err(e) => return Ok(e), 53 | } 54 | 55 | Ok(ErrorType::NoError) 56 | } 57 | 58 | pub fn req_get_room_list_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 59 | let (com_id, search_req) = self.get_com_and_fb::(data)?; 60 | 61 | let resp = self.shared.gui_room_manager.read().get_room_list_gui(&com_id, &search_req); 62 | Client::add_data_packet(reply, &resp); 63 | Ok(ErrorType::NoError) 64 | } 65 | 66 | pub fn req_set_room_search_flag_gui(&mut self, data: &mut StreamExtractor) -> Result { 67 | let set_req = self.get_fb::(data)?; 68 | let room_id = GuiRoomManager::fb_vec_to_room_id(set_req.roomid())?; 69 | let stealth = set_req.stealth(); 70 | 71 | match self.shared.gui_room_manager.write().set_search_flag(&room_id, stealth, self.client_info.user_id) { 72 | Ok(()) => Ok(ErrorType::NoError), 73 | Err(e) => Ok(e), 74 | } 75 | } 76 | 77 | pub fn req_get_room_search_flag_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 78 | let get_req = self.get_fb::(data)?; 79 | let room_id = GuiRoomManager::fb_vec_to_room_id(get_req.id())?; 80 | 81 | match self.shared.gui_room_manager.read().get_search_flag(&room_id) { 82 | Ok(resp) => { 83 | Client::add_data_packet(reply, &resp); 84 | Ok(ErrorType::NoError) 85 | } 86 | Err(e) => Ok(e), 87 | } 88 | } 89 | 90 | pub fn req_set_room_info_gui(&mut self, data: &mut StreamExtractor) -> Result { 91 | let set_req = self.get_fb::(data)?; 92 | let room_id = GuiRoomManager::fb_vec_to_room_id(set_req.id())?; 93 | let attrs = Client::validate_and_unwrap(set_req.attr())?; 94 | 95 | match self.shared.gui_room_manager.write().set_room_info_gui(&room_id, attrs, self.client_info.user_id) { 96 | Ok(()) => Ok(ErrorType::NoError), 97 | Err(e) => Ok(e), 98 | } 99 | } 100 | 101 | pub fn req_get_room_info_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 102 | let get_req = self.get_fb::(data)?; 103 | let room_id = GuiRoomManager::fb_vec_to_room_id(get_req.id())?; 104 | let attrs = Client::validate_and_unwrap(get_req.attr())?; 105 | 106 | match self.shared.gui_room_manager.read().get_room_info_gui(&room_id, attrs) { 107 | Ok(resp) => { 108 | Client::add_data_packet(reply, &resp); 109 | Ok(ErrorType::NoError) 110 | } 111 | Err(e) => Ok(e), 112 | } 113 | } 114 | 115 | pub async fn req_quickmatch_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 116 | let (com_id, qm_req) = self.get_com_and_fb::(data)?; 117 | 118 | let (resp, notif) = self.shared.gui_room_manager.write().quickmatch_gui(&com_id, &qm_req, &self.client_info); 119 | 120 | if let Some((client_ids, notification)) = notif { 121 | let mut n_msg: Vec = Vec::with_capacity(notification.len() + size_of::()); 122 | Client::add_data_packet(&mut n_msg, ¬ification); 123 | let notif_vec = Client::create_notification(NotificationType::QuickMatchCompleteGUI, &n_msg); 124 | self.send_notification(¬if_vec, &client_ids).await; 125 | self.self_notification(¬if_vec); 126 | } 127 | 128 | Client::add_data_packet(reply, &resp); 129 | Ok(ErrorType::NoError) 130 | } 131 | 132 | pub async fn req_searchjoin_gui(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 133 | let (com_id, sj_req) = self.get_com_and_fb::(data)?; 134 | 135 | let res = self.shared.gui_room_manager.write().search_join_gui(&com_id, &sj_req, &self.client_info); 136 | match res { 137 | Ok((resp, notif_users, notif_data)) => { 138 | let mut n_msg: Vec = Vec::with_capacity(notif_data.len() + size_of::()); 139 | Client::add_data_packet(&mut n_msg, ¬if_data); 140 | let notif = Client::create_notification(NotificationType::MemberJoinedRoomGUI, &n_msg); 141 | self.send_notification(¬if, ¬if_users).await; 142 | 143 | Client::add_data_packet(reply, &resp); 144 | Ok(ErrorType::NoError) 145 | } 146 | Err(e) => Ok(e), 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/server/client/cmd_score.rs: -------------------------------------------------------------------------------- 1 | // Score Commands 2 | 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::sync::atomic::{AtomicU64, Ordering}; 6 | 7 | use tokio::fs; 8 | 9 | use crate::server::client::*; 10 | use crate::server::database::db_score::{DbBoardInfo, DbScoreInfo}; 11 | use crate::server::database::DbError; 12 | use crate::server::Server; 13 | 14 | const SCORE_DATA_DIRECTORY: &str = "score_data"; 15 | const SCORE_FILE_EXTENSION: &str = "sdt"; 16 | static SCORE_DATA_ID_DISPENSER: AtomicU64 = AtomicU64::new(1); 17 | 18 | impl Server { 19 | pub fn setup_config_scoreboards(db: &Database) -> Result<(), String> { 20 | let file = File::open("scoreboards.cfg"); 21 | 22 | if let Err(e) = file { 23 | return Err(format!("Unexpected error opening scoreboards.cfg: {}", e)); 24 | } 25 | let mut file = file.unwrap(); 26 | 27 | let mut buf_file = String::new(); 28 | file.read_to_string(&mut buf_file).map_err(|e| format!("Failed to read scoreboards.cfg: {}", e))?; 29 | 30 | struct ScoreboardConfig { 31 | communication_id: String, 32 | tables: Vec, 33 | rank_limit: u32, 34 | update_mode: u32, 35 | sort_mode: u32, 36 | upload_num_limit: u32, 37 | upload_size_limit: u32, 38 | } 39 | 40 | let config_scoreboards: Vec = buf_file 41 | .lines() 42 | .filter_map(|l| { 43 | if l.trim().is_empty() || l.trim().chars().nth(0).unwrap() == '#' { 44 | return None; 45 | } 46 | 47 | let board_infos: Vec<&str> = l.trim().split('|').map(|x| x.trim()).collect(); 48 | if board_infos.len() != 7 { 49 | println!("scoreboards.cfg: line({}) is invalid", l); 50 | return None; 51 | } 52 | 53 | let communication_id = board_infos[0].to_owned(); 54 | let tables: Result, _> = board_infos[1].split(',').map(|x| x.trim()).map(|x| x.parse::()).collect(); 55 | 56 | if tables.is_err() || tables.as_ref().unwrap().is_empty() { 57 | println!("scoreboards.cfg: line({}) contains invalid table ids", l); 58 | return None; 59 | } 60 | 61 | let rank_limit = board_infos[2].parse::(); 62 | let update_mode = match board_infos[3] { 63 | "NORMAL_UPDATE" => Some(0u32), 64 | "FORCE_UPDATE" => Some(1u32), 65 | _ => None, 66 | }; 67 | let sort_mode = match board_infos[4] { 68 | "DESCENDING_ORDER" => Some(0u32), 69 | "ASCENDING_ORDER" => Some(1u32), 70 | _ => None, 71 | }; 72 | let upload_num_limit = board_infos[5].parse::(); 73 | let upload_size_limit = board_infos[6].parse::(); 74 | 75 | if rank_limit.is_err() || update_mode.is_none() || sort_mode.is_none() || upload_num_limit.is_err() || upload_size_limit.is_err() { 76 | println!("scoreboards.cfg: line({}) contains invalid data", l); 77 | return None; 78 | } 79 | 80 | Some(ScoreboardConfig { 81 | communication_id, 82 | tables: tables.unwrap(), 83 | rank_limit: rank_limit.unwrap(), 84 | update_mode: update_mode.unwrap(), 85 | sort_mode: sort_mode.unwrap(), 86 | upload_num_limit: upload_num_limit.unwrap(), 87 | upload_size_limit: upload_size_limit.unwrap(), 88 | }) 89 | }) 90 | .collect(); 91 | 92 | for config in &config_scoreboards { 93 | let db_infos = DbBoardInfo { 94 | rank_limit: config.rank_limit, 95 | update_mode: config.update_mode, 96 | sort_mode: config.sort_mode, 97 | upload_num_limit: config.upload_num_limit, 98 | upload_size_limit: config.upload_size_limit, 99 | }; 100 | 101 | for table in &config.tables { 102 | if db.create_or_set_score_board_details(&config.communication_id, *table, &db_infos).is_err() { 103 | return Err("scoreboards.cfg: error setting up table".to_string()); 104 | } 105 | } 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | pub fn initialize_score_data_handler() -> Result<(), String> { 112 | let max = Server::create_data_directory(SCORE_DATA_DIRECTORY, SCORE_FILE_EXTENSION)?; 113 | SCORE_DATA_ID_DISPENSER.store(max, Ordering::SeqCst); 114 | Ok(()) 115 | } 116 | 117 | pub fn clean_score_data(conn: r2d2::PooledConnection) -> Result<(), String> { 118 | let db = Database::new(conn); 119 | 120 | let dir_ids_list = Server::get_ids_from_directory(SCORE_DATA_DIRECTORY, SCORE_FILE_EXTENSION)?; 121 | let db_ids_list = db.score_get_all_data_ids().map_err(|_| String::from("Failure to get score data ids from database"))?; 122 | 123 | let unused_ids = dir_ids_list.difference(&db_ids_list); 124 | let mut num_deleted = 0; 125 | 126 | for unused_id in unused_ids { 127 | let filename = Client::score_id_to_path(*unused_id); 128 | std::fs::remove_file(&filename).map_err(|e| format!("Failed to delete score data file({}): {}", filename, e))?; 129 | num_deleted += 1; 130 | } 131 | 132 | if num_deleted != 0 { 133 | println!("Deleted {} score data files", num_deleted); 134 | } 135 | 136 | Ok(()) 137 | } 138 | } 139 | 140 | impl Client { 141 | fn score_id_to_path(id: u64) -> String { 142 | format!("{}/{:020}.{}", SCORE_DATA_DIRECTORY, id, SCORE_FILE_EXTENSION) 143 | } 144 | 145 | async fn create_score_data_file(data: &[u8]) -> u64 { 146 | let id = SCORE_DATA_ID_DISPENSER.fetch_add(1, Ordering::SeqCst); 147 | let path = Client::score_id_to_path(id); 148 | 149 | let file = fs::File::create(&path).await; 150 | if let Err(e) = file { 151 | error!("Failed to create score data {}: {}", &path, e); 152 | return 0; 153 | } 154 | let mut file = file.unwrap(); 155 | 156 | if let Err(e) = file.write_all(data).await { 157 | error!("Failed to write score data {}: {}", &path, e); 158 | return 0; 159 | } 160 | 161 | id 162 | } 163 | 164 | async fn get_score_data_file(id: u64) -> Result, ErrorType> { 165 | let path = Client::score_id_to_path(id); 166 | 167 | fs::read(&path).await.map_err(|e| { 168 | error!("Failed to open/read score data file {}: {}", &path, e); 169 | ErrorType::NotFound 170 | }) 171 | } 172 | 173 | async fn delete_score_data(id: u64) { 174 | let path = Client::score_id_to_path(id); 175 | if let Err(e) = std::fs::remove_file(&path) { 176 | error!("Failed to delete score data {}: {}", &path, e); 177 | // Todo: wait in case of usage by another user 178 | // or clean it on next server restart? 179 | } 180 | } 181 | 182 | pub async fn get_board_infos(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 183 | let com_id = self.get_com_id_with_redir(data); 184 | let board_id = data.get::(); 185 | 186 | if data.error() { 187 | warn!("Error while extracting data from GetBoardInfos command"); 188 | return Err(ErrorType::Malformed); 189 | } 190 | 191 | let db = Database::new(self.get_database_connection()?); 192 | let res = db.get_board_infos(&com_id, board_id, self.config.read().is_create_missing()); 193 | if let Err(e) = res { 194 | match e { 195 | DbError::Empty => { 196 | return Ok(ErrorType::NotFound); 197 | } 198 | _ => { 199 | return Err(ErrorType::DbFail); 200 | } 201 | } 202 | } 203 | let res = res.unwrap(); 204 | 205 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 206 | let board_info = BoardInfo::create( 207 | &mut builder, 208 | &BoardInfoArgs { 209 | rankLimit: res.rank_limit, 210 | updateMode: res.update_mode, 211 | sortMode: res.sort_mode, 212 | uploadNumLimit: res.upload_num_limit, 213 | uploadSizeLimit: res.upload_size_limit, 214 | }, 215 | ); 216 | builder.finish(board_info, None); 217 | let finished_data = builder.finished_data().to_vec(); 218 | Client::add_data_packet(reply, &finished_data); 219 | 220 | Ok(ErrorType::NoError) 221 | } 222 | 223 | pub async fn record_score(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 224 | let (com_id, score_req) = self.get_com_and_fb::(data)?; 225 | 226 | let score_infos = DbScoreInfo { 227 | user_id: self.client_info.user_id, 228 | character_id: score_req.pcId(), 229 | score: score_req.score(), 230 | comment: score_req.comment().map(|s| s.to_owned()), 231 | game_info: score_req.data().map(|v| v.iter().collect()), 232 | data_id: None, 233 | timestamp: Client::get_psn_timestamp(), 234 | }; 235 | 236 | let db = Database::new(self.get_database_connection()?); 237 | let res = db.record_score(&com_id, score_req.boardId(), &score_infos, self.config.read().is_create_missing()); 238 | 239 | match res { 240 | Ok(board_infos) => { 241 | let pos = self 242 | .shared 243 | .score_cache 244 | .insert_score(&board_infos, &com_id, score_req.boardId(), &score_infos, &self.client_info.npid, &self.client_info.online_name); 245 | reply.extend(&pos.to_le_bytes()); 246 | Ok(ErrorType::NoError) 247 | } 248 | Err(e) => Ok(match e { 249 | DbError::ScoreNotBest => ErrorType::ScoreNotBest, 250 | DbError::Internal => ErrorType::DbFail, 251 | _ => unreachable!(), 252 | }), 253 | } 254 | } 255 | 256 | pub async fn record_score_data(&mut self, data: &mut StreamExtractor) -> Result { 257 | let com_id = self.get_com_id_with_redir(data); 258 | let score_req = data.get_flatbuffer::(); 259 | let score_data = data.get_rawdata(); 260 | 261 | if data.error() || score_req.is_err() { 262 | warn!("Error while extracting data from RecordScoreData command"); 263 | return Err(ErrorType::Malformed); 264 | } 265 | let score_req = score_req.unwrap(); 266 | 267 | // Before going further make sure that the score exist 268 | if let Err(e) = self 269 | .shared 270 | .score_cache 271 | .contains_score_with_no_data(&com_id, self.client_info.user_id, score_req.pcId(), score_req.boardId(), score_req.score()) 272 | { 273 | return Ok(e); 274 | } 275 | 276 | let score_data_id = Client::create_score_data_file(&score_data).await; 277 | 278 | // Update cache 279 | if let Err(e) = self 280 | .shared 281 | .score_cache 282 | .set_game_data(&com_id, self.client_info.user_id, score_req.pcId(), score_req.boardId(), score_data_id) 283 | { 284 | Client::delete_score_data(score_data_id).await; 285 | return Ok(e); 286 | } 287 | 288 | // Update db 289 | let db = Database::new(self.get_database_connection()?); 290 | if db 291 | .set_score_data(&com_id, self.client_info.user_id, score_req.pcId(), score_req.boardId(), score_req.score(), score_data_id) 292 | .is_err() 293 | { 294 | error!("Unexpected error updating score game data in database!"); 295 | } 296 | 297 | Ok(ErrorType::NoError) 298 | } 299 | 300 | pub async fn get_score_data(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 301 | let (com_id, score_req) = self.get_com_and_fb::(data)?; 302 | let npid = Client::validate_and_unwrap(score_req.npId())?; 303 | 304 | let db = Database::new(self.get_database_connection()?); 305 | let user_id = db.get_user_id(npid); 306 | 307 | if user_id.is_err() { 308 | return Ok(ErrorType::NotFound); 309 | } 310 | let user_id = user_id.unwrap(); 311 | 312 | let data_id = self.shared.score_cache.get_game_data_id(&com_id, user_id, score_req.pcId(), score_req.boardId()); 313 | if let Err(e) = data_id { 314 | return Ok(e); 315 | } 316 | let data_id = data_id.unwrap(); 317 | 318 | let data = Client::get_score_data_file(data_id).await; 319 | if let Err(e) = data { 320 | return Ok(e); 321 | } 322 | let data = data.unwrap(); 323 | Client::add_data_packet(reply, &data); 324 | 325 | Ok(ErrorType::NoError) 326 | } 327 | 328 | pub async fn get_score_range(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 329 | let (com_id, score_req) = self.get_com_and_fb::(data)?; 330 | 331 | let getscore_result = self.shared.score_cache.get_score_range( 332 | &com_id, 333 | score_req.boardId(), 334 | score_req.startRank(), 335 | score_req.numRanks(), 336 | score_req.withComment(), 337 | score_req.withGameInfo(), 338 | ); 339 | let finished_data = getscore_result.serialize(); 340 | Client::add_data_packet(reply, &finished_data); 341 | 342 | Ok(ErrorType::NoError) 343 | } 344 | 345 | pub async fn get_score_friends(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 346 | let (com_id, score_req) = self.get_com_and_fb::(data)?; 347 | 348 | let mut id_vec = Vec::new(); 349 | 350 | if score_req.include_self() { 351 | id_vec.push((self.client_info.user_id, 0)); 352 | } 353 | 354 | let friends = { self.shared.client_infos.read().get(&self.client_info.user_id).unwrap().friend_info.read().friends.clone() }; 355 | 356 | friends.iter().for_each(|(user_id, _)| id_vec.push((*user_id, 0))); 357 | id_vec.truncate(score_req.max() as usize); 358 | 359 | let getscore_result = self 360 | .shared 361 | .score_cache 362 | .get_score_ids(&com_id, score_req.boardId(), &id_vec, score_req.withComment(), score_req.withGameInfo()); 363 | let finished_data = getscore_result.serialize(); 364 | 365 | reply.extend(&(finished_data.len() as u32).to_le_bytes()); 366 | reply.extend(finished_data); 367 | 368 | Ok(ErrorType::NoError) 369 | } 370 | 371 | pub async fn get_score_npid(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 372 | let (com_id, score_req) = self.get_com_and_fb::(data)?; 373 | 374 | let db = Database::new(self.get_database_connection()?); 375 | 376 | let mut id_vec: Vec<(i64, i32)> = Vec::new(); 377 | if let Some(npids) = score_req.npids() { 378 | for i in 0..npids.len() { 379 | let npid_and_pcid = npids.get(i); 380 | let user_id = db.get_user_id(npid_and_pcid.npid().unwrap_or("")).unwrap_or(0); 381 | id_vec.push((user_id, npid_and_pcid.pcId())); 382 | } 383 | } 384 | 385 | let getscore_result = self 386 | .shared 387 | .score_cache 388 | .get_score_ids(&com_id, score_req.boardId(), &id_vec, score_req.withComment(), score_req.withGameInfo()); 389 | let finished_data = getscore_result.serialize(); 390 | Client::add_data_packet(reply, &finished_data); 391 | 392 | Ok(ErrorType::NoError) 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/server/client/cmd_server.rs: -------------------------------------------------------------------------------- 1 | // Server/World Commands 2 | 3 | use crate::server::client::*; 4 | 5 | impl Client { 6 | pub fn req_get_server_list(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 7 | let com_id = self.get_com_id_with_redir(data); 8 | 9 | if data.error() { 10 | warn!("Error while extracting data from GetServerList command"); 11 | return Err(ErrorType::Malformed); 12 | } 13 | 14 | let servs = Database::new(self.get_database_connection()?).get_server_list(&com_id, self.config.read().is_create_missing()); 15 | if servs.is_err() { 16 | return Err(ErrorType::DbFail); 17 | } 18 | let servs = servs.unwrap(); 19 | 20 | let num_servs = servs.len() as u16; 21 | reply.extend(&num_servs.to_le_bytes()); 22 | for serv in servs { 23 | reply.extend(&serv.to_le_bytes()); 24 | } 25 | 26 | info!("Returning {} servers for comId {}", num_servs, com_id_to_string(&com_id)); 27 | 28 | Ok(ErrorType::NoError) 29 | } 30 | pub fn req_get_world_list(&mut self, data: &mut StreamExtractor, reply: &mut Vec) -> Result { 31 | let com_id = self.get_com_id_with_redir(data); 32 | let server_id = data.get::(); 33 | 34 | if data.error() { 35 | warn!("Error while extracting data from GetWorldList command"); 36 | return Err(ErrorType::Malformed); 37 | } 38 | 39 | let worlds = Database::new(self.get_database_connection()?).get_world_list(&com_id, server_id, self.config.read().is_create_missing()); 40 | if worlds.is_err() { 41 | return Err(ErrorType::DbFail); 42 | } 43 | let worlds = worlds.unwrap(); 44 | 45 | let num_worlds = worlds.len() as u32; 46 | reply.extend(&num_worlds.to_le_bytes()); 47 | for world in worlds { 48 | reply.extend(&world.to_le_bytes()); 49 | } 50 | 51 | info!("Returning {} worlds", num_worlds); 52 | 53 | Ok(ErrorType::NoError) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/server/client/cmd_session.rs: -------------------------------------------------------------------------------- 1 | // Session Commands 2 | 3 | use crate::server::client::*; 4 | use crate::server::room_manager::RoomBinAttr; 5 | 6 | const SCE_NP_MATCHING2_USER_BIN_ATTR_1_ID: u16 = 0x5F; 7 | 8 | pub struct ClientSharedSessionInfo { 9 | bin_attr: RoomBinAttr<128>, 10 | } 11 | 12 | impl ClientSharedSessionInfo { 13 | pub fn new() -> ClientSharedSessionInfo { 14 | ClientSharedSessionInfo { 15 | bin_attr: RoomBinAttr::<128>::with_id(SCE_NP_MATCHING2_USER_BIN_ATTR_1_ID), 16 | } 17 | } 18 | } 19 | 20 | impl Client { 21 | pub async fn req_set_userinfo(&mut self, data: &mut StreamExtractor) -> Result { 22 | let (_com_id, setuser_req) = self.get_com_and_fb::(data)?; 23 | 24 | if setuser_req.userBinAttr().is_none() || setuser_req.userBinAttr().as_ref().unwrap().is_empty() { 25 | return Ok(ErrorType::NoError); 26 | } 27 | let binattrs = setuser_req.userBinAttr().unwrap(); 28 | if binattrs.len() > 1 { 29 | error!("SetUserInfo request sent more than one binattr!"); 30 | } 31 | 32 | // Take the first valid one? 33 | let mut binattr = None; 34 | 'search_loop: for i in 0..binattrs.len() { 35 | if binattrs.get(i).id() == SCE_NP_MATCHING2_USER_BIN_ATTR_1_ID { 36 | binattr = Some(binattrs.get(i)); 37 | break 'search_loop; 38 | } 39 | } 40 | if binattr.is_none() { 41 | error!("SetUserInfo request didn't contain a valid binattr id!"); 42 | return Ok(ErrorType::NoError); 43 | } 44 | let binattr = binattr.unwrap(); 45 | 46 | let client_info = self.shared.client_infos.read(); 47 | let mut session_info = client_info.get(&self.client_info.user_id).unwrap().session_info.write(); 48 | session_info.bin_attr = RoomBinAttr::from_flatbuffer(&binattr); 49 | 50 | Ok(ErrorType::NoError) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/server/client/notifications.rs: -------------------------------------------------------------------------------- 1 | use crate::server::client::*; 2 | 3 | #[derive(Clone, Copy)] 4 | #[repr(u16)] 5 | pub enum NotificationType { 6 | UserJoinedRoom, 7 | UserLeftRoom, 8 | RoomDestroyed, 9 | UpdatedRoomDataInternal, 10 | UpdatedRoomMemberDataInternal, 11 | FriendQuery, // Other user sent a friend request 12 | FriendNew, // Add a friend to the friendlist(either accepted a friend request or friend accepted it) 13 | FriendLost, // Remove friend from the friendlist(user removed friend or friend removed friend) 14 | FriendStatus, // Set status of friend to Offline or Online 15 | RoomMessageReceived, 16 | MessageReceived, 17 | FriendPresenceChanged, 18 | SignalingHelper, 19 | MemberJoinedRoomGUI, 20 | MemberLeftRoomGUI, 21 | RoomDisappearedGUI, 22 | RoomOwnerChangedGUI, 23 | _UserKickedGUI, 24 | QuickMatchCompleteGUI, 25 | } 26 | 27 | impl Client { 28 | #[must_use] 29 | pub fn create_notification(n_type: NotificationType, data: &[u8]) -> Vec { 30 | let final_size = data.len() + HEADER_SIZE as usize; 31 | 32 | let mut final_vec = Vec::with_capacity(final_size); 33 | final_vec.push(PacketType::Notification as u8); 34 | final_vec.extend(&(n_type as u16).to_le_bytes()); 35 | final_vec.extend(&(final_size as u32).to_le_bytes()); 36 | final_vec.extend(&0u64.to_le_bytes()); // packet_id doesn't matter for notifications 37 | final_vec.extend(data); 38 | 39 | final_vec 40 | } 41 | 42 | pub fn create_friend_status_notification(npid: &str, timestamp: u64, online: bool) -> Vec { 43 | let mut n_msg: Vec = Vec::new(); 44 | n_msg.push(if online { 1 } else { 0 }); 45 | n_msg.extend(×tamp.to_le_bytes()); 46 | n_msg.extend(npid.as_bytes()); 47 | n_msg.push(0); 48 | Client::create_notification(NotificationType::FriendStatus, &n_msg) 49 | } 50 | 51 | pub fn create_new_friend_notification(npid: &str, online: bool) -> Vec { 52 | let mut n_msg: Vec = Vec::new(); 53 | n_msg.push(if online { 1 } else { 0 }); 54 | n_msg.extend(npid.as_bytes()); 55 | n_msg.push(0); 56 | Client::create_notification(NotificationType::FriendNew, &n_msg) 57 | } 58 | 59 | pub async fn send_single_notification(&self, notif: &[u8], user_id: i64) { 60 | let channel_copy; 61 | { 62 | let client_infos = self.shared.client_infos.read(); 63 | let client_info = client_infos.get(&user_id); 64 | if let Some(client_info) = client_info { 65 | channel_copy = client_info.channel.clone(); 66 | } else { 67 | return; 68 | } 69 | } 70 | let _ = channel_copy.send(notif.to_vec()).await; 71 | } 72 | 73 | pub async fn send_notification(&self, notif: &[u8], user_list: &HashSet) { 74 | for user_id in user_list { 75 | self.send_single_notification(notif, *user_id).await; 76 | } 77 | } 78 | 79 | pub fn self_notification(&mut self, notif: &[u8]) { 80 | self.post_reply_notifications.push(notif.to_vec()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/server/client/ticket.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU64, Ordering}; 2 | use std::time::SystemTime; 3 | 4 | use crate::TicketSignInfo; 5 | 6 | use openssl::sign::Signer; 7 | 8 | use tracing::error; 9 | 10 | enum TicketData { 11 | Empty(), 12 | U32(u32), 13 | U64(u64), 14 | Time(u64), 15 | Binary(Vec), 16 | BString(Vec), 17 | Blob(u8, Vec), 18 | } 19 | 20 | impl TicketData { 21 | fn id(&self) -> u16 { 22 | match self { 23 | TicketData::Empty() => 0, 24 | TicketData::U32(_) => 1, 25 | TicketData::U64(_) => 2, 26 | TicketData::BString(_) => 4, 27 | TicketData::Time(_) => 7, 28 | TicketData::Binary(_) => 8, 29 | TicketData::Blob(id, _) => 0x3000 | (*id as u16), 30 | } 31 | } 32 | 33 | fn len(&self) -> u16 { 34 | match self { 35 | TicketData::Empty() => 0, 36 | TicketData::U32(_) => 4, 37 | TicketData::U64(_) => 8, 38 | TicketData::BString(string_data) => string_data.len() as u16, 39 | TicketData::Time(_) => 8, 40 | TicketData::Binary(binary_data) => binary_data.len() as u16, 41 | TicketData::Blob(_, sdata) => sdata.iter().map(|x| x.len() + 4).sum(), 42 | } 43 | } 44 | 45 | pub fn write(&self, dest: &mut Vec) { 46 | dest.extend(&self.id().to_be_bytes()); 47 | dest.extend(&self.len().to_be_bytes()); 48 | match self { 49 | TicketData::Empty() => {} 50 | TicketData::U32(value) => dest.extend(&value.to_be_bytes()), 51 | TicketData::U64(value) => dest.extend(&value.to_be_bytes()), 52 | TicketData::BString(string_data) => dest.extend(string_data), 53 | TicketData::Time(time) => dest.extend(&time.to_be_bytes()), 54 | TicketData::Binary(binary_data) => dest.extend(binary_data), 55 | TicketData::Blob(_, sdata) => { 56 | for sub in sdata { 57 | sub.write(dest); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | pub struct Ticket { 65 | data: Vec, 66 | } 67 | 68 | static TICKET_ID_DISPENSER: AtomicU64 = AtomicU64::new(1); 69 | 70 | // TODO: get saved value from DB or 1 to initialze dispenser (to ensure continuity after restart) 71 | // impl Server { 72 | // pub fn initialize_ticket_dispenser() -> Result<(), String> { 73 | // Ok(()) 74 | // } 75 | // } 76 | 77 | impl Ticket { 78 | fn sign_ticket(user_blob: &TicketData, sign_info: &Option) -> TicketData { 79 | let signature = TicketData::Blob(2, vec![TicketData::Binary(vec![0, 0, 0, 0]), TicketData::Binary([0; 56].to_vec())]); 80 | 81 | if sign_info.is_none() { 82 | return signature; 83 | } 84 | 85 | let sign_info = sign_info.as_ref().unwrap(); 86 | 87 | let signer = Signer::new(sign_info.digest, &sign_info.key); 88 | if let Err(e) = signer { 89 | error!("Failed to create Signer for ticket data: {}", e); 90 | return signature; 91 | } 92 | let mut signer = signer.unwrap(); 93 | 94 | let signature_size = signer.len(); 95 | if let Err(e) = signature_size { 96 | error!("Failed to get signature size: {}", e); 97 | return signature; 98 | } 99 | let signature_size = signature_size.unwrap(); 100 | 101 | let mut vec_sign = vec![0; signature_size]; 102 | 103 | let mut user_rawdata = Vec::new(); 104 | user_blob.write(&mut user_rawdata); 105 | 106 | let sign_res = signer.sign_oneshot(&mut vec_sign, &user_rawdata); 107 | if let Err(e) = sign_res { 108 | error!("Failed to sign ticket data: {}", e); 109 | return signature; 110 | } 111 | let sign_res = sign_res.unwrap(); 112 | vec_sign.resize(sign_res, 0); 113 | 114 | TicketData::Blob(2, vec![TicketData::Binary(vec![b'R', b'P', b'C', b'N']), TicketData::Binary(vec_sign)]) 115 | } 116 | 117 | pub fn new(user_id: u64, npid: &str, service_id: &str, cookie: Vec, sign_info: &Option) -> Ticket { 118 | let ticket_id = TICKET_ID_DISPENSER.fetch_add(1, Ordering::SeqCst); 119 | 120 | let serial_str = format!("{}", ticket_id); 121 | let mut serial_vec = serial_str.as_bytes().to_vec(); 122 | serial_vec.resize(0x14, 0); 123 | 124 | // used to be 0x33333333 but some games actually check this value(Tony Hawk: SHRED) 125 | // 0x100 is supposed to be the release network, other known values are 8 and 1(test and dev networks?) 126 | let issuer_id: u32 = 0x100; 127 | let issued_date: u64 = (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64) - (60 * 1000); 128 | let expire_date = issued_date + (60 * 1000 * 15); 129 | 130 | let mut online_id = npid.as_bytes().to_vec(); 131 | online_id.resize(0x20, 0); 132 | 133 | let mut service_id: Vec = service_id.as_bytes().to_vec(); 134 | service_id.resize(0x18, 0); 135 | 136 | let mut user_data = vec![ 137 | TicketData::Binary(serial_vec), 138 | TicketData::U32(issuer_id), 139 | TicketData::Time(issued_date), 140 | TicketData::Time(expire_date), 141 | TicketData::U64(user_id), 142 | TicketData::BString(online_id), 143 | TicketData::Binary(vec![b'b', b'r', 0, 0]), // region (yes you're going to brazil) 144 | TicketData::BString(vec![b'u', b'n', 0, 0]), // domain 145 | TicketData::Binary(service_id), 146 | TicketData::U32(0), // status 147 | ]; 148 | 149 | if !cookie.is_empty() { 150 | user_data.push(TicketData::Binary(cookie)); 151 | } 152 | 153 | user_data.push(TicketData::Empty()); 154 | user_data.push(TicketData::Empty()); 155 | 156 | let user_blob = TicketData::Blob(0, user_data); 157 | let signature = Ticket::sign_ticket(&user_blob, sign_info); 158 | 159 | Ticket { data: vec![user_blob, signature] } 160 | } 161 | 162 | pub fn generate_blob(&self) -> Vec { 163 | let mut ticket_blob: Vec = Vec::new(); 164 | 165 | // Version 166 | ticket_blob.extend(&(0x21010000u32).to_be_bytes()); 167 | 168 | let size: u32 = self.data.iter().map(|x| (x.len() + 4) as u32).sum::(); 169 | ticket_blob.extend(&size.to_be_bytes()); 170 | 171 | for data in &self.data { 172 | data.write(&mut ticket_blob); 173 | } 174 | 175 | ticket_blob 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/server/database/db_score.rs: -------------------------------------------------------------------------------- 1 | use crate::server::database::*; 2 | 3 | #[derive(Clone)] 4 | pub struct DbBoardInfo { 5 | pub rank_limit: u32, 6 | pub update_mode: u32, 7 | pub sort_mode: u32, 8 | pub upload_num_limit: u32, 9 | pub upload_size_limit: u32, 10 | } 11 | 12 | #[derive(Clone, Default)] 13 | pub struct DbScoreInfo { 14 | pub user_id: i64, 15 | pub character_id: i32, 16 | pub score: i64, 17 | pub comment: Option, 18 | pub game_info: Option>, 19 | pub data_id: Option, 20 | pub timestamp: u64, 21 | } 22 | 23 | impl Default for DbBoardInfo { 24 | fn default() -> DbBoardInfo { 25 | DbBoardInfo { 26 | rank_limit: 100, 27 | update_mode: 0, // SCE_NP_SCORE_NORMAL_UPDATE 28 | sort_mode: 0, // SCE_NP_SCORE_DESCENDING_ORDER 29 | upload_num_limit: 10, 30 | upload_size_limit: 6_000_000, // 6MB 31 | } 32 | } 33 | } 34 | 35 | impl Database { 36 | pub fn score_get_all_data_ids(&self) -> Result, DbError> { 37 | let mut stmt = self.conn.prepare("SELECT data_id FROM score WHERE data_id IS NOT NULL").map_err(|_| DbError::Internal)?; 38 | let data_ids: HashSet = stmt 39 | .query_map([], |r| Ok(r.get_unwrap(0))) 40 | .map_err(|_| DbError::Internal)? 41 | .collect::, _>>() 42 | .map_err(|_| DbError::Internal)?; 43 | Ok(data_ids) 44 | } 45 | 46 | fn create_score_board(&self, com_id: &ComId, board_id: u32) -> Result<(), DbError> { 47 | let com_id_str = com_id_to_string(com_id); 48 | let default_boardinfo: DbBoardInfo = Default::default(); 49 | 50 | if let Err(e) = self.conn.execute( 51 | "INSERT INTO score_table ( communication_id, board_id, rank_limit, update_mode, sort_mode, upload_num_limit, upload_size_limit ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 )", 52 | rusqlite::params![ 53 | com_id_str, 54 | board_id, 55 | default_boardinfo.rank_limit, 56 | default_boardinfo.update_mode, 57 | default_boardinfo.sort_mode, 58 | default_boardinfo.upload_num_limit, 59 | default_boardinfo.upload_size_limit, 60 | ], 61 | ) { 62 | match e { 63 | rusqlite::Error::StatementChangedRows(_) => {} 64 | _ => { 65 | error!("Unexpected error inserting scoreboard({}:{}) in score_table: {}", com_id_str, board_id, e); 66 | return Err(DbError::Internal); 67 | } 68 | } 69 | } 70 | 71 | Ok(()) 72 | } 73 | 74 | pub fn create_or_set_score_board_details(&self, com_id: &str, board_id: u32, board_infos: &DbBoardInfo) -> Result<(), DbError> { 75 | let res = self.conn.execute( 76 | "INSERT INTO score_table ( communication_id, board_id, rank_limit, update_mode, sort_mode, upload_num_limit, upload_size_limit ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 ) ON CONFLICT( communication_id, board_id ) DO UPDATE SET rank_limit = excluded.rank_limit, update_mode = excluded.update_mode, sort_mode = excluded.sort_mode, upload_num_limit = excluded.upload_num_limit, upload_size_limit = excluded.upload_size_limit", 77 | rusqlite::params![ 78 | com_id, 79 | board_id, 80 | board_infos.rank_limit, 81 | board_infos.update_mode, 82 | board_infos.sort_mode, 83 | board_infos.upload_num_limit, 84 | board_infos.upload_size_limit, 85 | ], 86 | ); 87 | 88 | if let Err(e) = res { 89 | println!("Unexpected error setting score table details: {}", e); 90 | return Err(DbError::Internal); 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | pub fn get_score_tables(&self) -> Result, DbError> { 97 | let mut stmt = self 98 | .conn 99 | .prepare("SELECT communication_id, board_id, rank_limit, update_mode, sort_mode, upload_num_limit, upload_size_limit FROM score_table") 100 | .map_err(|e| { 101 | println!("Failed to prepare statement: {}", e); 102 | DbError::Internal 103 | })?; 104 | let rows = stmt.query_map([], |r| { 105 | let com_id: String = r.get_unwrap(0); 106 | let board_id: u32 = r.get_unwrap(1); 107 | Ok(( 108 | (com_id, board_id), 109 | DbBoardInfo { 110 | rank_limit: r.get_unwrap(2), 111 | update_mode: r.get_unwrap(3), 112 | sort_mode: r.get_unwrap(4), 113 | upload_num_limit: r.get_unwrap(5), 114 | upload_size_limit: r.get_unwrap(6), 115 | }, 116 | )) 117 | }); 118 | 119 | let mut tables_map = HashMap::new(); 120 | 121 | match rows { 122 | Err(rusqlite::Error::QueryReturnedNoRows) => {} 123 | Err(e) => { 124 | println!("Err: Failed to query score tables: {}", e); 125 | return Err(DbError::Internal); 126 | } 127 | Ok(rows) => { 128 | for table in rows { 129 | let table = table.unwrap(); 130 | tables_map.insert(table.0, table.1); 131 | } 132 | } 133 | } 134 | 135 | Ok(tables_map) 136 | } 137 | 138 | pub fn get_scores_from_table(&self, com_id: &str, board_id: u32, table_info: &DbBoardInfo) -> Result, DbError> { 139 | let statement_str = if table_info.sort_mode == 0 { 140 | "SELECT user_id, character_id, score, comment, game_info, data_id, timestamp FROM score WHERE communication_id = ?1 AND board_id = ?2 ORDER BY score DESC, timestamp ASC, user_id ASC LIMIT ?3" 141 | } else { 142 | "SELECT user_id, character_id, score, comment, game_info, data_id, timestamp FROM score WHERE communication_id = ?1 AND board_id = ?2 ORDER BY score ASC, timestamp ASC, user_id ASC LIMIT ?3" 143 | }; 144 | 145 | let mut stmt = self.conn.prepare(statement_str).map_err(|_| DbError::Internal)?; 146 | let rows = stmt 147 | .query_map(rusqlite::params![com_id, board_id, table_info.rank_limit], |r| { 148 | Ok(DbScoreInfo { 149 | user_id: r.get_unwrap(0), 150 | character_id: r.get_unwrap(1), 151 | score: r.get_unwrap(2), 152 | comment: r.get_unwrap(3), 153 | game_info: r.get_unwrap(4), 154 | data_id: r.get_unwrap(5), 155 | timestamp: r.get_unwrap(6), 156 | }) 157 | }) 158 | .map_err(|_| DbError::Internal)?; 159 | 160 | let mut vec_scores = Vec::new(); 161 | for row in rows { 162 | vec_scores.push(row.unwrap()); 163 | } 164 | 165 | Ok(vec_scores) 166 | } 167 | 168 | pub fn get_board_infos(&self, com_id: &ComId, board_id: u32, create_missing: bool) -> Result { 169 | let com_id_str = com_id_to_string(com_id); 170 | 171 | let res = self.conn.query_row( 172 | "SELECT rank_limit, update_mode, sort_mode, upload_num_limit, upload_size_limit FROM score_table WHERE communication_id = ?1 AND board_id = ?2", 173 | rusqlite::params![com_id_str, board_id], 174 | |r| { 175 | Ok(DbBoardInfo { 176 | rank_limit: r.get_unwrap(0), 177 | update_mode: r.get_unwrap(1), 178 | sort_mode: r.get_unwrap(2), 179 | upload_num_limit: r.get_unwrap(3), 180 | upload_size_limit: r.get_unwrap(4), 181 | }) 182 | }, 183 | ); 184 | 185 | if let Err(e) = res { 186 | if e == rusqlite::Error::QueryReturnedNoRows { 187 | if create_missing { 188 | self.create_score_board(com_id, board_id)?; 189 | return self.get_board_infos(com_id, board_id, false); 190 | } 191 | warn!("Attempted to query an unexisting score board({}:{})", &com_id_str, board_id); 192 | return Err(DbError::Empty); 193 | } 194 | 195 | error!("Unexpected error querying score board: {}", e); 196 | return Err(DbError::Internal); 197 | } 198 | 199 | Ok(res.unwrap()) 200 | } 201 | 202 | pub fn record_score(&self, com_id: &ComId, board_id: u32, score_infos: &DbScoreInfo, create_missing: bool) -> Result { 203 | let board_infos = self.get_board_infos(com_id, board_id, create_missing)?; 204 | let com_id_str = com_id_to_string(com_id); 205 | 206 | let query_str = if board_infos.update_mode == 0 { 207 | if board_infos.sort_mode == 0 { 208 | "INSERT INTO score ( communication_id, board_id, user_id, character_id, score, comment, game_info, data_id, timestamp ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8 ) ON CONFLICT ( communication_id, board_id, user_id, character_id ) DO UPDATE SET score = excluded.score, comment = excluded.comment, game_info = excluded.game_info, data_id = NULL, timestamp = excluded.timestamp WHERE excluded.score >= score" 209 | } else { 210 | "INSERT INTO score ( communication_id, board_id, user_id, character_id, score, comment, game_info, data_id, timestamp ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8 ) ON CONFLICT ( communication_id, board_id, user_id, character_id ) DO UPDATE SET score = excluded.score, comment = excluded.comment, game_info = excluded.game_info, data_id = NULL, timestamp = excluded.timestamp WHERE excluded.score <= score" 211 | } 212 | } else { 213 | "INSERT INTO score ( communication_id, board_id, user_id, character_id, score, comment, game_info, data_id, timestamp ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8 ) ON CONFLICT ( communication_id, board_id, user_id, character_id ) DO UPDATE SET score = excluded.score, comment = excluded.comment, game_info = excluded.game_info, data_id = NULL, timestamp = excluded.timestamp" 214 | }; 215 | 216 | let res = self.conn.execute( 217 | query_str, 218 | rusqlite::params![ 219 | com_id_str, 220 | board_id, 221 | score_infos.user_id, 222 | score_infos.character_id, 223 | score_infos.score, 224 | score_infos.comment, 225 | score_infos.game_info, 226 | score_infos.timestamp 227 | ], 228 | ); 229 | 230 | match res { 231 | Ok(n) => { 232 | if n == 1 { 233 | Ok(board_infos) 234 | } else { 235 | Err(DbError::ScoreNotBest) 236 | } 237 | } 238 | Err(e) => { 239 | error!("Unexpected error inserting score: {}", e); 240 | Err(DbError::Internal) 241 | } 242 | } 243 | } 244 | 245 | pub fn set_score_data(&self, com_id: &ComId, user_id: i64, character_id: i32, board_id: u32, score: i64, data_id: u64) -> Result<(), DbError> { 246 | let com_id_str = com_id_to_string(com_id); 247 | let res = self.conn.execute( 248 | "UPDATE score SET data_id = ?1 WHERE communication_id = ?2 AND board_id = ?3 AND user_id = ?4 AND character_id = ?5 AND score = ?6", 249 | rusqlite::params![data_id, com_id_str, board_id, user_id, character_id, score], 250 | ); 251 | 252 | match res { 253 | Ok(n) => { 254 | if n == 1 { 255 | Ok(()) 256 | } else { 257 | Err(DbError::Invalid) 258 | } 259 | } 260 | Err(e) => { 261 | error!("Unexpected error setting game data: {}", e); 262 | Err(DbError::Internal) 263 | } 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/server/database/db_tus.rs: -------------------------------------------------------------------------------- 1 | use crate::server::database::*; 2 | 3 | use crate::server::client::cmd_tus::{TusOpeType, TusSorting, TusStatusSorting}; 4 | 5 | pub struct DbTusVarInfo { 6 | pub timestamp: u64, 7 | pub author_id: i64, 8 | pub variable: i64, 9 | } 10 | 11 | pub struct DbTusDataStatus { 12 | pub timestamp: u64, 13 | pub author_id: i64, 14 | pub data_id: u64, 15 | } 16 | 17 | impl Database { 18 | pub fn tus_get_all_data_ids(&self) -> Result, DbError> { 19 | let mut stmt = self.conn.prepare("SELECT data_id FROM tus_data").map_err(|_| DbError::Internal)?; 20 | let rows = stmt.query_map([], |r| Ok(r.get_unwrap(0))).map_err(|_| DbError::Internal)?; 21 | 22 | let mut hs_data_ids = HashSet::new(); 23 | for row in rows { 24 | let to_insert = row.unwrap(); 25 | if !hs_data_ids.insert(to_insert) { 26 | println!("Duplicate tus data_id found: {}!", to_insert); 27 | return Err(DbError::Internal); 28 | } 29 | } 30 | 31 | let mut stmt = self.conn.prepare("SELECT data_id FROM tus_data_vuser").map_err(|_| DbError::Internal)?; 32 | let rows = stmt.query_map([], |r| Ok(r.get_unwrap(0))).map_err(|_| DbError::Internal)?; 33 | 34 | for row in rows { 35 | let to_insert = row.unwrap(); 36 | if !hs_data_ids.insert(to_insert) { 37 | println!("Duplicate tus data_id found: {}!", to_insert); 38 | return Err(DbError::Internal); 39 | } 40 | } 41 | 42 | Ok(hs_data_ids) 43 | } 44 | 45 | pub fn tus_set_vuser_variable(&self, com_id: &ComId, vuser: &str, slot: i32, value: i64, author_id: i64, timestamp: u64) -> Result<(), DbError> { 46 | let res = self.conn.execute( 47 | "INSERT INTO tus_var_vuser( vuser, communication_id, slot_id, var_value, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) ON CONFLICT( vuser, communication_id, slot_id ) DO UPDATE SET var_value = excluded.var_value, timestamp = excluded.timestamp, author_id = excluded.author_id", 48 | rusqlite::params![vuser, com_id, slot, value, timestamp, author_id], 49 | ); 50 | 51 | match res { 52 | Ok(n) => { 53 | if n == 1 { 54 | Ok(()) 55 | } else { 56 | Err(DbError::Invalid) 57 | } 58 | } 59 | Err(e) => { 60 | error!("Unexpected error setting vuser variable: {}", e); 61 | Err(DbError::Internal) 62 | } 63 | } 64 | } 65 | 66 | pub fn tus_set_user_variable(&self, com_id: &ComId, user: i64, slot: i32, value: i64, author_id: i64, timestamp: u64) -> Result<(), DbError> { 67 | let res = self.conn.execute( 68 | "INSERT INTO tus_var( owner_id, communication_id, slot_id, var_value, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) ON CONFLICT( owner_id, communication_id, slot_id ) DO UPDATE SET var_value = excluded.var_value, timestamp = excluded.timestamp, author_id = excluded.author_id", 69 | rusqlite::params![user, com_id, slot, value, timestamp, author_id], 70 | ); 71 | 72 | match res { 73 | Ok(n) => { 74 | if n == 1 { 75 | Ok(()) 76 | } else { 77 | Err(DbError::Invalid) 78 | } 79 | } 80 | Err(e) => { 81 | error!("Unexpected error setting vuser variable: {}", e); 82 | Err(DbError::Internal) 83 | } 84 | } 85 | } 86 | 87 | pub fn tus_get_vuser_variable(&self, com_id: &ComId, vuser: &str, slot: i32) -> Result { 88 | let res = self.conn.query_row( 89 | "SELECT timestamp, author_id, var_value FROM tus_var_vuser WHERE communication_id = ?1 AND vuser = ?2 AND slot_id = ?3", 90 | rusqlite::params![com_id, vuser, slot], 91 | |r| { 92 | Ok(DbTusVarInfo { 93 | timestamp: r.get_unwrap(0), 94 | author_id: r.get_unwrap(1), 95 | variable: r.get_unwrap(2), 96 | }) 97 | }, 98 | ); 99 | 100 | match res { 101 | Ok(tus_var_info) => Ok(tus_var_info), 102 | Err(rusqlite::Error::QueryReturnedNoRows) => Err(DbError::Empty), 103 | Err(e) => { 104 | error!("Unexpected error querying for tus vuser var: {}", e); 105 | Err(DbError::Internal) 106 | } 107 | } 108 | } 109 | 110 | pub fn tus_get_user_variable(&self, com_id: &ComId, user: i64, slot: i32) -> Result { 111 | let res = self.conn.query_row( 112 | "SELECT timestamp, author_id, var_value FROM tus_var WHERE communication_id = ?1 AND owner_id = ?2 AND slot_id = ?3", 113 | rusqlite::params![com_id, user, slot], 114 | |r| { 115 | Ok(DbTusVarInfo { 116 | timestamp: r.get_unwrap(0), 117 | author_id: r.get_unwrap(1), 118 | variable: r.get_unwrap(2), 119 | }) 120 | }, 121 | ); 122 | 123 | match res { 124 | Ok(tus_var_info) => Ok(tus_var_info), 125 | Err(rusqlite::Error::QueryReturnedNoRows) => Err(DbError::Empty), 126 | Err(e) => { 127 | error!("Unexpected error querying for tus var: {}", e); 128 | Err(DbError::Internal) 129 | } 130 | } 131 | } 132 | 133 | pub fn tus_get_user_variable_from_user_list(&self, com_id: &ComId, user_list: &[i64], slot: i32, sorting: cmd_tus::TusSorting, max: u32) -> Result, DbError> { 134 | let stmt_sorting = match sorting { 135 | TusSorting::SCE_NP_TUS_VARIABLE_SORTTYPE_DESCENDING_DATE => "timestamp DESC", 136 | TusSorting::SCE_NP_TUS_VARIABLE_SORTTYPE_ASCENDING_DATE => "timestamp ASC", 137 | TusSorting::SCE_NP_TUS_VARIABLE_SORTTYPE_DESCENDING_VALUE => "var_value DESC", 138 | TusSorting::SCE_NP_TUS_VARIABLE_SORTTYPE_ASCENDING_VALUE => "var_value ASC", 139 | }; 140 | 141 | let stmt_string = format!( 142 | "SELECT owner_id, timestamp, author_id, var_value FROM tus_var WHERE communication_id = ? AND slot_id = ? AND owner_id IN ({}) ORDER BY {} LIMIT {}", 143 | generate_string_from_user_list(user_list), 144 | stmt_sorting, 145 | max 146 | ); 147 | 148 | let mut stmt = self.conn.prepare(&stmt_string).map_err(|_| DbError::Internal)?; 149 | 150 | let rows = stmt 151 | .query_map(rusqlite::params! { com_id, slot }, |r| { 152 | Ok(( 153 | r.get_unwrap(0), 154 | DbTusVarInfo { 155 | timestamp: r.get_unwrap(1), 156 | author_id: r.get_unwrap(2), 157 | variable: r.get_unwrap(3), 158 | }, 159 | )) 160 | }) 161 | .map_err(|_| DbError::Internal)?; 162 | 163 | let mut vec_var_infos = Vec::new(); 164 | for row in rows { 165 | vec_var_infos.push(row.unwrap()); 166 | } 167 | 168 | Ok(vec_var_infos) 169 | } 170 | 171 | fn prepare_cond_string(compare_timestamp: Option, compare_author: Option) -> String { 172 | let mut cond_string = String::new(); 173 | 174 | if let Some(compare_timestamp) = compare_timestamp { 175 | let _ = write!(cond_string, "WHERE timestamp <= {}", compare_timestamp); 176 | } 177 | 178 | if let Some(compare_author) = compare_author { 179 | let _ = write!(cond_string, "{} author_id == {}", if cond_string.is_empty() { "WHERE" } else { " AND" }, compare_author); 180 | } 181 | 182 | cond_string 183 | } 184 | 185 | pub fn tus_add_and_get_user_variable( 186 | &self, 187 | com_id: &ComId, 188 | user: i64, 189 | slot: i32, 190 | value: i64, 191 | author_id: i64, 192 | timestamp: u64, 193 | compare_timestamp: Option, 194 | compare_author: Option, 195 | ) -> Result { 196 | let cond_string = Database::prepare_cond_string(compare_timestamp, compare_author); 197 | let full_query = format!("INSERT INTO tus_var( owner_id, communication_id, slot_id, var_value, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) ON CONFLICT( owner_id, communication_id, slot_id ) DO UPDATE SET var_value = var_value + excluded.var_value, timestamp = excluded.timestamp, author_id = excluded.author_id {} RETURNING timestamp, author_id, var_value", cond_string); 198 | 199 | match self.conn.query_row(&full_query, rusqlite::params![user, com_id, slot, value, timestamp, author_id], |r| { 200 | Ok(DbTusVarInfo { 201 | timestamp: r.get_unwrap(0), 202 | author_id: r.get_unwrap(1), 203 | variable: r.get_unwrap(2), 204 | }) 205 | }) { 206 | Ok(res) => Ok(res), 207 | Err(rusqlite::Error::QueryReturnedNoRows) => self 208 | .conn 209 | .query_row( 210 | "SELECT timestamp, author_id, var_value FROM tus_var WHERE owner_id = ?1 AND communication_id = ?2 AND slot_id = ?3", 211 | rusqlite::params![user, com_id, slot], 212 | |r| { 213 | Ok(DbTusVarInfo { 214 | timestamp: r.get_unwrap(0), 215 | author_id: r.get_unwrap(1), 216 | variable: r.get_unwrap(2), 217 | }) 218 | }, 219 | ) 220 | .map_err(|e| { 221 | error!("Unexpected error in tus_add_and_get_user_variable SELECT: {}", e); 222 | DbError::Internal 223 | }), 224 | Err(e) => { 225 | error!("Unexpected error in tus_add_and_get_user_variable: {}", e); 226 | Err(DbError::Internal) 227 | } 228 | } 229 | } 230 | 231 | pub fn tus_add_and_get_vuser_variable( 232 | &self, 233 | com_id: &ComId, 234 | vuser: &str, 235 | slot: i32, 236 | value: i64, 237 | author_id: i64, 238 | timestamp: u64, 239 | compare_timestamp: Option, 240 | compare_author: Option, 241 | ) -> Result { 242 | let cond_string = Database::prepare_cond_string(compare_timestamp, compare_author); 243 | let full_query = format!("INSERT INTO tus_var_vuser( vuser, communication_id, slot_id, var_value, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) ON CONFLICT( vuser, communication_id, slot_id ) DO UPDATE SET var_value = var_value + excluded.var_value, timestamp = excluded.timestamp, author_id = excluded.author_id {} RETURNING timestamp, author_id, var_value", cond_string); 244 | 245 | match self.conn.query_row(&full_query, rusqlite::params![vuser, com_id, slot, value, timestamp, author_id], |r| { 246 | Ok(DbTusVarInfo { 247 | timestamp: r.get_unwrap(0), 248 | author_id: r.get_unwrap(1), 249 | variable: r.get_unwrap(2), 250 | }) 251 | }) { 252 | Ok(res) => Ok(res), 253 | Err(rusqlite::Error::QueryReturnedNoRows) => self 254 | .conn 255 | .query_row( 256 | "SELECT timestamp, author_id, var_value FROM tus_var_vuser WHERE vuser = ?1 AND communication_id = ?2 AND slot_id = ?3", 257 | rusqlite::params![vuser, com_id, slot], 258 | |r| { 259 | Ok(DbTusVarInfo { 260 | timestamp: r.get_unwrap(0), 261 | author_id: r.get_unwrap(1), 262 | variable: r.get_unwrap(2), 263 | }) 264 | }, 265 | ) 266 | .map_err(|e| { 267 | error!("Unexpected error in tus_add_and_get_vuser_variable SELECT: {}", e); 268 | DbError::Internal 269 | }), 270 | Err(e) => { 271 | error!("Unexpected error in tus_add_and_get_vuser_variable: {}", e); 272 | Err(DbError::Internal) 273 | } 274 | } 275 | } 276 | 277 | fn prepare_try_and_set_cond_string(compare_op: TusOpeType, compare_timestamp: Option, compare_author: Option) -> String { 278 | let op = match compare_op { 279 | TusOpeType::SCE_NP_TUS_OPETYPE_EQUAL => "==", 280 | TusOpeType::SCE_NP_TUS_OPETYPE_NOT_EQUAL => "!=", 281 | TusOpeType::SCE_NP_TUS_OPETYPE_GREATER_THAN => ">", 282 | TusOpeType::SCE_NP_TUS_OPETYPE_GREATER_OR_EQUAL => ">=", 283 | TusOpeType::SCE_NP_TUS_OPETYPE_LESS_THAN => "<", 284 | TusOpeType::SCE_NP_TUS_OPETYPE_LESS_OR_EQUAL => "<=", 285 | }; 286 | 287 | let mut conditional = format!("var_value {} ?7", op); 288 | 289 | if let Some(compare_timestamp) = compare_timestamp { 290 | write!(conditional, " AND timestamp <= {}", compare_timestamp).unwrap(); 291 | } 292 | 293 | if let Some(compare_author) = compare_author { 294 | write!(conditional, " AND author_id == {}", compare_author).unwrap(); 295 | } 296 | 297 | conditional 298 | } 299 | 300 | pub fn tus_try_and_set_user_variable( 301 | &self, 302 | com_id: &ComId, 303 | user: i64, 304 | slot: i32, 305 | value_to_set: i64, 306 | author_id: i64, 307 | timestamp: u64, 308 | compare_value: i64, 309 | compare_op: TusOpeType, 310 | compare_timestamp: Option, 311 | compare_author: Option, 312 | ) -> Result { 313 | let conditional = Database::prepare_try_and_set_cond_string(compare_op, compare_timestamp, compare_author); 314 | 315 | let full_query = format!( 316 | "INSERT INTO tus_var( owner_id, communication_id, slot_id, var_value, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) \ 317 | ON CONFLICT ( owner_id, communication_id, slot_id ) DO \ 318 | UPDATE SET var_value = excluded.var_value, timestamp = excluded.timestamp, author_id = excluded.author_id \ 319 | WHERE {} RETURNING timestamp, author_id, var_value", 320 | conditional 321 | ); 322 | 323 | match self 324 | .conn 325 | .query_row(&full_query, rusqlite::params![user, com_id, slot, value_to_set, timestamp, author_id, compare_value], |r| { 326 | Ok(DbTusVarInfo { 327 | timestamp: r.get_unwrap(0), 328 | author_id: r.get_unwrap(1), 329 | variable: r.get_unwrap(2), 330 | }) 331 | }) { 332 | Ok(res) => Ok(res), 333 | Err(rusqlite::Error::QueryReturnedNoRows) => self 334 | .conn 335 | .query_row( 336 | "SELECT timestamp, author_id, var_value FROM tus_var WHERE owner_id = ?1 AND communication_id = ?2 AND slot_id = ?3", 337 | rusqlite::params![user, com_id, slot], 338 | |r| { 339 | Ok(DbTusVarInfo { 340 | timestamp: r.get_unwrap(0), 341 | author_id: r.get_unwrap(1), 342 | variable: r.get_unwrap(2), 343 | }) 344 | }, 345 | ) 346 | .map_err(|e| { 347 | error!("Unexpected error in tus_try_and_set_user_variable SELECT: {}", e); 348 | DbError::Internal 349 | }), 350 | Err(e) => { 351 | error!("Unexpected error in tus_try_and_set_user_variable: {}", e); 352 | Err(DbError::Internal) 353 | } 354 | } 355 | } 356 | 357 | pub fn tus_try_and_set_vuser_variable( 358 | &self, 359 | com_id: &ComId, 360 | vuser: &str, 361 | slot: i32, 362 | value_to_set: i64, 363 | author_id: i64, 364 | timestamp: u64, 365 | compare_value: i64, 366 | compare_op: TusOpeType, 367 | compare_timestamp: Option, 368 | compare_author: Option, 369 | ) -> Result { 370 | let conditional = Database::prepare_try_and_set_cond_string(compare_op, compare_timestamp, compare_author); 371 | 372 | let full_query = format!( 373 | "INSERT INTO tus_var_vuser( vuser, communication_id, slot_id, var_value, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) \ 374 | ON CONFLICT ( vuser, communication_id, slot_id ) DO \ 375 | UPDATE SET var_value = excluded.var_value, timestamp = excluded.timestamp, author_id = excluded.author_id \ 376 | WHERE {} RETURNING timestamp, author_id, var_value", 377 | conditional 378 | ); 379 | 380 | match self 381 | .conn 382 | .query_row(&full_query, rusqlite::params![vuser, com_id, slot, value_to_set, timestamp, author_id, compare_value], |r| { 383 | Ok(DbTusVarInfo { 384 | timestamp: r.get_unwrap(0), 385 | author_id: r.get_unwrap(1), 386 | variable: r.get_unwrap(2), 387 | }) 388 | }) { 389 | Ok(res) => Ok(res), 390 | Err(rusqlite::Error::QueryReturnedNoRows) => self 391 | .conn 392 | .query_row( 393 | "SELECT timestamp, author_id, var_value FROM tus_var_vuser WHERE vuser = ?1 AND communication_id = ?2 AND slot_id = ?3", 394 | rusqlite::params![vuser, com_id, slot], 395 | |r| { 396 | Ok(DbTusVarInfo { 397 | timestamp: r.get_unwrap(0), 398 | author_id: r.get_unwrap(1), 399 | variable: r.get_unwrap(2), 400 | }) 401 | }, 402 | ) 403 | .map_err(|e| { 404 | error!("Unexpected error in tus_try_and_set_vuser_variable SELECT: {}", e); 405 | DbError::Internal 406 | }), 407 | Err(e) => { 408 | error!("Unexpected error in tus_try_and_set_vuser_variable: {}", e); 409 | Err(DbError::Internal) 410 | } 411 | } 412 | } 413 | 414 | pub fn tus_delete_user_variable_with_slotlist(&self, com_id: &ComId, user: i64, slot_list: &[i32]) -> Result<(), DbError> { 415 | let stmt = format!( 416 | "DELETE FROM tus_var WHERE communication_id = ?1 AND owner_id = ?2 AND slot_id IN ({})", 417 | generate_string_from_slot_list(slot_list) 418 | ); 419 | 420 | self.conn.execute(&stmt, rusqlite::params![com_id, user]).map_err(|e| { 421 | error!("Unexpected error deleting variable in tus_delete_user_variable_with_slotlist: {}", e); 422 | DbError::Internal 423 | })?; 424 | 425 | Ok(()) 426 | } 427 | 428 | pub fn tus_delete_vuser_variable_with_slotlist(&self, com_id: &ComId, vuser: &str, slot_list: &[i32]) -> Result<(), DbError> { 429 | let stmt = format!( 430 | "DELETE FROM tus_var_vuser WHERE communication_id = ?1 AND vuser = ?2 AND slot_id IN ({})", 431 | generate_string_from_slot_list(slot_list) 432 | ); 433 | 434 | self.conn.execute(&stmt, rusqlite::params![com_id, vuser]).map_err(|e| { 435 | error!("Unexpected error deleting variable in tus_delete_vuser_variable_with_slotlist: {}", e); 436 | DbError::Internal 437 | })?; 438 | 439 | Ok(()) 440 | } 441 | 442 | pub fn tus_get_user_data_timestamp_and_author(&self, com_id: &ComId, user: i64, slot: i32) -> Result, DbError> { 443 | let res = self.conn.query_row( 444 | "SELECT timestamp, author_id FROM tus_data WHERE communication_id = ?1 AND owner_id = ?2 AND slot_id = ?3", 445 | rusqlite::params![com_id, user, slot], 446 | |r| Ok((r.get_unwrap(0), r.get_unwrap(1))), 447 | ); 448 | 449 | match res { 450 | Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), 451 | Err(_) => Err(DbError::Internal), 452 | Ok(res) => Ok(Some(res)), 453 | } 454 | } 455 | 456 | pub fn tus_get_vuser_data_timestamp_and_author(&self, com_id: &ComId, vuser: &str, slot: i32) -> Result, DbError> { 457 | let res = self.conn.query_row( 458 | "SELECT timestamp, author_id FROM tus_data_vuser WHERE communication_id = ?1 AND vuser = ?2 AND slot_id = ?3", 459 | rusqlite::params![com_id, vuser, slot], 460 | |r| Ok((r.get_unwrap(0), r.get_unwrap(1))), 461 | ); 462 | 463 | match res { 464 | Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), 465 | Err(_) => Err(DbError::Internal), 466 | Ok(res) => Ok(Some(res)), 467 | } 468 | } 469 | 470 | pub fn tus_set_user_data( 471 | &self, 472 | com_id: &ComId, 473 | user: i64, 474 | slot: i32, 475 | data_id: u64, 476 | info: &Option<&[u8]>, 477 | author_id: i64, 478 | timestamp: u64, 479 | compare_timestamp: Option, 480 | compare_author: Option, 481 | ) -> Result<(), DbError> { 482 | let cond_string = Database::prepare_cond_string(compare_timestamp, compare_author); 483 | let full_query = format!( 484 | "INSERT INTO tus_data( owner_id, communication_id, slot_id, data_id, data_info, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 ) \ 485 | ON CONFLICT ( owner_id, communication_id, slot_id ) DO \ 486 | UPDATE SET data_id = excluded.data_id, data_info = excluded.data_info, timestamp = excluded.timestamp, author_id = excluded.author_id \ 487 | {} RETURNING TRUE", 488 | cond_string 489 | ); 490 | 491 | self.conn 492 | .query_row(&full_query, rusqlite::params![user, com_id, slot, data_id, info.unwrap_or_default(), timestamp, author_id], |_| Ok(())) 493 | .map_err(|e| match e { 494 | rusqlite::Error::QueryReturnedNoRows => DbError::Empty, 495 | _ => { 496 | error!("Unexpected error in tus_set_user_data: {}", e); 497 | DbError::Internal 498 | } 499 | }) 500 | } 501 | 502 | pub fn tus_set_vuser_data( 503 | &self, 504 | com_id: &ComId, 505 | vuser: &str, 506 | slot: i32, 507 | data_id: u64, 508 | info: &Option<&[u8]>, 509 | author_id: i64, 510 | timestamp: u64, 511 | compare_timestamp: Option, 512 | compare_author: Option, 513 | ) -> Result<(), DbError> { 514 | let cond_string = Database::prepare_cond_string(compare_timestamp, compare_author); 515 | let full_query = format!( 516 | "INSERT INTO tus_data_vuser( vuser, communication_id, slot_id, data_id, data_info, timestamp, author_id ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 ) \ 517 | ON CONFLICT ( vuser, communication_id, slot_id ) DO \ 518 | UPDATE SET data_id = excluded.data_id, data_info = excluded.data_info, timestamp = excluded.timestamp, author_id = excluded.author_id \ 519 | {} RETURNING TRUE", 520 | cond_string 521 | ); 522 | 523 | self.conn 524 | .query_row(&full_query, rusqlite::params![vuser, com_id, slot, data_id, info.unwrap_or_default(), timestamp, author_id], |_| Ok(())) 525 | .map_err(|e| match e { 526 | rusqlite::Error::QueryReturnedNoRows => DbError::Empty, 527 | _ => { 528 | error!("Unexpected error in tus_set_vuser_data: {}", e); 529 | DbError::Internal 530 | } 531 | }) 532 | } 533 | 534 | pub fn tus_get_user_data(&self, com_id: &ComId, user: i64, slot: i32) -> Result<(DbTusDataStatus, Vec), DbError> { 535 | self.conn 536 | .query_row( 537 | "SELECT timestamp, author_id, data_id, data_info FROM tus_data WHERE communication_id = ?1 AND owner_id = ?2 AND slot_id = ?3", 538 | rusqlite::params![com_id, user, slot], 539 | |r| { 540 | Ok(( 541 | DbTusDataStatus { 542 | timestamp: r.get_unwrap(0), 543 | author_id: r.get_unwrap(1), 544 | data_id: r.get_unwrap(2), 545 | }, 546 | r.get_unwrap(3), 547 | )) 548 | }, 549 | ) 550 | .map_err(|e| match e { 551 | rusqlite::Error::QueryReturnedNoRows => DbError::Empty, 552 | e => { 553 | error!("Unexpected error in tus_get_user_data: {}", e); 554 | DbError::Internal 555 | } 556 | }) 557 | } 558 | 559 | pub fn tus_get_vuser_data(&self, com_id: &ComId, vuser: &str, slot: i32) -> Result<(DbTusDataStatus, Vec), DbError> { 560 | self.conn 561 | .query_row( 562 | "SELECT timestamp, author_id, data_id, data_info FROM tus_data_vuser WHERE communication_id = ?1 AND vuser = ?2 AND slot_id = ?3", 563 | rusqlite::params![com_id, vuser, slot], 564 | |r| { 565 | Ok(( 566 | DbTusDataStatus { 567 | timestamp: r.get_unwrap(0), 568 | author_id: r.get_unwrap(1), 569 | data_id: r.get_unwrap(2), 570 | }, 571 | r.get_unwrap(3), 572 | )) 573 | }, 574 | ) 575 | .map_err(|e| match e { 576 | rusqlite::Error::QueryReturnedNoRows => DbError::Empty, 577 | e => { 578 | error!("Unexpected error in tus_get_vuser_data: {}", e); 579 | DbError::Internal 580 | } 581 | }) 582 | } 583 | 584 | pub fn tus_get_user_data_from_user_list( 585 | &self, 586 | com_id: &ComId, 587 | user_list: &[i64], 588 | slot: i32, 589 | sorting: cmd_tus::TusStatusSorting, 590 | max: u32, 591 | ) -> Result)>, DbError> { 592 | let stmt_sorting = match sorting { 593 | TusStatusSorting::SCE_NP_TUS_DATASTATUS_SORTTYPE_DESCENDING_DATE => "timestamp DESC", 594 | TusStatusSorting::SCE_NP_TUS_DATASTATUS_SORTTYPE_ASCENDING_DATE => "timestamp ASC", 595 | }; 596 | 597 | let stmt_string = format!( 598 | "SELECT owner_id, timestamp, author_id, data_id, data_info FROM tus_data WHERE communication_id = ? AND slot_id = ? AND owner_id IN ({}) ORDER BY {} LIMIT {}", 599 | generate_string_from_user_list(user_list), 600 | stmt_sorting, 601 | max 602 | ); 603 | 604 | let mut stmt = self.conn.prepare(&stmt_string).map_err(|_| DbError::Internal)?; 605 | 606 | let rows = stmt 607 | .query_map(rusqlite::params! { com_id, slot }, |r| { 608 | Ok(( 609 | r.get_unwrap(0), 610 | DbTusDataStatus { 611 | timestamp: r.get_unwrap(1), 612 | author_id: r.get_unwrap(2), 613 | data_id: r.get_unwrap(3), 614 | }, 615 | r.get_unwrap(4), 616 | )) 617 | }) 618 | .map_err(|_| DbError::Internal)?; 619 | 620 | let mut vec_var_status = Vec::new(); 621 | for row in rows { 622 | vec_var_status.push(row.unwrap()); 623 | } 624 | 625 | Ok(vec_var_status) 626 | } 627 | 628 | pub fn tus_delete_user_data_with_slotlist(&self, com_id: &ComId, user: i64, slot_list: &[i32]) -> Result<(), DbError> { 629 | let stmt = format!( 630 | "DELETE FROM tus_data WHERE communication_id = ?1 AND owner_id = ?2 AND slot_id IN ({})", 631 | generate_string_from_slot_list(slot_list) 632 | ); 633 | 634 | self.conn.execute(&stmt, rusqlite::params![com_id, user]).map_err(|e| { 635 | error!("Unexpected error deleting variable in tus_delete_user_data_with_slotlist: {}", e); 636 | DbError::Internal 637 | })?; 638 | 639 | Ok(()) 640 | } 641 | 642 | pub fn tus_delete_vuser_data_with_slotlist(&self, com_id: &ComId, vuser: &str, slot_list: &[i32]) -> Result<(), DbError> { 643 | let stmt = format!( 644 | "DELETE FROM tus_data_vuser WHERE communication_id = ?1 AND vuser = ?2 AND slot_id IN ({})", 645 | generate_string_from_slot_list(slot_list) 646 | ); 647 | 648 | self.conn.execute(&stmt, rusqlite::params![com_id, vuser]).map_err(|e| { 649 | error!("Unexpected error deleting variable in tus_delete_vuser_data_with_slotlist: {}", e); 650 | DbError::Internal 651 | })?; 652 | 653 | Ok(()) 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /src/server/game_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::convert::Infallible; 3 | use std::fmt::Write; 4 | use std::io; 5 | use std::net::ToSocketAddrs; 6 | use std::sync::atomic::{AtomicI64, AtomicU32, Ordering}; 7 | use std::sync::Arc; 8 | 9 | use hyper::server::conn::http1; 10 | use hyper::service::service_fn; 11 | use hyper::{Method, Request, Response}; 12 | use hyper_util::rt::TokioIo; 13 | use parking_lot::{Mutex, RwLock}; 14 | use tokio::net::TcpListener; 15 | use tracing::{error, info, warn}; 16 | 17 | use crate::server::client::{com_id_to_string, ComId, TerminateWatch}; 18 | use crate::server::Server; 19 | use crate::Client; 20 | 21 | struct GameInfo { 22 | num_users: AtomicI64, 23 | name_hints: RwLock>, 24 | } 25 | 26 | struct CachedResponse { 27 | timestamp: AtomicU32, 28 | cached_response: Mutex>, 29 | } 30 | 31 | pub struct GameTracker { 32 | num_users: AtomicI64, 33 | psn_games: RwLock>, 34 | ticket_games: RwLock>, 35 | cached_response: CachedResponse, 36 | } 37 | 38 | impl Server { 39 | pub async fn start_stat_server(&self, term_watch: TerminateWatch, game_tracker: Arc) -> io::Result<()> { 40 | let (bind_addr, timeout); 41 | { 42 | let config = self.config.read(); 43 | bind_addr = config.get_stat_server_binds().clone(); 44 | timeout = config.get_stat_server_timeout(); 45 | } 46 | 47 | if let Some((host, port)) = &bind_addr { 48 | let str_addr = host.to_owned() + ":" + port; 49 | let mut addr = str_addr 50 | .to_socket_addrs() 51 | .map_err(|e| io::Error::new(e.kind(), format!("Stat: {} is not a valid address", &str_addr)))?; 52 | let addr = addr 53 | .next() 54 | .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, format!("Stat: {} is not a valid address", &str_addr)))?; 55 | 56 | let listener = TcpListener::bind(addr) 57 | .await 58 | .map_err(|e| io::Error::new(e.kind(), format!("Stat: error binding to <{}>: {}", &addr, e)))?; 59 | 60 | info!("Stat server now waiting for connections on {}", str_addr); 61 | 62 | tokio::task::spawn(async move { 63 | GameTracker::server_proc(listener, term_watch, game_tracker, timeout).await; 64 | }); 65 | } 66 | 67 | Ok(()) 68 | } 69 | } 70 | 71 | impl GameTracker { 72 | async fn server_proc(listener: TcpListener, mut term_watch: TerminateWatch, game_tracker: Arc, timeout: u32) { 73 | 'stat_server_loop: loop { 74 | tokio::select! { 75 | accept_res = listener.accept() => { 76 | if let Err(e) = accept_res { 77 | warn!("Stat: Error accepting a client: {}", e); 78 | continue 'stat_server_loop; 79 | } 80 | 81 | let (stream, peer_addr) = accept_res.unwrap(); 82 | let io = TokioIo::new(stream); 83 | 84 | info!("Stat: new client from {}", peer_addr); 85 | { 86 | let game_tracker = game_tracker.clone(); 87 | tokio::task::spawn(async move { 88 | if let Err(err) = http1::Builder::new().keep_alive(false).serve_connection(io, service_fn(|r| GameTracker::handle_stat_server_req(r, game_tracker.clone(), timeout))).await { 89 | warn!("Stat: Error serving connection: {}", err); 90 | } 91 | }); 92 | } 93 | } 94 | _ = term_watch.recv.changed() => { 95 | break 'stat_server_loop; 96 | } 97 | } 98 | } 99 | info!("GameTracker::server_proc terminating"); 100 | } 101 | 102 | async fn handle_stat_server_req(req: Request, game_tracker: Arc, timeout: u32) -> Result, Infallible> { 103 | if req.method() != Method::GET || req.uri() != "/rpcn_stats" { 104 | return Ok(Response::new("".to_owned())); 105 | } 106 | 107 | if timeout == 0 { 108 | return Ok(Response::builder().header("Content-Type", "application/json").body(game_tracker.to_json()).unwrap()); 109 | } 110 | 111 | let new_timestamp = Client::get_timestamp_seconds(); 112 | let mut response = game_tracker.cached_response.cached_response.lock(); 113 | 114 | if new_timestamp > game_tracker.cached_response.timestamp.load(Ordering::SeqCst) + timeout { 115 | *response = Response::builder().header("Content-Type", "application/json").body(game_tracker.to_json()).unwrap(); 116 | } 117 | 118 | Ok((*response).clone()) 119 | } 120 | 121 | fn to_json(&self) -> String { 122 | let psn_games: Vec<(String, i64, Vec)> = self 123 | .psn_games 124 | .read() 125 | .iter() 126 | .filter_map(|(name, game_info)| { 127 | let num_users = game_info.num_users.load(Ordering::SeqCst); 128 | if num_users != 0 { 129 | Some((com_id_to_string(name), num_users, game_info.name_hints.read().iter().cloned().collect())) 130 | } else { 131 | None 132 | } 133 | }) 134 | .collect(); 135 | 136 | let ticket_games: Vec<(String, i64)> = self 137 | .ticket_games 138 | .read() 139 | .iter() 140 | .filter_map(|(name, num_users)| { 141 | let num_users = num_users.load(Ordering::SeqCst); 142 | if num_users != 0 { 143 | Some((name.clone(), num_users)) 144 | } else { 145 | None 146 | } 147 | }) 148 | .collect(); 149 | 150 | let mut res = String::from("{\n"); 151 | let _ = write!(res, " \"num_users\" : {}", self.num_users.load(Ordering::SeqCst)); 152 | 153 | // The game id doesn't need to be sanitized as it is composed only of alphanumerical ascii chars(checked before being passed to game tracker) 154 | 155 | let sanitize_for_json = |s: &str| -> String { 156 | let mut res = String::with_capacity(s.len()); 157 | 158 | for c in s.chars() { 159 | match c { 160 | '"' => { 161 | let _ = write!(res, "\\\""); 162 | } 163 | '\\' => { 164 | let _ = write!(res, "\\\\"); 165 | } 166 | '\x08' | '\x0C' | '\n' | '\r' | '\t' => {} // \b and \f 167 | _ => res.push(c), 168 | } 169 | } 170 | 171 | res 172 | }; 173 | 174 | let add_games_with_hints = |string: &mut String, section_name: &str, v: &Vec<(String, i64, Vec)>| { 175 | if !v.is_empty() { 176 | let _ = write!(string, ",\n \"{}\": {{\n", section_name); 177 | 178 | for (index, (name, num, name_hints)) in v.iter().enumerate() { 179 | let _ = write!(string, " \"{}\": [{}", name, num); 180 | for hint in name_hints { 181 | let _ = write!(string, ", \"{}\"", sanitize_for_json(hint)); 182 | } 183 | let _ = write!(string, "]"); 184 | *string += if index != (v.len() - 1) { ",\n" } else { "\n" }; 185 | } 186 | 187 | *string += " }" 188 | } 189 | }; 190 | 191 | let add_games = |string: &mut String, section_name: &str, v: &Vec<(String, i64)>| { 192 | if !v.is_empty() { 193 | let _ = write!(string, ",\n \"{}\": {{\n", section_name); 194 | 195 | for (index, (name, num)) in v.iter().enumerate() { 196 | let _ = write!(string, " \"{}\": {}", name, num); 197 | *string += if index != (v.len() - 1) { ",\n" } else { "\n" }; 198 | } 199 | 200 | *string += " }" 201 | } 202 | }; 203 | 204 | add_games_with_hints(&mut res, "psn_games", &psn_games); 205 | add_games(&mut res, "ticket_games", &ticket_games); 206 | 207 | res += "\n}"; 208 | 209 | res 210 | } 211 | 212 | pub fn new() -> GameTracker { 213 | GameTracker { 214 | num_users: AtomicI64::new(0), 215 | psn_games: RwLock::new(HashMap::new()), 216 | ticket_games: RwLock::new(HashMap::new()), 217 | cached_response: CachedResponse { 218 | timestamp: AtomicU32::new(0), 219 | cached_response: Mutex::new(Response::new("".to_string())), 220 | }, 221 | } 222 | } 223 | 224 | pub fn add_gamename_hint(&self, com_id: &ComId, name_hint: &str) { 225 | let psn_games = self.psn_games.read(); 226 | let game_info = psn_games.get(com_id); 227 | if game_info.is_none() { 228 | error!("Inconsistency in gametracker!"); 229 | return; 230 | } 231 | let game_info = game_info.unwrap(); 232 | 233 | if game_info.name_hints.read().contains(name_hint) { 234 | return; 235 | } 236 | 237 | game_info.name_hints.write().insert(name_hint.to_string()); 238 | } 239 | 240 | pub fn increase_num_users(&self) { 241 | self.num_users.fetch_add(1, Ordering::SeqCst); 242 | } 243 | 244 | pub fn decrease_num_users(&self) { 245 | self.num_users.fetch_sub(1, Ordering::SeqCst); 246 | } 247 | 248 | fn add_value_psn(&self, com_id: &ComId, to_add: i64) { 249 | { 250 | let psn_games = self.psn_games.read(); 251 | if psn_games.contains_key(com_id) { 252 | psn_games[com_id].num_users.fetch_add(to_add, Ordering::SeqCst); 253 | return; 254 | } 255 | } 256 | 257 | self.psn_games 258 | .write() 259 | .entry(*com_id) 260 | .or_insert_with(|| GameInfo { 261 | num_users: AtomicI64::new(0), 262 | name_hints: RwLock::new(HashSet::new()), 263 | }) 264 | .num_users 265 | .fetch_add(to_add, Ordering::SeqCst); 266 | } 267 | 268 | pub fn increase_count_psn(&self, com_id: &ComId) { 269 | self.add_value_psn(com_id, 1); 270 | } 271 | 272 | pub fn decrease_count_psn(&self, com_id: &ComId) { 273 | self.add_value_psn(com_id, -1); 274 | } 275 | 276 | fn add_value_ticket(&self, service_id: &str, to_add: i64) { 277 | { 278 | let ticket_games = self.ticket_games.read(); 279 | if ticket_games.contains_key(service_id) { 280 | ticket_games[service_id].fetch_add(to_add, Ordering::SeqCst); 281 | return; 282 | } 283 | } 284 | 285 | self.ticket_games 286 | .write() 287 | .entry(service_id.to_owned()) 288 | .or_insert_with(|| AtomicI64::new(0)) 289 | .fetch_add(to_add, Ordering::SeqCst); 290 | } 291 | 292 | pub fn increase_count_ticket(&self, service_id: &str) { 293 | self.add_value_ticket(service_id, 1); 294 | } 295 | 296 | pub fn decrease_count_ticket(&self, service_id: &str) { 297 | self.add_value_ticket(service_id, -1); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/server/score_cache.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::sync::Arc; 4 | 5 | use crate::server::client::{com_id_to_string, Client, ComId, ErrorType}; 6 | use crate::server::database::db_score::{DbBoardInfo, DbScoreInfo}; 7 | use crate::server::database::Database; 8 | use crate::server::stream_extractor::np2_structs_generated::*; 9 | use crate::server::Server; 10 | 11 | use parking_lot::RwLock; 12 | use tracing::warn; 13 | 14 | struct ScoreUserCache { 15 | npid: String, 16 | online_name: String, 17 | } 18 | 19 | struct ScoreTableCache { 20 | sorted_scores: Vec, 21 | npid_lookup: HashMap>, 22 | table_info: DbBoardInfo, 23 | last_insert: u64, 24 | } 25 | 26 | pub struct ScoresCache { 27 | tables: RwLock>>>, 28 | users: RwLock>, 29 | } 30 | 31 | #[derive(Default)] 32 | pub struct ScoreRankDataCache { 33 | npid: String, 34 | online_name: String, 35 | pcid: i32, 36 | rank: u32, 37 | score: i64, 38 | has_gamedata: bool, 39 | timestamp: u64, 40 | } 41 | 42 | pub struct GetScoreResultCache { 43 | pub scores: Vec, 44 | pub comments: Option>, 45 | pub infos: Option>>, 46 | pub timestamp: u64, 47 | pub total_records: u32, 48 | } 49 | 50 | #[derive(Hash, PartialEq, Eq)] 51 | pub struct TableDescription { 52 | com_id: String, 53 | board_id: u32, 54 | } 55 | 56 | impl TableDescription { 57 | pub fn new(com_id: String, board_id: u32) -> TableDescription { 58 | TableDescription { com_id, board_id } 59 | } 60 | } 61 | 62 | impl Server { 63 | pub fn initialize_score(conn: r2d2::PooledConnection) -> Result, String> { 64 | let db = Database::new(conn); 65 | 66 | Server::setup_config_scoreboards(&db)?; 67 | Server::initialize_score_data_handler()?; 68 | 69 | let cache = Arc::new(ScoresCache::new()); 70 | 71 | // Populate cache from database 72 | let tables = db.get_score_tables().map_err(|_| "Failed to read database scores for the cache")?; 73 | 74 | let mut users_list: HashSet = HashSet::new(); 75 | 76 | { 77 | let mut cache_data = cache.tables.write(); 78 | for ((com_id, board_id), table_info) in tables { 79 | let sorted_scores = db 80 | .get_scores_from_table(&com_id, board_id, &table_info) 81 | .map_err(|_| format!("Failed to read scores for table {}:{}", com_id, board_id))?; 82 | let mut npid_lookup = HashMap::new(); 83 | 84 | for (index, score) in sorted_scores.iter().enumerate() { 85 | users_list.insert(score.user_id); 86 | let user_entry: &mut HashMap = npid_lookup.entry(score.user_id).or_default(); 87 | user_entry.insert(score.character_id, index); 88 | } 89 | 90 | cache_data.insert( 91 | TableDescription::new(com_id, board_id), 92 | Arc::new(RwLock::new(ScoreTableCache { 93 | sorted_scores, 94 | npid_lookup, 95 | table_info, 96 | last_insert: Client::get_psn_timestamp(), 97 | })), 98 | ); 99 | } 100 | } 101 | 102 | { 103 | let vec_userinfo = db 104 | .get_username_and_online_name_from_user_ids(&users_list) 105 | .map_err(|_| "Failed to acquire all the users informations!")?; 106 | let mut users_data = cache.users.write(); 107 | vec_userinfo.iter().for_each(|u| { 108 | users_data.insert( 109 | u.0, 110 | ScoreUserCache { 111 | npid: u.1.clone(), 112 | online_name: u.2.clone(), 113 | }, 114 | ); 115 | }); 116 | } 117 | 118 | Ok(cache) 119 | } 120 | } 121 | 122 | impl ScoreRankDataCache { 123 | pub fn to_flatbuffer<'a>(&self, builder: &mut flatbuffers::FlatBufferBuilder<'a>) -> flatbuffers::WIPOffset> { 124 | let str_npid = builder.create_string(&self.npid); 125 | let str_online_name = builder.create_string(&self.online_name); 126 | ScoreRankData::create( 127 | builder, 128 | &ScoreRankDataArgs { 129 | npId: Some(str_npid), 130 | onlineName: Some(str_online_name), 131 | pcId: self.pcid, 132 | rank: self.rank + 1, 133 | score: self.score, 134 | hasGameData: self.has_gamedata, 135 | recordDate: self.timestamp, 136 | }, 137 | ) 138 | } 139 | } 140 | 141 | impl GetScoreResultCache { 142 | pub fn serialize(&self) -> Vec { 143 | let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); 144 | 145 | let mut vec_ranks = Vec::new(); 146 | for s in &self.scores { 147 | vec_ranks.push(s.to_flatbuffer(&mut builder)); 148 | } 149 | let rank_array = Some(builder.create_vector(&vec_ranks)); 150 | 151 | let comment_array = if let Some(ref comments) = self.comments { 152 | let mut vec_comments = Vec::new(); 153 | for c in comments { 154 | vec_comments.push(builder.create_string(c)); 155 | } 156 | Some(builder.create_vector(&vec_comments)) 157 | } else { 158 | None 159 | }; 160 | 161 | let info_array = if let Some(ref game_infos) = self.infos { 162 | let mut vec_infos = Vec::new(); 163 | for i in game_infos { 164 | let data = Some(builder.create_vector(i)); 165 | vec_infos.push(ScoreInfo::create(&mut builder, &ScoreInfoArgs { data })); 166 | } 167 | Some(builder.create_vector(&vec_infos)) 168 | } else { 169 | None 170 | }; 171 | 172 | let board_info = GetScoreResponse::create( 173 | &mut builder, 174 | &GetScoreResponseArgs { 175 | rankArray: rank_array, 176 | commentArray: comment_array, 177 | infoArray: info_array, 178 | lastSortDate: self.timestamp, 179 | totalRecord: self.total_records, 180 | }, 181 | ); 182 | builder.finish(board_info, None); 183 | builder.finished_data().to_vec() 184 | } 185 | } 186 | 187 | impl DbScoreInfo { 188 | fn cmp(&self, other: &DbScoreInfo, sort_mode: u32) -> Ordering { 189 | let ret_value = if sort_mode == 0 { Ordering::Less } else { Ordering::Greater }; 190 | 191 | match self.score.cmp(&other.score) { 192 | Ordering::Greater => ret_value, 193 | Ordering::Less => ret_value.reverse(), 194 | Ordering::Equal => match self.timestamp.cmp(&other.timestamp) { 195 | Ordering::Less => Ordering::Less, 196 | Ordering::Greater => Ordering::Greater, 197 | Ordering::Equal => other.user_id.cmp(&self.user_id), 198 | }, 199 | } 200 | } 201 | } 202 | 203 | impl ScoreTableCache { 204 | fn from_db(db_info: &DbBoardInfo) -> ScoreTableCache { 205 | ScoreTableCache { 206 | sorted_scores: Vec::new(), 207 | npid_lookup: HashMap::new(), 208 | table_info: db_info.clone(), 209 | last_insert: 0, 210 | } 211 | } 212 | } 213 | 214 | impl ScoresCache { 215 | fn new() -> ScoresCache { 216 | ScoresCache { 217 | tables: RwLock::new(HashMap::new()), 218 | users: RwLock::new(HashMap::new()), 219 | } 220 | } 221 | 222 | fn get_table(&self, com_id: &ComId, board_id: u32, db_info: &DbBoardInfo) -> Arc> { 223 | let table_desc = TableDescription::new(com_id_to_string(com_id), board_id); 224 | { 225 | let tables = self.tables.read(); 226 | if let Some(table) = tables.get(&table_desc) { 227 | return table.clone(); 228 | } 229 | } 230 | 231 | self.tables 232 | .write() 233 | .entry(table_desc) 234 | .or_insert_with(|| Arc::new(RwLock::new(ScoreTableCache::from_db(db_info)))) 235 | .clone() 236 | } 237 | 238 | fn add_user(&self, user_id: i64, npid: &str, online_name: &str) { 239 | if self.users.read().contains_key(&user_id) { 240 | return; 241 | } 242 | 243 | self.users.write().insert( 244 | user_id, 245 | ScoreUserCache { 246 | npid: npid.to_owned(), 247 | online_name: online_name.to_owned(), 248 | }, 249 | ); 250 | } 251 | 252 | pub fn insert_score(&self, db_info: &DbBoardInfo, com_id: &ComId, board_id: u32, score: &DbScoreInfo, npid: &str, online_name: &str) -> u32 { 253 | let table = self.get_table(com_id, board_id, db_info); 254 | let mut table = table.write(); 255 | 256 | // First check if the user_id/char_id is already in the cache 257 | // Note that the inserted position may be lower to the previous position if the score inserted is the same but timestamp is higher 258 | let mut initial_pos = None; 259 | 260 | if table.npid_lookup.contains_key(&score.user_id) && table.npid_lookup[&score.user_id].contains_key(&score.character_id) { 261 | let pos = table.npid_lookup[&score.user_id][&score.character_id]; 262 | table.sorted_scores.remove(pos); 263 | initial_pos = Some(pos); 264 | } 265 | 266 | // Update last insert/sort time 267 | table.last_insert = Client::get_psn_timestamp(); 268 | 269 | if (table.sorted_scores.len() < table.table_info.rank_limit as usize) || score.cmp(table.sorted_scores.last().unwrap(), table.table_info.sort_mode) == Ordering::Less { 270 | let insert_pos = table.sorted_scores.binary_search_by(|probe| probe.cmp(score, table.table_info.sort_mode)).unwrap_err(); 271 | table.sorted_scores.insert(insert_pos, (*score).clone()); 272 | table.npid_lookup.entry(score.user_id).or_default().insert(score.character_id, insert_pos); 273 | 274 | let reorder_start = if let Some(pos) = initial_pos { std::cmp::min(pos, insert_pos + 1) } else { insert_pos + 1 }; 275 | 276 | // Set index for everything after insert_pos 277 | for i in reorder_start..table.sorted_scores.len() { 278 | let cur_user_id = table.sorted_scores[i].user_id; 279 | let cur_char_id = table.sorted_scores[i].character_id; 280 | *table.npid_lookup.get_mut(&cur_user_id).unwrap().get_mut(&cur_char_id).unwrap() = i; 281 | } 282 | 283 | if table.sorted_scores.len() > table.table_info.rank_limit as usize { 284 | // Remove the last score 285 | let last = table.sorted_scores.last().unwrap(); 286 | let user_id = last.user_id; 287 | let char_id = last.character_id; 288 | 289 | let last_index = table.sorted_scores.len() - 1; 290 | table.sorted_scores.remove(last_index); 291 | table.npid_lookup.get_mut(&user_id).unwrap().remove(&char_id); 292 | } 293 | 294 | self.add_user(score.user_id, npid, online_name); 295 | 296 | (insert_pos + 1) as u32 297 | } else { 298 | table.table_info.rank_limit + 1 299 | } 300 | } 301 | 302 | pub fn get_score_range(&self, com_id: &ComId, board_id: u32, start_rank: u32, num_ranks: u32, with_comment: bool, with_gameinfo: bool) -> GetScoreResultCache { 303 | let mut vec_scores = Vec::new(); 304 | let mut vec_comments = if with_comment { Some(Vec::new()) } else { None }; 305 | let mut vec_gameinfos = if with_gameinfo { Some(Vec::new()) } else { None }; 306 | 307 | let table = self.get_table(com_id, board_id, &Default::default()); 308 | let table = table.read(); 309 | let users = self.users.read(); 310 | 311 | let start_index = (start_rank - 1) as usize; 312 | let end_index = std::cmp::min(start_index + num_ranks as usize, table.sorted_scores.len()); 313 | for index in start_index..end_index { 314 | let cur_score = &table.sorted_scores[index]; 315 | let cur_user = &users[&cur_score.user_id]; 316 | vec_scores.push(ScoreRankDataCache { 317 | npid: cur_user.npid.clone(), 318 | online_name: cur_user.online_name.clone(), 319 | pcid: cur_score.character_id, 320 | rank: index as u32, 321 | score: cur_score.score, 322 | has_gamedata: cur_score.data_id.is_some(), 323 | timestamp: cur_score.timestamp, 324 | }); 325 | if let Some(ref mut comments) = vec_comments { 326 | comments.push(cur_score.comment.clone().unwrap_or_default()); 327 | } 328 | if let Some(ref mut gameinfos) = vec_gameinfos { 329 | gameinfos.push(cur_score.game_info.clone().unwrap_or_default()); 330 | } 331 | } 332 | GetScoreResultCache { 333 | scores: vec_scores, 334 | comments: vec_comments, 335 | infos: vec_gameinfos, 336 | timestamp: table.last_insert, 337 | total_records: table.sorted_scores.len() as u32, 338 | } 339 | } 340 | 341 | pub fn get_score_ids(&self, com_id: &ComId, board_id: u32, npids: &[(i64, i32)], with_comment: bool, with_gameinfo: bool) -> GetScoreResultCache { 342 | let mut vec_scores = Vec::new(); 343 | let mut vec_comments = if with_comment { Some(Vec::new()) } else { None }; 344 | let mut vec_gameinfos = if with_gameinfo { Some(Vec::new()) } else { None }; 345 | 346 | let table = self.get_table(com_id, board_id, &Default::default()); 347 | let table = table.read(); 348 | let users = self.users.read(); 349 | 350 | npids.iter().for_each(|(user_id, pcid)| { 351 | if !table.npid_lookup.contains_key(user_id) || !table.npid_lookup[user_id].contains_key(pcid) { 352 | vec_scores.push(Default::default()); 353 | if let Some(ref mut comments) = vec_comments { 354 | comments.push(Default::default()); 355 | } 356 | if let Some(ref mut gameinfos) = vec_gameinfos { 357 | gameinfos.push(Default::default()); 358 | } 359 | } else { 360 | let index = table.npid_lookup[user_id][pcid]; 361 | let cur_user = &users[user_id]; 362 | let cur_score = &table.sorted_scores[index]; 363 | vec_scores.push(ScoreRankDataCache { 364 | npid: cur_user.npid.clone(), 365 | online_name: cur_user.online_name.clone(), 366 | pcid: cur_score.character_id, 367 | rank: index as u32, 368 | score: cur_score.score, 369 | has_gamedata: cur_score.data_id.is_some(), 370 | timestamp: cur_score.timestamp, 371 | }); 372 | if let Some(ref mut comments) = vec_comments { 373 | comments.push(cur_score.comment.clone().unwrap_or_default()); 374 | } 375 | if let Some(ref mut gameinfos) = vec_gameinfos { 376 | gameinfos.push(cur_score.game_info.clone().unwrap_or_default()); 377 | } 378 | } 379 | }); 380 | 381 | GetScoreResultCache { 382 | scores: vec_scores, 383 | comments: vec_comments, 384 | infos: vec_gameinfos, 385 | timestamp: table.last_insert, 386 | total_records: table.sorted_scores.len() as u32, 387 | } 388 | } 389 | 390 | pub fn contains_score_with_no_data(&self, com_id: &ComId, user_id: i64, character_id: i32, board_id: u32, score: i64) -> Result<(), ErrorType> { 391 | let table = self.get_table(com_id, board_id, &DbBoardInfo::default()); 392 | 393 | { 394 | let table = table.read(); 395 | if !table.npid_lookup.contains_key(&user_id) || !table.npid_lookup[&user_id].contains_key(&character_id) { 396 | warn!("Attempted to set score data for a missing UserId/PcId!"); 397 | return Err(ErrorType::NotFound); 398 | } 399 | 400 | let rank = table.npid_lookup[&user_id][&character_id]; 401 | let the_score = &table.sorted_scores[rank]; 402 | 403 | if the_score.score != score { 404 | warn!("Attempted to update score data for wrong score!"); 405 | return Err(ErrorType::ScoreInvalid); 406 | } 407 | 408 | if the_score.data_id.is_some() { 409 | warn!("Attempted to update score data of score with existing score data!"); 410 | return Err(ErrorType::ScoreHasData); 411 | } 412 | } 413 | 414 | Ok(()) 415 | } 416 | 417 | pub fn set_game_data(&self, com_id: &ComId, user_id: i64, character_id: i32, board_id: u32, data_id: u64) -> Result<(), ErrorType> { 418 | let table = self.get_table(com_id, board_id, &DbBoardInfo::default()); 419 | 420 | { 421 | // Existence needs to be checked again as another score might have pushed this one out 422 | let mut table = table.write(); 423 | if !table.npid_lookup.contains_key(&user_id) || !table.npid_lookup[&user_id].contains_key(&character_id) { 424 | return Err(ErrorType::NotFound); 425 | } 426 | 427 | // Score itself doesn't need to be checked as only current user can change it 428 | 429 | let rank = table.npid_lookup[&user_id][&character_id]; 430 | let the_score = &mut table.sorted_scores[rank]; 431 | 432 | the_score.data_id = Some(data_id); 433 | } 434 | 435 | Ok(()) 436 | } 437 | 438 | pub fn get_game_data_id(&self, com_id: &ComId, user_id: i64, character_id: i32, board_id: u32) -> Result { 439 | let table = self.get_table(com_id, board_id, &DbBoardInfo::default()); 440 | 441 | { 442 | let table = table.read(); 443 | if !table.npid_lookup.contains_key(&user_id) || !table.npid_lookup[&user_id].contains_key(&character_id) { 444 | return Err(ErrorType::NotFound); 445 | } 446 | 447 | let rank = table.npid_lookup[&user_id][&character_id]; 448 | let the_score = &table.sorted_scores[rank]; 449 | if let Some(data_id) = the_score.data_id { 450 | Ok(data_id) 451 | } else { 452 | Err(ErrorType::NotFound) 453 | } 454 | } 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/server/stream_extractor.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::all)] 2 | #[allow(unused_imports)] 3 | #[rustfmt::skip] 4 | pub mod np2_structs_generated; 5 | 6 | pub mod fb_helpers; 7 | 8 | use crate::server::client::{ComId, COMMUNICATION_ID_SIZE}; 9 | use num_traits::*; 10 | use std::cell::Cell; 11 | use std::mem; 12 | 13 | pub struct StreamExtractor { 14 | vec: Vec, 15 | i: Cell, 16 | error: Cell, 17 | } 18 | 19 | impl StreamExtractor { 20 | pub fn new(vec: Vec) -> StreamExtractor { 21 | StreamExtractor { 22 | vec, 23 | i: Cell::new(0), 24 | error: Cell::new(false), 25 | } 26 | } 27 | pub fn error(&self) -> bool { 28 | self.error.get() 29 | } 30 | 31 | pub fn get(&self) -> T { 32 | let size = mem::size_of::(); 33 | 34 | if (self.i.get() + size) > self.vec.len() { 35 | self.error.set(true); 36 | return T::zero(); 37 | } 38 | 39 | let value = unsafe { std::ptr::read_unaligned(self.vec[self.i.get()..(self.i.get() + size)].as_ptr() as *const T) }; 40 | 41 | let value = T::from_le(value); 42 | 43 | self.i.set(self.i.get() + size); 44 | value 45 | } 46 | pub fn get_string(&self, empty: bool) -> String { 47 | let mut res_s = String::new(); 48 | 49 | while self.i.get() < self.vec.len() && self.vec[self.i.get()] != 0 { 50 | res_s.push(self.vec[self.i.get()] as char); 51 | self.i.set(self.i.get() + 1); 52 | } 53 | 54 | self.i.set(self.i.get() + 1); 55 | 56 | if self.i.get() > self.vec.len() || (!empty && res_s.is_empty()) { 57 | self.error.set(true); 58 | } 59 | 60 | res_s 61 | } 62 | pub fn get_rawdata(&self) -> Vec { 63 | let mut res_vec = Vec::new(); 64 | 65 | let size = self.get::() as usize; 66 | if (size + self.i.get()) > self.vec.len() { 67 | self.error.set(true); 68 | return res_vec; 69 | } 70 | 71 | let cur_i = self.i.get(); 72 | res_vec.extend_from_slice(&self.vec[cur_i..cur_i + size]); 73 | self.i.set(cur_i + size); 74 | 75 | res_vec 76 | } 77 | pub fn get_com_id(&self) -> ComId { 78 | let mut com_id: ComId = [0; COMMUNICATION_ID_SIZE]; 79 | 80 | if self.i.get() + com_id.len() > self.vec.len() { 81 | self.error.set(true); 82 | } else { 83 | for c in &mut com_id { 84 | *c = self.get::(); 85 | } 86 | } 87 | 88 | if !com_id[0..9].iter().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()) { 89 | self.error.set(true); 90 | } 91 | 92 | if com_id[9] != b'_' { 93 | self.error.set(true); 94 | } 95 | 96 | if !com_id[10..12].iter().all(|c| c.is_ascii_digit()) { 97 | self.error.set(true); 98 | } 99 | 100 | com_id 101 | } 102 | pub fn get_flatbuffer<'a, T: flatbuffers::Follow<'a> + flatbuffers::Verifiable + 'a>(&'a self) -> Result { 103 | let size = self.get::(); 104 | if (size as usize + self.i.get()) > self.vec.len() { 105 | self.error.set(true); 106 | return Err(()); 107 | } 108 | 109 | let ret = flatbuffers::root::(&self.vec[self.i.get()..]); 110 | self.i.set(self.i.get() + size as usize); 111 | 112 | ret.map_err(|_| ()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/server/stream_extractor/fb_helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::server::stream_extractor::np2_structs_generated::*; 2 | 3 | // Flatbuffers doesn't provide deep copy functionality so we have to make our own 4 | pub fn dc_opt_data<'a>(builder: &mut flatbuffers::FlatBufferBuilder<'a>, opt_data: Option<&PresenceOptionData>) -> flatbuffers::WIPOffset> { 5 | let mut opt_data_vec: Vec = Vec::new(); 6 | let mut opt_data_vec_len: u32 = 0; 7 | if let Some(opt_data) = opt_data { 8 | for i in 0..16 { 9 | opt_data_vec.push(opt_data.data().unwrap().get(i)); 10 | } 11 | opt_data_vec_len = opt_data.len(); 12 | } else { 13 | opt_data_vec.resize(16, 0u8); 14 | } 15 | let opt_data_vec = builder.create_vector(&opt_data_vec); 16 | 17 | PresenceOptionData::create( 18 | builder, 19 | &PresenceOptionDataArgs { 20 | len: opt_data_vec_len, 21 | data: Some(opt_data_vec), 22 | }, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/server/stream_extractor/np2_structs.fbs: -------------------------------------------------------------------------------- 1 | table SignalingAddr { 2 | ip:[uint8]; 3 | port:uint16; 4 | } 5 | 6 | table MatchingSignalingInfo { 7 | npid:string; 8 | addr:SignalingAddr; 9 | } 10 | 11 | table Matching2SignalingInfo { 12 | member_id:uint16; 13 | addr:SignalingAddr; 14 | } 15 | 16 | table BinAttr { 17 | id:uint16; 18 | data:[uint8]; 19 | } 20 | 21 | table IntAttr { 22 | id:uint16; 23 | num:uint32; 24 | } 25 | 26 | table RoomMemberBinAttrInternal { 27 | updateDate:uint64; 28 | data:BinAttr; 29 | } 30 | 31 | table BinAttrInternal { 32 | updateDate:uint64; 33 | updateMemberId:uint16; 34 | data:BinAttr; 35 | } 36 | 37 | table OptParam { 38 | type:uint8; 39 | flag:uint8; 40 | hubMemberId:uint16 ; 41 | } 42 | 43 | table GroupConfig { 44 | slotNum:uint32; 45 | label:[uint8]; 46 | withPassword:bool; 47 | } 48 | 49 | table UserInfo { 50 | npId:string; 51 | onlineName:string; 52 | avatarUrl:string; 53 | } 54 | 55 | table RoomMemberDataInternal { 56 | userInfo:UserInfo; 57 | joinDate:uint64; 58 | memberId:uint16; 59 | teamId:uint8; 60 | roomGroup:RoomGroup; 61 | natType:uint8; 62 | flagAttr:uint32; 63 | roomMemberBinAttrInternal:[RoomMemberBinAttrInternal]; 64 | } 65 | 66 | table RoomGroup { 67 | groupId:uint8; 68 | withPassword:bool; 69 | label:[uint8]; 70 | slotNum:uint32; 71 | curGroupMemberNum:uint32; 72 | } 73 | 74 | table RoomDataInternal { 75 | serverId:uint16; 76 | worldId:uint32; 77 | lobbyId:uint64; 78 | roomId:uint64; 79 | passwordSlotMask:uint64; 80 | maxSlot:uint32; 81 | memberList:[RoomMemberDataInternal]; 82 | ownerId:uint16; 83 | roomGroup:[RoomGroup]; 84 | flagAttr:uint32; 85 | roomBinAttrInternal:[BinAttrInternal]; 86 | } 87 | 88 | table RoomDataExternal { 89 | serverId:uint16; 90 | worldId:uint32; 91 | publicSlotNum:uint16; 92 | privateSlotNum:uint16; 93 | lobbyId:uint64; 94 | roomId:uint64; 95 | openPublicSlotNum:uint16; 96 | maxSlot:uint16; 97 | openPrivateSlotNum:uint16; 98 | curMemberNum:uint16; 99 | passwordSlotMask:uint64; 100 | owner:UserInfo; 101 | roomGroup:[RoomGroup]; 102 | flagAttr:uint32; 103 | roomSearchableIntAttrExternal:[IntAttr]; 104 | roomSearchableBinAttrExternal:[BinAttr]; 105 | roomBinAttrExternal:[BinAttr]; 106 | } 107 | 108 | table IntSearchFilter { 109 | searchOperator:uint8; 110 | attr:IntAttr; 111 | } 112 | 113 | table BinSearchFilter { 114 | searchOperator:uint8; 115 | attr:BinAttr; 116 | } 117 | 118 | table PresenceOptionData { 119 | data:[uint8]; 120 | len:uint32; 121 | } 122 | 123 | table RoomGroupPasswordConfig { 124 | groupId:uint8; 125 | withPassword:bool; 126 | } 127 | 128 | table SearchRoomRequest { 129 | option:int32; 130 | worldId:uint32; 131 | lobbyId:uint64; 132 | rangeFilter_startIndex:uint32; 133 | rangeFilter_max:uint32; 134 | flagFilter:uint32; 135 | flagAttr:uint32; 136 | intFilter:[IntSearchFilter]; 137 | binFilter:[BinSearchFilter]; 138 | attrId:[uint16]; 139 | } 140 | 141 | table SearchRoomResponse { 142 | startIndex:uint32; 143 | total:uint32; 144 | rooms:[RoomDataExternal]; 145 | } 146 | 147 | table CreateJoinRoomRequest { 148 | worldId:uint32; 149 | lobbyId:uint64; 150 | maxSlot:uint32; 151 | flagAttr:uint32; 152 | roomBinAttrInternal:[BinAttr]; 153 | roomSearchableIntAttrExternal:[IntAttr]; 154 | roomSearchableBinAttrExternal:[BinAttr]; 155 | roomBinAttrExternal:[BinAttr]; 156 | roomPassword:[uint8]; 157 | groupConfig:[GroupConfig]; 158 | passwordSlotMask:uint64; 159 | allowedUser:[string]; 160 | blockedUser:[string]; 161 | 162 | joinRoomGroupLabel:[uint8]; 163 | roomMemberBinAttrInternal:[BinAttr]; 164 | teamId:uint8; 165 | sigOptParam:OptParam; 166 | } 167 | 168 | table JoinRoomRequest { 169 | roomId:uint64; 170 | roomPassword:[uint8]; 171 | joinRoomGroupLabel:[uint8]; 172 | roomMemberBinAttrInternal:[BinAttr]; 173 | optData:PresenceOptionData; 174 | teamId:uint8; 175 | } 176 | 177 | table JoinRoomResponse { 178 | room_data: RoomDataInternal; 179 | signaling_data: [Matching2SignalingInfo]; 180 | } 181 | 182 | table LeaveRoomRequest { 183 | roomId:uint64; 184 | optData:PresenceOptionData; 185 | } 186 | 187 | table GetRoomDataExternalListRequest { 188 | roomIds:[uint64]; 189 | attrIds:[uint16]; 190 | } 191 | 192 | table GetRoomDataExternalListResponse { 193 | rooms:[RoomDataExternal]; 194 | } 195 | 196 | table SetRoomDataExternalRequest { 197 | roomId:uint64; 198 | roomSearchableIntAttrExternal:[IntAttr]; 199 | roomSearchableBinAttrExternal:[BinAttr]; 200 | roomBinAttrExternal:[BinAttr]; 201 | } 202 | 203 | table SetRoomDataInternalRequest { 204 | roomId:uint64; 205 | flagFilter:uint32; 206 | flagAttr:uint32; 207 | roomBinAttrInternal:[BinAttr]; 208 | passwordConfig:[RoomGroupPasswordConfig]; 209 | passwordSlotMask:uint64; 210 | ownerPrivilegeRank:[uint16]; 211 | } 212 | 213 | table GetRoomMemberDataInternalRequest { 214 | roomId:uint64; 215 | memberId:uint16; 216 | attrId:[uint16]; 217 | } 218 | 219 | table SetRoomMemberDataInternalRequest { 220 | roomId:uint64; 221 | memberId:uint16; 222 | teamId:uint8; 223 | roomMemberBinAttrInternal:[BinAttr]; 224 | } 225 | 226 | table SetUserInfo { 227 | serverId:uint16; 228 | userBinAttr:[BinAttr]; 229 | } 230 | 231 | table GetRoomDataInternalRequest { 232 | roomId:uint64; 233 | attrId:[uint16]; 234 | } 235 | 236 | table RoomMemberUpdateInfo { 237 | roomMemberDataInternal:RoomMemberDataInternal; 238 | eventCause:uint8; 239 | optData:PresenceOptionData; 240 | } 241 | 242 | table NotificationUserJoinedRoom { 243 | room_id:uint64; 244 | update_info:RoomMemberUpdateInfo; 245 | signaling:SignalingAddr; 246 | } 247 | 248 | table RoomUpdateInfo { 249 | eventCause:uint8; 250 | errorCode:int32; 251 | optData:PresenceOptionData; 252 | } 253 | 254 | table RoomDataInternalUpdateInfo { 255 | newRoomDataInternal:RoomDataInternal; 256 | prevFlagAttr:uint32; 257 | prevRoomPasswordSlotMask:uint64; 258 | newRoomGroup:[uint8]; 259 | newRoomBinAttrInternal:[uint16]; 260 | } 261 | 262 | table RoomMemberDataInternalUpdateInfo { 263 | newRoomMemberDataInternal:RoomMemberDataInternal; 264 | prevFlagAttr:uint32; 265 | prevTeamId:uint8; 266 | newRoomMemberBinAttrInternal:[uint16]; 267 | } 268 | 269 | table GetPingInfoResponse { 270 | serverId:uint16; 271 | worldId:uint32; 272 | roomId:uint64; 273 | rtt:uint32; 274 | } 275 | 276 | table SendRoomMessageRequest { 277 | roomId:uint64; 278 | castType:uint8; 279 | dst:[uint16]; 280 | msg:[uint8]; 281 | option:uint8; 282 | } 283 | 284 | table RoomMessageInfo { 285 | filtered:bool; 286 | castType:uint8; 287 | dst:[uint16]; 288 | srcMember:UserInfo; 289 | msg:[uint8]; 290 | } 291 | 292 | table MessageDetails { 293 | communicationId:string; 294 | msgId:uint64; 295 | mainType:uint16; 296 | subType:uint16; 297 | msgFeatures:uint32; 298 | subject:string; 299 | body:string; 300 | data:[uint8]; 301 | } 302 | 303 | table SendMessageRequest { 304 | message:[uint8] (nested_flatbuffer: "MessageDetails"); 305 | npids:[string]; 306 | } 307 | 308 | table BoardInfo { 309 | rankLimit:uint32; 310 | updateMode:uint32; 311 | sortMode:uint32; 312 | uploadNumLimit:uint32; 313 | uploadSizeLimit:uint32; 314 | } 315 | 316 | table RecordScoreRequest { 317 | boardId:uint32; 318 | pcId:int32; 319 | score:int64; 320 | comment:string; 321 | data:[uint8]; 322 | } 323 | 324 | table GetScoreRangeRequest { 325 | boardId:uint32; 326 | startRank:uint32; 327 | numRanks:uint32; 328 | withComment:bool; 329 | withGameInfo:bool; 330 | } 331 | 332 | table ScoreNpIdPcId { 333 | npid:string; 334 | pcId:int32; 335 | } 336 | 337 | table GetScoreNpIdRequest { 338 | boardId:uint32; 339 | npids:[ScoreNpIdPcId]; 340 | withComment:bool; 341 | withGameInfo:bool; 342 | } 343 | 344 | table GetScoreFriendsRequest { 345 | boardId:uint32; 346 | include_self:bool; 347 | max:uint32; 348 | withComment:bool; 349 | withGameInfo:bool; 350 | } 351 | 352 | table ScoreRankData { 353 | npId:string; 354 | onlineName:string; 355 | pcId:int32; 356 | rank:uint32; 357 | score:int64; 358 | hasGameData:bool; 359 | recordDate:uint64; 360 | } 361 | 362 | table ScoreInfo { 363 | data:[uint8]; 364 | } 365 | 366 | table GetScoreResponse { 367 | rankArray:[ScoreRankData]; 368 | commentArray:[string]; 369 | infoArray:[ScoreInfo]; 370 | lastSortDate:uint64; 371 | totalRecord:uint32; 372 | } 373 | 374 | table RecordScoreGameDataRequest { 375 | boardId:uint32; 376 | pcId:int32; 377 | score:int64; 378 | } 379 | 380 | table GetScoreGameDataRequest { 381 | boardId:uint32; 382 | npId:string; 383 | pcId:int32; 384 | } 385 | 386 | table TusUser { 387 | vuser:bool; 388 | npid:string; 389 | } 390 | 391 | table TusVariable { 392 | ownerId:string; 393 | hasData:bool; 394 | lastChangedDate:uint64; 395 | lastChangedAuthorId:string; 396 | variable:int64; 397 | oldVariable:int64; 398 | } 399 | 400 | table TusVarResponse { 401 | vars:[TusVariable]; 402 | } 403 | 404 | table TusSetMultiSlotVariableRequest { 405 | user:TusUser; 406 | slotIdArray:[int32]; 407 | variableArray:[int64]; 408 | } 409 | 410 | table TusGetMultiSlotVariableRequest { 411 | user:TusUser; 412 | slotIdArray:[int32]; 413 | } 414 | 415 | table TusGetMultiUserVariableRequest { 416 | users:[TusUser]; 417 | slotId:int32; 418 | } 419 | 420 | table TusGetFriendsVariableRequest { 421 | slotId:int32; 422 | includeSelf:bool; 423 | sortType:int32; 424 | arrayNum:uint32; 425 | } 426 | 427 | table TusAddAndGetVariableRequest { 428 | user:TusUser; 429 | slotId:int32; 430 | inVariable:int64; 431 | isLastChangedDate:[uint64]; 432 | isLastChangedAuthorId:string; 433 | } 434 | 435 | table TusTryAndSetVariableRequest { 436 | user:TusUser; 437 | slotId:int32; 438 | opeType:int32; 439 | variable:int64; 440 | isLastChangedDate:[uint64]; 441 | isLastChangedAuthorId:string; 442 | compareValue:[int64]; 443 | } 444 | 445 | table TusDeleteMultiSlotVariableRequest { 446 | user:TusUser; 447 | slotIdArray:[int32]; 448 | } 449 | 450 | table TusSetDataRequest { 451 | user:TusUser; 452 | slotId:int32; 453 | data:[uint8]; 454 | info:[uint8]; 455 | isLastChangedDate:[uint64]; 456 | isLastChangedAuthorId:string; 457 | } 458 | 459 | table TusDataStatus { 460 | ownerId:string; 461 | hasData:bool; 462 | lastChangedDate:uint64; 463 | lastChangedAuthorId:string; 464 | info:[uint8]; 465 | } 466 | 467 | table TusData { 468 | status: TusDataStatus; 469 | data:[uint8]; 470 | } 471 | 472 | table TusDataStatusResponse { 473 | status: [TusDataStatus]; 474 | } 475 | 476 | table TusGetDataRequest { 477 | user:TusUser; 478 | slotId:int32; 479 | } 480 | 481 | table TusGetMultiSlotDataStatusRequest { 482 | user:TusUser; 483 | slotIdArray:[int32]; 484 | } 485 | 486 | table TusGetMultiUserDataStatusRequest { 487 | users:[TusUser]; 488 | slotId:int32; 489 | } 490 | 491 | table TusGetFriendsDataStatusRequest { 492 | slotId:int32; 493 | includeSelf:bool; 494 | sortType:int32; 495 | arrayNum:uint32; 496 | } 497 | 498 | table TusDeleteMultiSlotDataRequest { 499 | user:TusUser; 500 | slotIdArray:[int32]; 501 | } 502 | 503 | table SetPresenceRequest { 504 | title:string; 505 | status:string; 506 | comment:string; 507 | data:[uint8]; 508 | } 509 | 510 | table MatchingSearchCondition { 511 | attr_type:uint32; 512 | attr_id:uint32; 513 | comp_op:uint32; 514 | comp_value:uint32; 515 | } 516 | 517 | table MatchingAttr { 518 | attr_type:uint32; 519 | attr_id:uint32; 520 | num:uint32; 521 | data:[uint8]; 522 | } 523 | 524 | table CreateRoomGUIRequest { 525 | total_slots:uint32; 526 | private_slots:uint32; 527 | privilege_grant:bool; 528 | stealth:bool; 529 | game_attrs:[MatchingAttr]; 530 | } 531 | 532 | table GUIUserInfo { 533 | info:UserInfo; 534 | owner:bool; 535 | } 536 | 537 | table MatchingRoomStatus { 538 | id:[uint8]; 539 | members:[GUIUserInfo]; 540 | kick_actor:string; 541 | opt:[uint8]; 542 | } 543 | 544 | table GetRoomListGUIRequest { 545 | range_start:uint32; 546 | range_max:uint32; 547 | conds:[MatchingSearchCondition]; 548 | attrs:[MatchingAttr]; 549 | } 550 | 551 | table MatchingRoom { 552 | id:[uint8]; 553 | attr:[MatchingAttr]; 554 | } 555 | 556 | table MatchingRoomList { 557 | start:uint32; 558 | total:uint32; 559 | rooms:[MatchingRoom]; 560 | } 561 | 562 | table MatchingGuiRoomId { 563 | id:[uint8]; 564 | } 565 | 566 | table SetRoomSearchFlagGUI { 567 | roomid:[uint8]; 568 | stealth:bool; 569 | } 570 | 571 | table QuickMatchGUIRequest { 572 | conds:[MatchingSearchCondition]; 573 | available_num:uint32; 574 | } 575 | 576 | table SearchJoinRoomGUIRequest { 577 | conds:[MatchingSearchCondition]; 578 | attrs:[MatchingAttr]; 579 | } 580 | 581 | table MatchingSearchJoinRoomInfo { 582 | room:MatchingRoomStatus; 583 | attr:[MatchingAttr]; 584 | } 585 | -------------------------------------------------------------------------------- /src/server/udp_server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryInto; 3 | use std::io; 4 | use std::net::IpAddr; 5 | use std::sync::Arc; 6 | 7 | use parking_lot::RwLock; 8 | use tokio::net::UdpSocket; 9 | use tracing::{error, info, warn}; 10 | 11 | use crate::server::client::{ClientSharedInfo, TerminateWatch}; 12 | use crate::server::Server; 13 | 14 | pub struct UdpServer { 15 | host_ipv4: String, 16 | host_ipv6: String, 17 | client_infos: Arc>>, 18 | term_watch: TerminateWatch, 19 | socket: Option, 20 | } 21 | 22 | impl Server { 23 | pub async fn start_udp_server(&self, term_watch: TerminateWatch) -> io::Result<()> { 24 | // Starts udp signaling helper 25 | let (addr_ipv4, addr_ipv6); 26 | { 27 | let config = self.config.read(); 28 | addr_ipv4 = config.get_host_ipv4().clone(); 29 | addr_ipv6 = config.get_host_ipv6().clone(); 30 | } 31 | 32 | let mut udp_serv = UdpServer::new(addr_ipv4, addr_ipv6, self.client_infos.clone(), term_watch); 33 | udp_serv.start().await?; 34 | 35 | tokio::task::spawn(async move { 36 | udp_serv.server_proc().await; 37 | }); 38 | 39 | Ok(()) 40 | } 41 | } 42 | 43 | impl UdpServer { 44 | pub fn new(host_ipv4: String, host_ipv6: String, client_infos: Arc>>, term_watch: TerminateWatch) -> UdpServer { 45 | UdpServer { 46 | host_ipv4, 47 | host_ipv6, 48 | client_infos, 49 | term_watch, 50 | socket: None, 51 | } 52 | } 53 | 54 | fn try_to_bind_ipv6(str_addr: &str) -> Result { 55 | // We need to use socket2's Socket to force IPv6 only off for windows 56 | let socket = socket2::Socket::new(socket2::Domain::IPV6, socket2::Type::DGRAM, None)?; 57 | socket.set_only_v6(false)?; 58 | socket.set_nonblocking(true)?; 59 | let address: std::net::SocketAddr = str_addr 60 | .parse() 61 | .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Address is not a valid IPv6 address".to_string()))?; 62 | socket.bind(&address.into())?; 63 | Ok(UdpSocket::from_std(std::net::UdpSocket::from(socket))?) 64 | } 65 | 66 | pub async fn start(&mut self) -> io::Result<()> { 67 | // We try to bind to IPv6 and if it fails to IPv4 68 | let bind_addr_ipv6 = format!("[{}]:3657", self.host_ipv6.clone()); 69 | let bind_addr_ipv4 = format!("{}:3657", self.host_ipv4.clone()); 70 | self.socket = Some({ 71 | let sock_res = UdpServer::try_to_bind_ipv6(&bind_addr_ipv6); 72 | if let Err(e) = sock_res { 73 | warn!("Failed to bind to IPv6({}): {}", bind_addr_ipv6, e); 74 | UdpSocket::bind(&bind_addr_ipv4) 75 | .await 76 | .map_err(|e| io::Error::new(e.kind(), format!("Error binding udp server to IPv4({}): {}", &bind_addr_ipv4, e)))? 77 | } else { 78 | sock_res.unwrap() 79 | } 80 | }); 81 | 82 | info!("Udp server now waiting for packets on <{}>", self.socket.as_ref().unwrap().local_addr().unwrap()); 83 | Ok(()) 84 | } 85 | 86 | fn update_signaling_information(&self, user_id: i64, local_addr: [u8; 4], ip_addr: IpAddr, ip_port: u16) { 87 | let client_infos = self.client_infos.read(); 88 | let client_info = client_infos.get(&user_id); 89 | 90 | match client_info { 91 | None => {} 92 | Some(client_info) => { 93 | let need_update = { 94 | let client_si = client_info.signaling_info.read(); 95 | 96 | match ip_addr { 97 | IpAddr::V4(addr) => client_si.addr_p2p_ipv4.0 != addr.octets() || client_si.addr_p2p_ipv4.1 != ip_port, 98 | IpAddr::V6(addr) => { 99 | // If IPv6 is bound we can receive both IPv6 and IPv4 packets 100 | if let Some(actually_ipv4) = addr.to_ipv4() { 101 | client_si.addr_p2p_ipv4.0 != actually_ipv4.octets() || client_si.addr_p2p_ipv4.1 != ip_port 102 | } else { 103 | client_si.addr_p2p_ipv6.as_ref().is_none_or(|si_addr| si_addr.0 != addr.octets() || si_addr.1 != ip_port) 104 | } 105 | } 106 | } 107 | }; 108 | 109 | if need_update { 110 | let mut client_si = client_info.signaling_info.write(); 111 | client_si.local_addr_p2p = local_addr; 112 | 113 | match ip_addr { 114 | IpAddr::V4(addr) => { 115 | client_si.addr_p2p_ipv4 = (addr.octets(), ip_port); 116 | } 117 | IpAddr::V6(addr) => { 118 | if let Some(actually_ipv4) = addr.to_ipv4() { 119 | client_si.addr_p2p_ipv4 = (actually_ipv4.octets(), ip_port); 120 | } else { 121 | client_si.addr_p2p_ipv6 = Some((addr.octets(), ip_port)); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | async fn handle_socket_input(&self, socket: &UdpSocket, recv_result: io::Result<(usize, core::net::SocketAddr)>, recv_buf: &[u8], send_buf: &mut [u8]) { 131 | if let Err(e) = recv_result { 132 | let err_kind = e.kind(); 133 | if err_kind == io::ErrorKind::WouldBlock || err_kind == io::ErrorKind::TimedOut { 134 | return; 135 | } else { 136 | error!("Error recv_from: {}", e); 137 | return; 138 | } 139 | } 140 | 141 | // Parse packet 142 | let (amt, src) = recv_result.unwrap(); 143 | 144 | if amt != (1 + 8 + 4) || recv_buf[0] != 1 { 145 | warn!("Received invalid packet from {}", src); 146 | return; 147 | } 148 | 149 | let user_id = i64::from_le_bytes((&recv_buf[1..9]).try_into().unwrap()); 150 | let local_addr: [u8; 4] = recv_buf[9..13].try_into().unwrap(); 151 | 152 | let ip_addr = src.ip(); 153 | let ip_port = src.port(); 154 | 155 | self.update_signaling_information(user_id, local_addr, ip_addr, ip_port); 156 | 157 | send_buf[0..2].clone_from_slice(&0u16.to_le_bytes()); // VPort 0 158 | send_buf[2] = 0; // Subset 0 159 | 160 | let send_result = match ip_addr { 161 | IpAddr::V4(addr) => { 162 | send_buf[3..7].clone_from_slice(&addr.octets()); 163 | send_buf[7..9].clone_from_slice(&src.port().to_be_bytes()); 164 | socket.send_to(&send_buf[0..9], src).await 165 | } 166 | IpAddr::V6(addr) => { 167 | if let Some(actually_ipv4) = addr.to_ipv4() { 168 | send_buf[3..7].clone_from_slice(&actually_ipv4.octets()); 169 | send_buf[7..9].clone_from_slice(&src.port().to_be_bytes()); 170 | socket.send_to(&send_buf[0..9], src).await 171 | } else { 172 | send_buf[3..19].clone_from_slice(&addr.octets()); 173 | send_buf[19..21].clone_from_slice(&src.port().to_be_bytes()); 174 | socket.send_to(&send_buf[0..21], src).await 175 | } 176 | } 177 | }; 178 | 179 | if let Err(e) = send_result { 180 | error!("Error send_to: {}", e); 181 | } 182 | } 183 | 184 | async fn server_proc(&mut self) { 185 | let mut recv_buf = Box::new([0u8; 65535]); 186 | let mut send_buf = Box::new([0u8; 65535]); 187 | 188 | let socket = self.socket.take().unwrap(); 189 | 190 | 'udp_server_loop: loop { 191 | tokio::select! { 192 | recv_result = socket.recv_from(recv_buf.as_mut_slice()) => { 193 | self.handle_socket_input(&socket, recv_result, recv_buf.as_slice(), send_buf.as_mut_slice()).await; 194 | } 195 | _ = self.term_watch.recv.changed() => { 196 | break 'udp_server_loop; 197 | } 198 | } 199 | } 200 | info!("UdpServer::server_proc terminating"); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/server/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::server::Server; 2 | use std::collections::HashSet; 3 | use std::io; 4 | 5 | impl Server { 6 | pub fn get_ids_from_directory(directory: &str, file_extension: &str) -> Result, String> { 7 | let mut list_ids = HashSet::new(); 8 | 9 | for file in std::fs::read_dir(directory).map_err(|e| format!("Failed to list score_data directory: {}", e))? { 10 | if let Err(e) = file { 11 | println!("Error reading file inside {}: {}", directory, e); 12 | continue; 13 | } 14 | let file = file.unwrap(); 15 | 16 | let filename = file.file_name().into_string(); 17 | if filename.is_err() { 18 | println!("A file inside {} contains invalid unicode", directory); 19 | continue; 20 | } 21 | let filename = filename.unwrap(); 22 | 23 | let split_filename = filename.split_once('.'); 24 | if split_filename.is_none() { 25 | println!("A file inside {} doesn't contain a dot", directory); 26 | continue; 27 | } 28 | let (file_prefix, file_suffix) = split_filename.unwrap(); 29 | 30 | if file_suffix != file_extension { 31 | println!("A file in {} is not a .{}", directory, file_extension); 32 | continue; 33 | } 34 | 35 | let r = file_prefix.parse::(); 36 | if r.is_err() { 37 | println!("A file inside {} doesn't have an integer filename: {}", directory, filename); 38 | continue; 39 | } 40 | list_ids.insert(r.unwrap()); 41 | } 42 | 43 | Ok(list_ids) 44 | } 45 | 46 | pub fn create_data_directory(directory: &str, file_extension: &str) -> Result { 47 | match std::fs::create_dir(directory) { 48 | Ok(_) => {} 49 | Err(e) => match e.kind() { 50 | io::ErrorKind::AlreadyExists => {} 51 | other_error => return Err(format!("Failed to create directory(\"{}\"): {}", directory, other_error)), 52 | }, 53 | } 54 | 55 | Ok(Server::get_ids_from_directory(directory, file_extension)?.iter().max().unwrap_or(&0) + 1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ticket_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | ME4wEAYHKoZIzj0CAQYFK4EEACADOgAEsHvA8K3bl2V+nziQOejSucl9wqMdMELn 3 | 0Eebk9gcQrCr32xCGRox4x+TNC+PAzvVKcLFf9taCn0= 4 | -----END PUBLIC KEY----- 5 | --------------------------------------------------------------------------------