├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── Readme.md ├── accord-gui ├── Cargo.toml └── src │ ├── config.rs │ ├── connection_handler.rs │ ├── controllers.rs │ ├── main.rs │ ├── resources │ └── accord-logo.svg │ └── widgets.rs ├── client ├── Cargo.toml └── src │ └── main.rs ├── encryption_outline.txt ├── server ├── Cargo.toml └── src │ ├── channel.rs │ ├── commands.rs │ ├── config.rs │ ├── connection.rs │ ├── lib.rs │ ├── logging.rs │ ├── main.rs │ └── tui.rs └── src ├── connection.rs ├── lib.rs ├── packets.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accord" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["LoipesMas"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [workspace] 10 | members = [ 11 | "client", 12 | "server", 13 | "accord-gui", 14 | ] 15 | 16 | 17 | [dependencies] 18 | serde = {version = "1.0.133", features = ["derive"]} 19 | rmp-serde = "1.0.0" 20 | tokio = {version = "1.15.0", features = ["full"]} 21 | bytes = "1.1" 22 | chacha20poly1305 = "0.9.0" 23 | rand = "0.8.4" 24 | rand_chacha = "0.3.1" 25 | 26 | [profile.dev.package.num-bigint-dig] 27 | opt-level = 3 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 2, June 1991 4 | 5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 6 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Lesser General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | 125 | Thus, it is not the intent of this section to claim rights or contest 126 | your rights to work written entirely by you; rather, the intent is to 127 | exercise the right to control the distribution of derivative or 128 | collective works based on the Program. 129 | 130 | In addition, mere aggregation of another work not based on the Program 131 | with the Program (or with a work based on the Program) on a volume of 132 | a storage or distribution medium does not bring the other work under 133 | the scope of this License. 134 | 135 | 3. You may copy and distribute the Program (or a work based on it, 136 | under Section 2) in object code or executable form under the terms of 137 | Sections 1 and 2 above provided that you also do one of the following: 138 | 139 | a) Accompany it with the complete corresponding machine-readable 140 | source code, which must be distributed under the terms of Sections 141 | 1 and 2 above on a medium customarily used for software interchange; or, 142 | 143 | b) Accompany it with a written offer, valid for at least three 144 | years, to give any third party, for a charge no more than your 145 | cost of physically performing source distribution, a complete 146 | machine-readable copy of the corresponding source code, to be 147 | distributed under the terms of Sections 1 and 2 above on a medium 148 | customarily used for software interchange; or, 149 | 150 | c) Accompany it with the information you received as to the offer 151 | to distribute corresponding source code. (This alternative is 152 | allowed only for noncommercial distribution and only if you 153 | received the program in object code or executable form with such 154 | an offer, in accord with Subsection b above.) 155 | 156 | The source code for a work means the preferred form of the work for 157 | making modifications to it. For an executable work, complete source 158 | code means all the source code for all modules it contains, plus any 159 | associated interface definition files, plus the scripts used to 160 | control compilation and installation of the executable. However, as a 161 | special exception, the source code distributed need not include 162 | anything that is normally distributed (in either source or binary 163 | form) with the major components (compiler, kernel, and so on) of the 164 | operating system on which the executable runs, unless that component 165 | itself accompanies the executable. 166 | 167 | If distribution of executable or object code is made by offering 168 | access to copy from a designated place, then offering equivalent 169 | access to copy the source code from the same place counts as 170 | distribution of the source code, even though third parties are not 171 | compelled to copy the source along with the object code. 172 | 173 | 4. You may not copy, modify, sublicense, or distribute the Program 174 | except as expressly provided under this License. Any attempt 175 | otherwise to copy, modify, sublicense or distribute the Program is 176 | void, and will automatically terminate your rights under this License. 177 | However, parties who have received copies, or rights, from you under 178 | this License will not have their licenses terminated so long as such 179 | parties remain in full compliance. 180 | 181 | 5. You are not required to accept this License, since you have not 182 | signed it. However, nothing else grants you permission to modify or 183 | distribute the Program or its derivative works. These actions are 184 | prohibited by law if you do not accept this License. Therefore, by 185 | modifying or distributing the Program (or any work based on the 186 | Program), you indicate your acceptance of this License to do so, and 187 | all its terms and conditions for copying, distributing or modifying 188 | the Program or works based on it. 189 | 190 | 6. Each time you redistribute the Program (or any work based on the 191 | Program), the recipient automatically receives a license from the 192 | original licensor to copy, distribute or modify the Program subject to 193 | these terms and conditions. You may not impose any further 194 | restrictions on the recipients' exercise of the rights granted herein. 195 | You are not responsible for enforcing compliance by third parties to 196 | this License. 197 | 198 | 7. If, as a consequence of a court judgment or allegation of patent 199 | infringement or for any other reason (not limited to patent issues), 200 | conditions are imposed on you (whether by court order, agreement or 201 | otherwise) that contradict the conditions of this License, they do not 202 | excuse you from the conditions of this License. If you cannot 203 | distribute so as to satisfy simultaneously your obligations under this 204 | License and any other pertinent obligations, then as a consequence you 205 | may not distribute the Program at all. For example, if a patent 206 | license would not permit royalty-free redistribution of the Program by 207 | all those who receive copies directly or indirectly through you, then 208 | the only way you could satisfy both it and this License would be to 209 | refrain entirely from distribution of the Program. 210 | 211 | If any portion of this section is held invalid or unenforceable under 212 | any particular circumstance, the balance of the section is intended to 213 | apply and the section as a whole is intended to apply in other 214 | circumstances. 215 | 216 | It is not the purpose of this section to induce you to infringe any 217 | patents or other property right claims or to contest validity of any 218 | such claims; this section has the sole purpose of protecting the 219 | integrity of the free software distribution system, which is 220 | implemented by public license practices. Many people have made 221 | generous contributions to the wide range of software distributed 222 | through that system in reliance on consistent application of that 223 | system; it is up to the author/donor to decide if he or she is willing 224 | to distribute software through any other system and a licensee cannot 225 | impose that choice. 226 | 227 | This section is intended to make thoroughly clear what is believed to 228 | be a consequence of the rest of this License. 229 | 230 | 8. If the distribution and/or use of the Program is restricted in 231 | certain countries either by patents or by copyrighted interfaces, the 232 | original copyright holder who places the Program under this License 233 | may add an explicit geographical distribution limitation excluding 234 | those countries, so that distribution is permitted only in or among 235 | countries not thus excluded. In such case, this License incorporates 236 | the limitation as if written in the body of this License. 237 | 238 | 9. The Free Software Foundation may publish revised and/or new versions 239 | of the General Public License from time to time. Such new versions will 240 | be similar in spirit to the present version, but may differ in detail to 241 | address new problems or concerns. 242 | 243 | Each version is given a distinguishing version number. If the Program 244 | specifies a version number of this License which applies to it and "any 245 | later version", you have the option of following the terms and conditions 246 | either of that version or of any later version published by the Free 247 | Software Foundation. If the Program does not specify a version number of 248 | this License, you may choose any version ever published by the Free Software 249 | Foundation. 250 | 251 | 10. If you wish to incorporate parts of the Program into other free 252 | programs whose distribution conditions are different, write to the author 253 | to ask for permission. For software which is copyrighted by the Free 254 | Software Foundation, write to the Free Software Foundation; we sometimes 255 | make exceptions for this. Our decision will be guided by the two goals 256 | of preserving the free status of all derivatives of our free software and 257 | of promoting the sharing and reuse of software generally. 258 | 259 | NO WARRANTY 260 | 261 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 262 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 263 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 264 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 265 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 266 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 267 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 268 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 269 | REPAIR OR CORRECTION. 270 | 271 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 272 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 273 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 274 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 275 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 276 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 277 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 278 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 279 | POSSIBILITY OF SUCH DAMAGES. 280 | 281 | END OF TERMS AND CONDITIONS 282 | 283 | How to Apply These Terms to Your New Programs 284 | 285 | If you develop a new program, and you want it to be of the greatest 286 | possible use to the public, the best way to achieve this is to make it 287 | free software which everyone can redistribute and change under these terms. 288 | 289 | To do so, attach the following notices to the program. It is safest 290 | to attach them to the start of each source file to most effectively 291 | convey the exclusion of warranty; and each file should have at least 292 | the "copyright" line and a pointer to where the full notice is found. 293 | 294 | 295 | Copyright (C) 296 | 297 | This program is free software; you can redistribute it and/or modify 298 | it under the terms of the GNU General Public License as published by 299 | the Free Software Foundation; either version 2 of the License, or 300 | (at your option) any later version. 301 | 302 | This program is distributed in the hope that it will be useful, 303 | but WITHOUT ANY WARRANTY; without even the implied warranty of 304 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 305 | GNU General Public License for more details. 306 | 307 | You should have received a copy of the GNU General Public License along 308 | with this program; if not, write to the Free Software Foundation, Inc., 309 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Lesser General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | --- 4 | 5 | 6 | **Instant messaging chat system over TCP.** 7 | Written in Rust with tokio-rs. 8 | Packet design and handshake inspired partially by Minecraft. 9 | 10 | 11 | 12 | 13 | ## Features 14 | - Standalone server 15 | - GUI client (using `druid` UI toolkit) with customizations via config file 16 | - TUI client 17 | - Encryption 18 | - Sending images (via clipboard) 19 | - Server management (banning, whitelists, etc) 20 | 21 | 22 | ## GUI 23 | ### Requirements 24 | Because accord's gui client uses `druid`, it requires gtk on Linux and BSD. 25 | See [druid's Readme notes](https://github.com/linebender/druid#platform-notes) for more information. 26 | 27 | ### Configuration 28 | GUI's theme (and some saved data) can be edited in `config.toml` file. 29 | - On Unix system it's in `$XDG_CONFIG_HOME/accord-gui/config.toml` 30 | - On Windows system it's in `$LOCALAPPDATA/accord-gui/config.toml` 31 | 32 | Colors are in hexadecimal format (`#rrggbb`, `#rrggbbaa`, `#rbg` or `#rbga`). 33 | 34 | ### Images from links 35 | GUI client can automatically try to load an image from a message with a link, however this is a potential security risk (e.g. IP grabbing), so it's disabled by default. 36 | (If you're using a VPN or a proxy, then the risk should be nonexistent and in worst-case scenario it's still less risky than clicking on a random link.) 37 | 38 | ## Short-term goals 39 | - Improve GUI experience (sidebar with active users, loading up past messages and more) 40 | - Verify that the encryption is secure 41 | - Add more features 42 | 43 | ## Long-term goals 44 | - Figure out long-term goals 45 | 46 | ## The Stack 47 | - Server: 48 | - tokio-rs 49 | - postgres 50 | - GUI: 51 | - tokio-rs 52 | - druid 53 | 54 | ## Setting up accord server 55 | 56 | ### Using docker container 57 | 1. Clone docker compose repo 58 | ``` 59 | git clone https://github.com/LoipesMas/accord-docker.git 60 | cd accord-docker 61 | ``` 62 | 2. Edit `config.toml` (you probably only want to change operators) 63 | 3. `docker compose up -d` to run the server in the background 64 | 65 | ### From source 66 | 1. Compile accord-server 67 | ``` 68 | git clone https://github.com/LoipesMas/accord.git 69 | cd accord 70 | cargo b -p accord-server --release 71 | ``` 72 | 2. Set up postgresql database somewhere. 73 | Refer to postgres instructions for how to do that. 74 | 4. Launch `accord-server`. It will error something about connecting to the database, but we just need the default config. 75 | 5. Edit the config (probably located in `~/.config/accord-server/config.toml`) with correct postgres credentials. 76 | 6. Launch `accord-server` again, this time it should connect. 77 | 7. Done! 78 | Now clients can connect. 79 | 80 | ## Contributing 81 | Contributions are very welcome! Features, ideas, bug fixes, anything. 82 | -------------------------------------------------------------------------------- /accord-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accord-gui" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | druid = { git = "https://github.com/linebender/druid.git", features=["im", "image-all"] } 10 | reqwest = "0.11" 11 | flexi_logger = "0.22.3" 12 | log = "*" 13 | toml = "0.5.9" 14 | serde = "*" 15 | xdg = "2.4.1" 16 | sha2 = "0.10.1" 17 | 18 | accord = {path = ".."} 19 | tokio = {version = "1.15.0", features = ["full"]} 20 | bytes = "1.1" 21 | chrono = "0.4.19" 22 | rsa = "0.5.0" 23 | rand = "0.8.4" 24 | rand_chacha = "0.3.1" 25 | -------------------------------------------------------------------------------- /accord-gui/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Represents config file loaded into memory 6 | #[derive(Serialize, Deserialize)] 7 | pub struct Config { 8 | pub address: String, 9 | pub username: String, 10 | pub remember_login: bool, 11 | pub images_from_links: bool, 12 | pub theme: Option, 13 | } 14 | 15 | impl Default for Config { 16 | fn default() -> Self { 17 | Self { 18 | address: Default::default(), 19 | username: Default::default(), 20 | remember_login: true, 21 | images_from_links: false, 22 | theme: Some(Default::default()), 23 | } 24 | } 25 | } 26 | 27 | const CONFIG_FILE: &str = "config.toml"; 28 | 29 | fn config_path() -> PathBuf { 30 | let mut path = config_path_dir(); 31 | path.push(CONFIG_FILE); 32 | path 33 | } 34 | 35 | #[cfg(unix)] 36 | fn config_path_dir() -> PathBuf { 37 | let xdg_dirs = xdg::BaseDirectories::with_prefix("accord-gui").unwrap(); 38 | xdg_dirs.get_config_home() 39 | } 40 | 41 | #[cfg(windows)] 42 | fn config_path_dir() -> PathBuf { 43 | let local_app_data = std::env::var("LOCALAPPDATA").unwrap(); 44 | let mut path = PathBuf::from(local_app_data); 45 | path.push("accord-gui"); 46 | path 47 | } 48 | 49 | /// Saves config. 50 | /// If [`Config::theme`] is `None`, it loads it from saved config. 51 | pub fn save_config(mut config: Config) -> std::io::Result<()> { 52 | log::info!("Saving config."); 53 | let config_path = config_path(); 54 | std::fs::create_dir_all(config_path_dir()).unwrap(); 55 | 56 | if config.theme.is_none() { 57 | // This _shouldn't_ create an infinite loop, because if `load_config` doesn't load a theme, 58 | // it uses default 59 | config.theme = load_config().theme; 60 | } 61 | 62 | let toml = toml::to_string(&config).unwrap(); 63 | std::fs::write(config_path, &toml) 64 | } 65 | 66 | pub fn load_config() -> Config { 67 | log::info!("Loading config."); 68 | let config_path = config_path(); 69 | let toml = std::fs::read_to_string(config_path); 70 | let mut config = if let Ok(toml) = toml { 71 | match toml::from_str(&toml) { 72 | Ok(config) => config, 73 | Err(e) => { 74 | log::error!("Failed to parse config: {e}."); 75 | std::process::exit(-1) 76 | } 77 | } 78 | } else { 79 | log::info!("Failed to load config, using default and saving default."); 80 | save_config(Config::default()).unwrap(); 81 | Config::default() 82 | }; 83 | if config.theme.is_none() { 84 | log::warn!("No `theme` field in config! Using default."); 85 | config.theme = Some(Default::default()); 86 | } 87 | config 88 | } 89 | -------------------------------------------------------------------------------- /accord-gui/src/connection_handler.rs: -------------------------------------------------------------------------------- 1 | use chrono::TimeZone; 2 | use druid::ExtEventSink; 3 | 4 | use tokio::{ 5 | net::TcpStream, 6 | runtime, 7 | sync::{mpsc, oneshot}, 8 | time::timeout, 9 | }; 10 | 11 | use accord::{connection::*, packets::*, ENC_TOK_LEN, SECRET_LEN}; 12 | 13 | use std::sync::Arc; 14 | 15 | use rand::{rngs::OsRng, Rng, SeedableRng}; 16 | use rand_chacha::ChaCha20Rng; 17 | 18 | use rsa::{PaddingScheme, PublicKey}; 19 | 20 | use crate::Message as GMessage; 21 | 22 | use log::{error, info}; 23 | 24 | /// Commands sent to GUI 25 | #[derive(Debug)] 26 | pub enum GuiCommand { 27 | /// Add message to message list 28 | AddMessage(GMessage), 29 | /// Connected to server 30 | Connected, 31 | /// Connection ended with reason as `String` 32 | ConnectionEnded(String), 33 | /// Send image stored in bytes 34 | /// 35 | /// Used on pasting image to textbox 36 | SendImage(Arc>), 37 | /// Store image in cache, identifed by the String (usually a hash of the image) 38 | StoreImage(String, Arc>), 39 | /// Set the list of connected users 40 | UpdateUserList(Vec), 41 | } 42 | 43 | /// Commands sent to ConnectionHandler (from GUI) 44 | #[derive(Debug)] 45 | pub enum ConnectionHandlerCommand { 46 | /// Connects with `(address, username, password)` 47 | Connect(String, String, String), 48 | /// Sends this packet to server 49 | Write(accord::packets::ServerboundPacket), 50 | } 51 | 52 | /// Handles connection to the server. 53 | /// Communicates with GUI with [`GuiCommand`]s and [`ConnectionHandlerCommand`]s. 54 | pub struct ConnectionHandler; 55 | 56 | impl ConnectionHandler { 57 | /// Awaits [`ConnectionHandlerCommand::Connect`] from GUI 58 | /// and then connects to the server. 59 | pub fn main_loop( 60 | self, 61 | mut rx: mpsc::Receiver, 62 | event_sink: ExtEventSink, 63 | ) { 64 | let rt = runtime::Runtime::new().unwrap(); 65 | rt.block_on(async move { 66 | loop { 67 | match rx.recv().await { 68 | Some(ConnectionHandlerCommand::Connect(addr, username, password)) => { 69 | self.connect(&mut rx, addr, username, password, &event_sink) 70 | .await; 71 | } 72 | c => { 73 | panic!("Expected ConnectionHandlerCommand::Connect, got {:?}", c); 74 | } 75 | } 76 | } 77 | }); 78 | } 79 | 80 | /// Connects to the server, establishes encryption, logs in 81 | /// and spawns reading and writing loops. 82 | pub async fn connect( 83 | &self, 84 | gui_rx: &mut mpsc::Receiver, 85 | addr: String, 86 | username: String, 87 | password: String, 88 | event_sink: &ExtEventSink, 89 | ) { 90 | //================================== 91 | // Connect 92 | //================================== 93 | info!("Connecting to: {}", addr); 94 | let socket = if let Ok(Ok(socket)) = 95 | timeout(std::time::Duration::from_secs(5), TcpStream::connect(addr)).await 96 | { 97 | socket 98 | } else { 99 | submit_command( 100 | event_sink, 101 | GuiCommand::ConnectionEnded("Failed to connect!".to_string()), 102 | ); 103 | return; 104 | }; 105 | 106 | info!("Connected!"); 107 | let connection = Connection::::new(socket); 108 | let (mut reader, mut writer) = connection.split(); 109 | 110 | //================================== 111 | // Encryption 112 | //================================== 113 | info!("Establishing encryption..."); 114 | let secret = None; 115 | let mut nonce_generator_write = None; 116 | let mut nonce_generator_read = None; 117 | 118 | // Request encryption 119 | writer 120 | .write_packet( 121 | ServerboundPacket::EncryptionRequest, 122 | &secret, 123 | nonce_generator_write.as_mut(), 124 | ) 125 | .await 126 | .unwrap(); 127 | 128 | // Handle encryption response 129 | let pub_key: rsa::RsaPublicKey; 130 | let token = if let Ok(Some(p)) = reader 131 | .read_packet(&secret, nonce_generator_read.as_mut()) 132 | .await 133 | { 134 | match p { 135 | ClientboundPacket::EncryptionResponse(pub_key_der, token_) => { 136 | info!("Encryption step 1 successful"); 137 | pub_key = rsa::pkcs8::FromPublicKey::from_public_key_der(&pub_key_der).unwrap(); 138 | assert_eq!(ENC_TOK_LEN, token_.len()); 139 | token_ 140 | } 141 | _ => { 142 | error!("Encryption failed. Server response: {:?}", p); 143 | std::process::exit(1) 144 | } 145 | } 146 | } else { 147 | error!("Failed to establish encryption"); 148 | std::process::exit(1) 149 | }; 150 | 151 | // Generate secret 152 | let mut secret = [0u8; SECRET_LEN]; 153 | OsRng.fill(&mut secret); 154 | 155 | // Encrypt and send 156 | let padding = PaddingScheme::new_pkcs1v15_encrypt(); 157 | let enc_secret = pub_key 158 | .encrypt(&mut OsRng, padding, &secret[..]) 159 | .expect("failed to encrypt"); 160 | let padding = PaddingScheme::new_pkcs1v15_encrypt(); 161 | let enc_token = pub_key 162 | .encrypt(&mut OsRng, padding, &token[..]) 163 | .expect("failed to encrypt"); 164 | writer 165 | .write_packet( 166 | ServerboundPacket::EncryptionConfirm(enc_secret, enc_token), 167 | &None, 168 | nonce_generator_write.as_mut(), 169 | ) 170 | .await 171 | .unwrap(); 172 | 173 | // From this point onward we assume everything is encrypted 174 | let secret = Some(secret.to_vec()); 175 | let mut seed = [0u8; accord::SECRET_LEN]; 176 | seed.copy_from_slice(&secret.as_ref().unwrap()[..]); 177 | nonce_generator_write = Some(ChaCha20Rng::from_seed(seed)); 178 | nonce_generator_read = Some(ChaCha20Rng::from_seed(seed)); 179 | 180 | // Expect EncryptionAck (should be encrypted) 181 | let p = reader 182 | .read_packet(&secret, nonce_generator_read.as_mut()) 183 | .await; 184 | match p { 185 | Ok(Some(ClientboundPacket::EncryptionAck)) => { 186 | info!("Encryption handshake successful!"); 187 | } 188 | Ok(_) => { 189 | error!("Failed encryption step 2. Server response: {:?}", p); 190 | std::process::exit(1); 191 | } 192 | Err(e) => { 193 | error!("{}", e); 194 | std::process::exit(1); 195 | } 196 | } 197 | 198 | //================================== 199 | // Login 200 | //================================== 201 | info!("Logging in..."); 202 | writer 203 | .write_packet( 204 | ServerboundPacket::Login { username, password }, 205 | &secret, 206 | nonce_generator_write.as_mut(), 207 | ) 208 | .await 209 | .unwrap(); 210 | 211 | // Next packet must be login related 212 | if let Ok(Some(p)) = reader 213 | .read_packet(&secret, nonce_generator_read.as_mut()) 214 | .await 215 | { 216 | match p { 217 | ClientboundPacket::LoginAck => { 218 | info!("Login successful"); 219 | } 220 | ClientboundPacket::LoginFailed(m) => { 221 | submit_command(event_sink, GuiCommand::ConnectionEnded(m)); 222 | return; 223 | } 224 | p => { 225 | let m = format!("Login failed. Server response: {:?}", p); 226 | submit_command(event_sink, GuiCommand::ConnectionEnded(m)); 227 | return; 228 | } 229 | } 230 | } else { 231 | submit_command( 232 | event_sink, 233 | GuiCommand::ConnectionEnded("Login failed ;/".to_string()), 234 | ); 235 | return; 236 | } 237 | submit_command(event_sink, GuiCommand::Connected); 238 | 239 | // Get last 50 messages 240 | writer 241 | .write_packet( 242 | ServerboundPacket::FetchMessages(0, 50), 243 | &secret, 244 | nonce_generator_write.as_mut(), 245 | ) 246 | .await 247 | .unwrap(); 248 | 249 | // Get player list on join 250 | writer 251 | .write_packet( 252 | ServerboundPacket::Command("list".to_string()), 253 | &secret, 254 | nonce_generator_write.as_mut(), 255 | ) 256 | .await 257 | .unwrap(); 258 | 259 | // To send close command when tcpstream is closed 260 | let (tx, rx) = oneshot::channel::<()>(); 261 | 262 | tokio::join!( 263 | Self::reading_loop(reader, tx, secret.clone(), nonce_generator_read, event_sink), 264 | Self::writing_loop(writer, rx, secret.clone(), nonce_generator_write, gui_rx) 265 | ); 266 | } 267 | 268 | /// Reads incoming packets, processes them and sends commands to GUI 269 | async fn reading_loop( 270 | mut reader: ConnectionReader, 271 | close_sender: oneshot::Sender<()>, 272 | secret: Option>, 273 | mut nonce_generator: Option, 274 | event_sink: &ExtEventSink, 275 | ) { 276 | let mut user_list = vec![]; 277 | 'l: loop { 278 | match reader.read_packet(&secret, nonce_generator.as_mut()).await { 279 | Ok(Some(ClientboundPacket::Message(Message { 280 | text, 281 | sender_id, 282 | sender, 283 | time, 284 | }))) => { 285 | let time = chrono::Local.timestamp(time as i64, 0); 286 | submit_command( 287 | event_sink, 288 | GuiCommand::AddMessage(GMessage { 289 | sender_id, 290 | sender, 291 | date: time.format("(%H:%M %d-%m)").to_string(), 292 | content: text, 293 | is_image: false, 294 | }), 295 | ); 296 | } 297 | Ok(Some(ClientboundPacket::UserJoined(username))) => { 298 | user_list.push(username); 299 | submit_command(event_sink, GuiCommand::UpdateUserList(user_list.clone())); 300 | } 301 | Ok(Some(ClientboundPacket::UserLeft(username))) => { 302 | user_list 303 | .iter() 304 | .position(|u| *u == username) 305 | .map(|p| user_list.remove(p)); 306 | submit_command(event_sink, GuiCommand::UpdateUserList(user_list.clone())); 307 | } 308 | Ok(Some(ClientboundPacket::UsersOnline(usernames))) => { 309 | user_list = usernames; 310 | submit_command(event_sink, GuiCommand::UpdateUserList(user_list.clone())); 311 | } 312 | Ok(Some(ClientboundPacket::ImageMessage(im))) => { 313 | use sha2::{Digest, Sha256}; 314 | let mut hasher = Sha256::new(); 315 | hasher.update(&im.image_bytes); 316 | 317 | // Hash to string 318 | let hash = hasher.finalize()[..16] 319 | .iter() 320 | .fold("".to_string(), |accum, item| { 321 | accum + &format!("{:02x}", item) 322 | }); 323 | 324 | let time = chrono::Local.timestamp(im.time as i64, 0); 325 | submit_command( 326 | event_sink, 327 | GuiCommand::StoreImage(hash.clone(), Arc::new(im.image_bytes)), 328 | ); 329 | let m = GMessage { 330 | content: hash, 331 | sender_id: im.sender_id, 332 | sender: im.sender, 333 | date: time.format("(%H:%M %d-%m)").to_string(), 334 | is_image: true, 335 | }; 336 | submit_command(event_sink, GuiCommand::AddMessage(m)); 337 | } 338 | Ok(Some(p)) => { 339 | error!("!!Unhandled packet: {:?}", p); 340 | } 341 | _ => { 342 | submit_command( 343 | event_sink, 344 | GuiCommand::ConnectionEnded("Connection closed.".to_string()), 345 | ); 346 | close_sender.send(()).unwrap(); 347 | break 'l; 348 | } 349 | } 350 | } 351 | } 352 | 353 | /// Writes packets, coming from GUI, to server connection 354 | async fn writing_loop( 355 | mut writer: ConnectionWriter, 356 | mut close_receiver: oneshot::Receiver<()>, 357 | secret: Option>, 358 | mut nonce_generator: Option, 359 | gui_rx: &mut mpsc::Receiver, 360 | ) { 361 | loop { 362 | tokio::select!( 363 | r = gui_rx.recv() => { 364 | if let Some(c) = r { 365 | match c { 366 | ConnectionHandlerCommand::Write(p) => { 367 | writer.write_packet(p, &secret, nonce_generator.as_mut()).await.unwrap(); 368 | }, 369 | c => { 370 | panic!("Got unexpected {:?}", c); 371 | } 372 | } 373 | } 374 | }, 375 | _ = &mut close_receiver => { 376 | break; 377 | } 378 | ); 379 | } 380 | } 381 | } 382 | 383 | /// Helper function to submit a GUI command 384 | fn submit_command(event_sink: &ExtEventSink, info: GuiCommand) { 385 | event_sink 386 | .submit_command(crate::GUI_COMMAND, info, druid::Target::Global) 387 | .unwrap(); 388 | } 389 | -------------------------------------------------------------------------------- /accord-gui/src/controllers.rs: -------------------------------------------------------------------------------- 1 | use crate::{GuiCommand, Message, GUI_COMMAND}; 2 | use druid::{ 3 | im::Vector, 4 | widget::{Controller, Image}, 5 | Env, Event, EventCtx, ImageBuf, Insets, Selector, Size, Widget, WidgetExt, WidgetPod, 6 | }; 7 | use std::{ 8 | collections::HashMap, 9 | sync::{Arc, Mutex}, 10 | }; 11 | 12 | const LIST_CHANGED: Selector = Selector::new("list-changed"); 13 | 14 | pub const SCROLL: Selector = Selector::new("scroll"); 15 | 16 | /// Widget that contains a dynamically loaded image 17 | /// 18 | /// "Heavily inspired" by RemoteImage from jpochyla's psst ;] 19 | pub struct ImageMessage { 20 | pub dled_images: Arc>>, 21 | placeholder: WidgetPod>>, 22 | image: Option>>>, 23 | } 24 | 25 | impl ImageMessage { 26 | /// Creates new `ImageMessage` 27 | pub fn new( 28 | placeholder: impl Widget + 'static, 29 | dled_images: Arc>>, 30 | ) -> Self { 31 | Self { 32 | placeholder: WidgetPod::new(placeholder).boxed(), 33 | dled_images, 34 | image: None, 35 | } 36 | } 37 | 38 | /// Tries to get relevant image from cache 39 | fn try_get_image(&mut self, id: &str) -> bool { 40 | if let Some(ib) = self.dled_images.lock().unwrap().get(id) { 41 | self.image.replace( 42 | WidgetPod::new( 43 | Image::new(ib.clone()) 44 | .fill_mode(druid::widget::FillStrat::Contain) 45 | .interpolation_mode(druid::piet::InterpolationMode::Bilinear) 46 | .fix_width(400.0) 47 | .align_left() 48 | .padding(Insets::uniform_xy(50.0, 0.0)), 49 | ) 50 | .boxed(), 51 | ); 52 | return true; 53 | } 54 | false 55 | } 56 | } 57 | 58 | impl Widget for ImageMessage { 59 | fn event(&mut self, ctx: &mut druid::EventCtx, event: &Event, data: &mut Message, env: &Env) { 60 | // Update Image if our image was downloaded 61 | if let Event::Command(cmd) = event { 62 | if let Some(link_c) = cmd.get(Selector::::new("image_downloaded")) { 63 | let link = &data.content; 64 | if link == link_c && self.try_get_image(link) { 65 | ctx.children_changed(); 66 | } 67 | return; 68 | } 69 | } 70 | 71 | if let Some(image) = self.image.as_mut() { 72 | image.event(ctx, event, data, env); 73 | } else { 74 | self.placeholder.event(ctx, event, data, env); 75 | } 76 | } 77 | fn lifecycle( 78 | &mut self, 79 | ctx: &mut druid::LifeCycleCtx, 80 | event: &druid::LifeCycle, 81 | data: &Message, 82 | env: &Env, 83 | ) { 84 | // Try to load image on creation 85 | if let druid::LifeCycle::WidgetAdded = event { 86 | if self.try_get_image(&data.content) { 87 | ctx.children_changed(); 88 | } 89 | } 90 | if let Some(image) = self.image.as_mut() { 91 | image.lifecycle(ctx, event, data, env); 92 | } else { 93 | self.placeholder.lifecycle(ctx, event, data, env); 94 | } 95 | } 96 | fn update( 97 | &mut self, 98 | ctx: &mut druid::UpdateCtx, 99 | _old_data: &Message, 100 | data: &Message, 101 | env: &Env, 102 | ) { 103 | // If we ever add message editing, we need to update this! 104 | if let Some(image) = self.image.as_mut() { 105 | image.update(ctx, data, env); 106 | } else { 107 | self.placeholder.update(ctx, data, env); 108 | } 109 | } 110 | 111 | fn layout( 112 | &mut self, 113 | ctx: &mut druid::LayoutCtx, 114 | bc: &druid::BoxConstraints, 115 | data: &Message, 116 | env: &Env, 117 | ) -> Size { 118 | if let Some(image) = self.image.as_mut() { 119 | let size = image.layout(ctx, bc, data, env); 120 | image.set_origin(ctx, data, env, druid::Point::ORIGIN); 121 | size 122 | } else { 123 | let size = self.placeholder.layout(ctx, bc, data, env); 124 | self.placeholder 125 | .set_origin(ctx, data, env, druid::Point::ORIGIN); 126 | size 127 | } 128 | } 129 | 130 | fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &Message, env: &Env) { 131 | if let Some(image) = self.image.as_mut() { 132 | image.paint(ctx, data, env) 133 | } else { 134 | self.placeholder.paint(ctx, data, env) 135 | } 136 | } 137 | } 138 | 139 | /// Controller to automatically scroll when new messages are added 140 | pub struct ScrollController { 141 | prev_child_size: Option, 142 | widget_added_time: std::time::Instant, 143 | } 144 | 145 | impl ScrollController { 146 | pub fn new() -> Self { 147 | Self { 148 | prev_child_size: None, 149 | widget_added_time: std::time::Instant::now(), 150 | } 151 | } 152 | } 153 | 154 | impl Controller, druid::widget::Scroll, W>> for ScrollController 155 | where 156 | W: Widget>, 157 | { 158 | fn event( 159 | &mut self, 160 | child: &mut druid::widget::Scroll, W>, 161 | ctx: &mut EventCtx, 162 | event: &Event, 163 | data: &mut Vector, 164 | env: &Env, 165 | ) { 166 | if let Event::Command(cmd) = event { 167 | if let Some(size) = cmd.get(LIST_CHANGED) { 168 | let mut should_scroll = true; 169 | if let Some(prev_size) = self.prev_child_size.replace(*size) { 170 | should_scroll = 171 | (prev_size.height - (child.offset().y + ctx.size().height)).abs() < 50.0; 172 | } 173 | 174 | // HACK: To make sure it gets scrolled to the bottom at startup 175 | if self.widget_added_time.elapsed().as_secs() < 3 { 176 | should_scroll = true; 177 | } 178 | if should_scroll { 179 | child.scroll_by(druid::Vec2 { x: 0.0, y: 1e10 }); 180 | ctx.children_changed(); 181 | } 182 | } 183 | if let Some(mult) = cmd.get(SCROLL) { 184 | const PG_SCROLL: f64 = 200.0; 185 | child.scroll_by(druid::Vec2 { 186 | x: 0.0, 187 | y: mult * PG_SCROLL, 188 | }); 189 | ctx.children_changed(); 190 | } 191 | } 192 | 193 | child.event(ctx, event, data, env) 194 | } 195 | 196 | fn lifecycle( 197 | &mut self, 198 | child: &mut druid::widget::Scroll, W>, 199 | ctx: &mut druid::LifeCycleCtx, 200 | event: &druid::LifeCycle, 201 | data: &Vector, 202 | env: &Env, 203 | ) { 204 | if let druid::LifeCycle::WidgetAdded = event { 205 | self.widget_added_time = std::time::Instant::now(); 206 | child.scroll_by(druid::Vec2 { x: 0.0, y: 1e10 }); 207 | ctx.children_changed(); 208 | } 209 | child.lifecycle(ctx, event, data, env) 210 | } 211 | } 212 | 213 | /// Controller to send command when list's size changes 214 | pub struct ListController; 215 | 216 | impl Controller, druid::widget::List> for ListController { 217 | fn lifecycle( 218 | &mut self, 219 | child: &mut druid::widget::List, 220 | ctx: &mut druid::LifeCycleCtx, 221 | event: &druid::LifeCycle, 222 | data: &Vector, 223 | env: &Env, 224 | ) { 225 | if let druid::LifeCycle::Size(size) = event { 226 | ctx.submit_command(LIST_CHANGED.with(*size)); 227 | } 228 | child.lifecycle(ctx, event, data, env) 229 | } 230 | } 231 | 232 | /// Take focus on connect screen 233 | pub struct TakeFocusConnect; 234 | 235 | impl> Controller for TakeFocusConnect { 236 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 237 | if let Event::WindowConnected = event { 238 | ctx.request_focus(); 239 | } else if let Event::Command(command) = event { 240 | if let Some(GuiCommand::ConnectionEnded(_)) = command.get(GUI_COMMAND) { 241 | ctx.request_focus(); 242 | } 243 | } 244 | child.event(ctx, event, data, env) 245 | } 246 | } 247 | 248 | /// Take focus on main screen 249 | pub struct TakeFocusMain; 250 | 251 | impl> Controller for TakeFocusMain { 252 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 253 | if let Event::Command(command) = event { 254 | if let Some(GuiCommand::Connected) = command.get(GUI_COMMAND) { 255 | ctx.request_focus(); 256 | } 257 | } 258 | child.event(ctx, event, data, env) 259 | } 260 | } 261 | 262 | /// Controller for message TextBox. 263 | /// Handles pasting. 264 | pub struct MessageTextBoxController; 265 | 266 | impl> Controller for MessageTextBoxController { 267 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 268 | if let Event::Paste(clipboard) = event { 269 | let supported_types = &["image/png", "image/jpeg"]; 270 | let best_available_type = clipboard.preferred_format(supported_types); 271 | 272 | if let Some(format) = best_available_type { 273 | let data = clipboard 274 | .get_format(format) 275 | .expect("I promise not to unwrap in production"); 276 | ctx.submit_command(GUI_COMMAND.with(GuiCommand::SendImage(Arc::new(data)))); 277 | } 278 | } 279 | child.event(ctx, event, data, env) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /accord-gui/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use accord::packets::ServerboundPacket; 7 | use config::Config; 8 | use tokio::sync::mpsc; 9 | 10 | use druid::{ 11 | im::Vector, 12 | kurbo::Insets, 13 | widget::{Button, Checkbox, Flex, Label, List, Svg, SvgData, TextBox, ViewSwitcher}, 14 | AppLauncher, Color, Data, Env, Event, FontDescriptor, FontFamily, ImageBuf, Lens, UnitPoint, 15 | Widget, WidgetExt, WindowDesc, 16 | }; 17 | 18 | use serde::{Deserialize, Serialize}; 19 | 20 | use flexi_logger::Logger; 21 | 22 | mod controllers; 23 | use controllers::*; 24 | 25 | mod connection_handler; 26 | use connection_handler::*; 27 | 28 | mod config; 29 | 30 | mod widgets; 31 | use widgets::*; 32 | 33 | //TODO: Loading up past messages 34 | 35 | #[derive(Serialize, Deserialize)] 36 | pub struct Theme { 37 | pub background1: String, 38 | pub background2: String, 39 | pub text_color1: String, 40 | pub color1: String, 41 | pub highlight: String, 42 | pub border: f64, 43 | } 44 | 45 | impl Default for Theme { 46 | fn default() -> Self { 47 | Self { 48 | background1: "#200730".to_string(), 49 | background2: "#030009".to_string(), 50 | text_color1: "#6ef3e7".to_string(), 51 | color1: "#7521ee29".to_string(), 52 | highlight: "#77777777".to_string(), 53 | border: 4.5, 54 | } 55 | } 56 | } 57 | 58 | /// Represents a message on the server 59 | #[derive(Debug, Data, Lens, Clone, PartialEq, Eq)] 60 | pub struct Message { 61 | pub sender_id: i64, 62 | pub sender: String, 63 | pub date: String, 64 | pub content: String, 65 | pub is_image: bool, 66 | } 67 | 68 | /// Views in accord-gui application 69 | #[derive(Debug, Data, Clone, Copy, PartialEq, Eq)] 70 | enum Views { 71 | /// Starting view. Login prompt 72 | Connect, 73 | /// Main view, with messages etc. 74 | Main, 75 | } 76 | 77 | #[derive(Debug, Lens, Data, Clone)] 78 | struct AppState { 79 | current_view: Views, 80 | info_label_text: Arc, 81 | input_text1: Arc, 82 | input_text2: Arc, 83 | input_text3: Arc, 84 | remember_login: bool, 85 | input_text4: Arc, 86 | /// For sending commands to [`ConnectionHandler`] 87 | connection_handler_tx: Arc>, 88 | /// List of connected users 89 | user_list: Vector, 90 | /// Cached messages 91 | messages: Vector, 92 | images_from_links: bool, 93 | } 94 | 95 | fn init_logger() { 96 | Logger::try_with_env_or_str("warn") 97 | .unwrap() 98 | .start() 99 | .unwrap(); 100 | } 101 | 102 | // This could be not static, but oh well 103 | // TODO: Maybe this should be just set in Env? 104 | static mut THEME: Option = None; 105 | 106 | pub const GUI_COMMAND: druid::Selector = druid::Selector::new("gui_command"); 107 | 108 | fn main() { 109 | init_logger(); 110 | 111 | let config = config::load_config(); 112 | 113 | // I solemnly swear this is the only place in which we mutate THEME 114 | unsafe { 115 | THEME = Some(config.theme.expect("Theme should be loaded from config!")); 116 | } 117 | 118 | let connection_handler = ConnectionHandler {}; 119 | let (tx, rx) = mpsc::channel(16); 120 | 121 | // Cache of images 122 | let dled_images = Arc::new(Mutex::new(HashMap::new())); 123 | 124 | let main_window = WindowDesc::new(ui_builder(Arc::clone(&dled_images))).title("accord"); 125 | 126 | let data = AppState { 127 | current_view: Views::Connect, 128 | info_label_text: Arc::new("".to_string()), 129 | input_text1: Arc::new(config.address.clone()), 130 | input_text2: Arc::new(config.username.clone()), 131 | input_text3: Arc::new("".to_string()), 132 | remember_login: config.remember_login, 133 | input_text4: Arc::new("".to_string()), 134 | connection_handler_tx: Arc::new(tx), 135 | user_list: Vector::new(), 136 | messages: Vector::new(), 137 | images_from_links: config.images_from_links, 138 | }; 139 | 140 | let launcher = AppLauncher::with_window(main_window).delegate(Delegate { 141 | dled_images, 142 | rt: tokio::runtime::Runtime::new().unwrap(), 143 | }); 144 | 145 | let event_sink = launcher.get_external_handle(); 146 | 147 | std::thread::spawn(move || { 148 | connection_handler.main_loop(rx, event_sink); 149 | }); 150 | 151 | launcher.launch(data).unwrap(); 152 | } 153 | 154 | /// Connect to server using data from input textboxes 155 | fn connect_click(data: &mut AppState) { 156 | let addr = try_parse_addr(&data.input_text1); 157 | if accord::utils::verify_username(&*data.input_text2) { 158 | data.info_label_text = Arc::new("Connecting...".to_string()); 159 | data.connection_handler_tx 160 | .blocking_send(ConnectionHandlerCommand::Connect( 161 | addr, 162 | data.input_text2.to_string(), 163 | data.input_text3.to_string(), 164 | )) 165 | .unwrap(); 166 | config::save_config(config_from_appstate(data)).unwrap(); 167 | } else { 168 | log::warn!("Invalid username"); 169 | data.info_label_text = Arc::new("Invalid username".to_string()); 170 | }; 171 | } 172 | 173 | /// Send message to server 174 | fn send_message_click(data: &mut AppState) { 175 | let s = data.input_text4.clone(); 176 | if accord::utils::verify_message(&*s) { 177 | let p = if let Some(command) = s.strip_prefix('/') { 178 | ServerboundPacket::Command(command.to_string()) 179 | } else { 180 | ServerboundPacket::Message(s.to_string()) 181 | }; 182 | data.connection_handler_tx 183 | .blocking_send(ConnectionHandlerCommand::Write(p)) 184 | .unwrap(); 185 | data.input_text4 = Arc::new(String::new()); 186 | } else { 187 | data.info_label_text = Arc::new("Invalid message".to_string()); 188 | }; 189 | } 190 | 191 | // Less typing 192 | fn unwrap_from_hex(s: &str) -> Color { 193 | Color::from_hex_str(s).unwrap() 194 | } 195 | 196 | /// Builds UI of connect view 197 | fn connect_view() -> impl Widget { 198 | let font = FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(20.0); 199 | let theme = unsafe { 200 | // We only read 201 | THEME.as_ref().unwrap() 202 | }; 203 | 204 | let input_label_c = |s: &str| -> druid::widget::Align { 205 | Label::new(s) 206 | .with_font(font.clone()) 207 | .with_text_color(unwrap_from_hex(&theme.text_color1)) 208 | .padding(7.0) 209 | .center() 210 | }; 211 | let input_box_c = || -> TextBox> { 212 | TextBox::new() 213 | .with_font(font.clone()) 214 | .with_text_color(unwrap_from_hex(&theme.text_color1)) 215 | }; 216 | 217 | let info_label = Label::dynamic(|data, _env| format!("{}", data)) 218 | .with_text_color(Color::YELLOW) 219 | .with_font(font.clone()) 220 | .padding(5.0) 221 | .lens(AppState::info_label_text); 222 | let label1 = input_label_c("Address:"); 223 | let label2 = input_label_c("Username:"); 224 | let label3 = input_label_c("Password:"); 225 | let button = Button::new("Connect") 226 | .on_click(|_, data, _| connect_click(data)) 227 | .padding(5.0); 228 | let input1 = input_box_c().lens(AppState::input_text1).expand_width(); 229 | let input2 = input_box_c().lens(AppState::input_text2).expand_width(); 230 | let input3 = input_box_c() 231 | .lens(AppState::input_text3) 232 | .expand_width() 233 | .controller(TakeFocusConnect); 234 | let checkbox = Checkbox::new("Remember login").lens(AppState::remember_login); 235 | 236 | let checkbox2 = Checkbox::new("Images from links").lens(AppState::images_from_links); 237 | 238 | let accord_logo_data = match include_str!("resources/accord-logo.svg").parse::() { 239 | Ok(svg) => svg, 240 | Err(err) => { 241 | log::error!("{}", err); 242 | log::error!("Using an empty SVG instead."); 243 | SvgData::default() 244 | } 245 | }; 246 | let accord_logo = Svg::new(accord_logo_data).fill_mode(druid::widget::FillStrat::ScaleDown); 247 | 248 | Flex::column() 249 | .with_child( 250 | accord_logo 251 | .fix_width(300.0) 252 | .align_vertical(UnitPoint::BOTTOM), 253 | ) 254 | .with_child(info_label) 255 | .with_child( 256 | Flex::column() 257 | .with_child( 258 | Flex::row() 259 | .with_child(label1) 260 | .with_flex_child(input1, 1.0) 261 | .fix_width(250.0), 262 | ) 263 | .with_child( 264 | Flex::row() 265 | .with_child(label2) 266 | .with_flex_child(input2, 1.0) 267 | .fix_width(250.0), 268 | ) 269 | .with_child( 270 | Flex::row() 271 | .with_child(label3) 272 | .with_flex_child(input3, 1.0) 273 | .fix_width(250.0), 274 | ) 275 | .with_child(checkbox) 276 | .with_child(button) 277 | .with_child(checkbox2) 278 | .padding(10.0) 279 | .fix_width(350.0) 280 | .padding((-30.0, 5.0, -20.0, 5.0)) 281 | .cut_corners(0.0, 20.0, 20.0, 0.0) 282 | .with_border(unwrap_from_hex(&theme.highlight), theme.border) 283 | .with_background(unwrap_from_hex(&theme.color1)), 284 | ) 285 | .align_vertical(UnitPoint::new(0.0, 0.25)) 286 | } 287 | 288 | /// Builds a [`Widget`] showing a message 289 | fn message(dled_images: Arc>>) -> impl Widget { 290 | let theme = unsafe { 291 | // We only read 292 | THEME.as_ref().unwrap() 293 | }; 294 | 295 | let font = FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(17.0); 296 | let content_label = Label::dynamic(|d: &String, _e: &_| d.clone()) 297 | .with_font(font.clone()) 298 | .with_text_color(unwrap_from_hex(&theme.text_color1)) 299 | .with_line_break_mode(druid::widget::LineBreaking::WordWrap) 300 | .lens(Message::content); 301 | let image_from_link = ImageMessage::new(content_label, dled_images); 302 | Flex::row() 303 | .cross_axis_alignment(druid::widget::CrossAxisAlignment::Start) 304 | .with_child( 305 | Label::dynamic(|data: &Message, _env| { 306 | if data.sender.is_empty() { 307 | "".to_string() 308 | } else { 309 | format!("{} {}:", data.sender, data.date) 310 | } 311 | }) 312 | .with_text_color(unwrap_from_hex(&theme.text_color1)) 313 | .with_font(font.with_weight(druid::FontWeight::BOLD)), 314 | ) 315 | .with_default_spacer() 316 | .with_flex_child(Flex::column().with_child(image_from_link), 1.0) 317 | .padding(Insets::uniform_xy(5.0, 5.0)) 318 | .cut_corners_sym(10.0) 319 | .with_background(unwrap_from_hex(&theme.color1)) 320 | .with_border(unwrap_from_hex(&theme.highlight), theme.border) 321 | .padding(Insets::uniform_xy(0.0, 1.0)) 322 | } 323 | 324 | /// Parses address from string. 325 | /// If string contains `':'`, it assumes it's "ADDRESS:PORT", 326 | /// else it assumes it's just the address. 327 | fn try_parse_addr(s: &str) -> String { 328 | if s.contains(':') { 329 | s.to_owned() 330 | } else { 331 | format!("{}:{}", s, accord::DEFAULT_PORT) 332 | } 333 | } 334 | 335 | /// Builds UI of main view 336 | fn main_view(dled_images: Arc>>) -> impl Widget { 337 | let theme = unsafe { 338 | // We only read 339 | THEME.as_ref().unwrap() 340 | }; 341 | let user_list_font = FontDescriptor::new(FontFamily::SYSTEM_UI) 342 | .with_size(15.0) 343 | .with_weight(druid::FontWeight::BOLD); 344 | 345 | let info_label = Label::dynamic(|data, _env| format!("{}", data)) 346 | .with_text_color(Color::YELLOW) 347 | .lens(AppState::info_label_text); 348 | 349 | let accord_logo_data = match include_str!("resources/accord-logo.svg").parse::() { 350 | Ok(svg) => svg, 351 | Err(err) => { 352 | log::error!("{}", err); 353 | log::error!("Using an empty SVG instead."); 354 | SvgData::default() 355 | } 356 | }; 357 | let accord_logo = Svg::new(accord_logo_data).fill_mode(druid::widget::FillStrat::ScaleDown); 358 | 359 | let user_list_widget = Flex::column() 360 | .cross_axis_alignment(druid::widget::CrossAxisAlignment::Start) 361 | .with_flex_child( 362 | List::new(move || Label::raw().with_font(user_list_font.clone())) 363 | .lens(AppState::user_list), 364 | 1.0, 365 | ) 366 | .with_child(Label::new("").fix_width(100.0)) 367 | .expand_height() 368 | .padding((10.0, 5.0, 5.0, 5.0)) 369 | .cut_corners(10.0, 0.0, 0.0, 10.0) 370 | .with_border(unwrap_from_hex(&theme.highlight), theme.border) 371 | .with_background(unwrap_from_hex(&theme.color1)) 372 | .padding((0.0, 0.0, 5.0, 0.0)); 373 | 374 | let messages_list_widget = List::new(move || message(Arc::clone(&dled_images))) 375 | .controller(ListController) 376 | .scroll() 377 | .vertical() 378 | .controller(ScrollController::new()) 379 | .expand_height() 380 | .lens(AppState::messages); 381 | 382 | let input_text_box = TextBox::multiline() 383 | .lens(AppState::input_text4) 384 | .expand_width() 385 | .controller(TakeFocusMain) 386 | .controller(MessageTextBoxController); 387 | 388 | let send_button = 389 | Button::new("Send").on_click(|_ctx, data: &mut AppState, _env| send_message_click(data)); 390 | 391 | Flex::column() 392 | .cross_axis_alignment(druid::widget::CrossAxisAlignment::Start) 393 | .with_child(accord_logo.fix_height(80.0).center()) 394 | .with_child(info_label) 395 | .with_flex_child( 396 | Flex::row() 397 | .cross_axis_alignment(druid::widget::CrossAxisAlignment::Start) 398 | .with_child(user_list_widget) 399 | .with_flex_child(messages_list_widget, 1.0), 400 | 1.0, 401 | ) 402 | .with_default_spacer() 403 | .with_child( 404 | Flex::row() 405 | .with_flex_child(input_text_box, 1.0) 406 | .with_default_spacer() 407 | .with_child(send_button), 408 | ) 409 | .padding(20.0) 410 | } 411 | 412 | /// Builds root widget 413 | fn ui_builder(dled_images: Arc>>) -> impl Widget { 414 | let theme = unsafe { 415 | // We only read 416 | THEME.as_ref().unwrap() 417 | }; 418 | Flex::column() 419 | .with_flex_child( 420 | ViewSwitcher::new( 421 | |data: &AppState, _env| data.current_view, 422 | move |selector, _data, _env| match *selector { 423 | Views::Connect => Box::new(connect_view()), 424 | Views::Main => Box::new(main_view(Arc::clone(&dled_images))), 425 | }, 426 | ), 427 | 1.0, 428 | ) 429 | .background(druid::LinearGradient::new( 430 | UnitPoint::BOTTOM, 431 | UnitPoint::TOP, 432 | ( 433 | unwrap_from_hex(&theme.background2), 434 | unwrap_from_hex(&theme.background1), 435 | ), 436 | )) 437 | } 438 | 439 | /// Main delegate for this app 440 | struct Delegate { 441 | dled_images: Arc>>, 442 | rt: tokio::runtime::Runtime, 443 | } 444 | 445 | /// Construct [`Config`] from [`AppState`] 446 | fn config_from_appstate(data: &AppState) -> Config { 447 | let (address, username) = if data.remember_login { 448 | (data.input_text1.to_string(), data.input_text2.to_string()) 449 | } else { 450 | Default::default() 451 | }; 452 | Config { 453 | address, 454 | username, 455 | remember_login: data.remember_login, 456 | images_from_links: data.images_from_links, 457 | theme: None, 458 | } 459 | } 460 | 461 | impl druid::AppDelegate for Delegate { 462 | fn event( 463 | &mut self, 464 | ctx: &mut druid::DelegateCtx, 465 | _window_id: druid::WindowId, 466 | event: Event, 467 | data: &mut AppState, 468 | _env: &Env, 469 | ) -> Option { 470 | use druid::keyboard_types::Key; 471 | match event { 472 | Event::KeyDown(ref kevent) => match kevent.key { 473 | Key::Enter => { 474 | match data.current_view { 475 | Views::Connect => connect_click(data), 476 | Views::Main => send_message_click(data), 477 | } 478 | None 479 | } 480 | Key::PageUp => { 481 | ctx.submit_command(controllers::SCROLL.with(-1.0)); 482 | None 483 | } 484 | Key::PageDown => { 485 | ctx.submit_command(controllers::SCROLL.with(1.0)); 486 | None 487 | } 488 | _ => Some(event), 489 | }, 490 | _ => Some(event), 491 | } 492 | } 493 | 494 | fn command( 495 | &mut self, 496 | ctx: &mut druid::DelegateCtx, 497 | _target: druid::Target, 498 | cmd: &druid::Command, 499 | data: &mut AppState, 500 | _env: &Env, 501 | ) -> druid::Handled { 502 | if let Some(command) = cmd.get(GUI_COMMAND) { 503 | match command { 504 | GuiCommand::AddMessage(m) => { 505 | data.messages.push_back(m.clone()); 506 | 507 | // Try to get image from message link 508 | if data.images_from_links { 509 | let dled_images = Arc::clone(&self.dled_images); 510 | let link = m.content.clone(); 511 | let event_sink = ctx.get_external_handle(); 512 | self.rt.spawn(async move { 513 | try_get_image_from_link(&link, dled_images, event_sink).await; 514 | }); 515 | } 516 | } 517 | GuiCommand::Connected => { 518 | data.info_label_text = Arc::new(String::new()); 519 | data.current_view = Views::Main; 520 | } 521 | GuiCommand::ConnectionEnded(m) => { 522 | data.messages = Vector::new(); 523 | data.info_label_text = Arc::new(m.to_string()); 524 | data.current_view = Views::Connect; 525 | } 526 | GuiCommand::SendImage(image_bytes) => { 527 | let v = image_bytes.to_vec(); 528 | let p = ServerboundPacket::ImageMessage(v); 529 | data.connection_handler_tx 530 | .blocking_send(ConnectionHandlerCommand::Write(p)) 531 | .unwrap(); 532 | } 533 | GuiCommand::StoreImage(hash, img_bytes) => { 534 | let img_buf = ImageBuf::from_data(img_bytes).unwrap(); 535 | 536 | let mut dled_images = self.dled_images.lock().unwrap(); 537 | dled_images.insert(hash.to_string(), img_buf); 538 | ctx.submit_command( 539 | druid::Selector::::new("image_downloaded").with(hash.to_string()), 540 | ); 541 | } 542 | GuiCommand::UpdateUserList(user_list) => data.user_list = user_list.into(), 543 | }; 544 | }; 545 | druid::Handled::No 546 | } 547 | } 548 | 549 | /// Tries to download and image from the link and stores it in `dled_images` cache. 550 | /// 551 | /// Returns `true` on success. 552 | async fn try_get_image_from_link( 553 | link: &str, 554 | dled_images: Arc>>, 555 | event_sink: druid::ExtEventSink, 556 | ) -> bool { 557 | if !dled_images.lock().unwrap().contains_key(link) { 558 | let client = reqwest::ClientBuilder::new() 559 | .timeout(std::time::Duration::from_secs(10)) 560 | .build() 561 | .unwrap(); 562 | 563 | // We get just head first to see if it's an image 564 | let req = client.head(link).build(); 565 | let resp = match req { 566 | Ok(req) => client.execute(req).await, 567 | Err(_) => return false, 568 | }; 569 | match resp { 570 | Ok(resp) => { 571 | if resp.status() == reqwest::StatusCode::OK 572 | && resp.headers().get("content-type").map_or(false, |v| { 573 | v.to_str().map_or(false, |s| s.starts_with("image/")) 574 | }) 575 | && resp.headers().get("content-length").map_or(false, |v| { 576 | v.to_str().map_or(false, |s| { 577 | s.parse::().map_or(false, |l| { 578 | l < 31457280 // 30 MB 579 | }) 580 | }) 581 | }) 582 | { 583 | let req = client.get(link).build().unwrap(); 584 | 585 | let resp = match client.execute(req).await { 586 | Ok(resp) => resp, 587 | Err(_) => return false, 588 | }; 589 | 590 | let img_bytes = resp.bytes().await.unwrap(); 591 | let img_buf = ImageBuf::from_data(&img_bytes).unwrap(); 592 | 593 | let mut dled_images = dled_images.lock().unwrap(); 594 | dled_images.insert(link.to_string(), img_buf); 595 | event_sink 596 | .submit_command( 597 | druid::Selector::::new("image_downloaded"), 598 | link.to_string(), 599 | druid::Target::Auto, 600 | ) 601 | .unwrap(); 602 | } 603 | } 604 | Err(e) => { 605 | log::warn!("Error when getting image: {}", e); 606 | return false; 607 | } 608 | }; 609 | }; 610 | 611 | true 612 | } 613 | -------------------------------------------------------------------------------- /accord-gui/src/resources/accord-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 38 | 40 | 50 | 60 | 70 | 73 | 77 | 81 | 82 | 85 | 89 | 93 | 94 | 103 | 109 | 112 | 120 | 121 | 130 | 139 | 149 | 158 | 167 | 176 | 177 | 181 | 187 | 191 | accord 201 | 203 | 212 | 213 | 219 | 223 | 227 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /accord-gui/src/widgets.rs: -------------------------------------------------------------------------------- 1 | use druid::{ 2 | kurbo::{PathEl, Shape}, 3 | Color, Data, Env, KeyOrValue, PaintCtx, Point, Rect, RenderContext, Size, Widget, WidgetPod, 4 | }; 5 | 6 | /// Extension on [`Widget`] to add helper functions for cut corners 7 | pub trait WidgetExt2: Widget + Sized + 'static { 8 | fn cut_corners( 9 | self, 10 | bottom_left_offset: f64, 11 | bottom_right_offset: f64, 12 | top_right_offset: f64, 13 | top_left_offset: f64, 14 | ) -> CutCorners { 15 | CutCorners::new( 16 | bottom_left_offset, 17 | bottom_right_offset, 18 | top_right_offset, 19 | top_left_offset, 20 | self, 21 | ) 22 | } 23 | 24 | fn cut_corners_sym(self, offset: f64) -> CutCorners { 25 | CutCorners::new_sym(offset, self) 26 | } 27 | } 28 | 29 | impl + 'static> WidgetExt2 for W {} 30 | 31 | struct BorderStyle { 32 | width: KeyOrValue, 33 | color: KeyOrValue, 34 | } 35 | 36 | /// Wrapper [`Widget`] that cuts off corners 37 | pub struct CutCorners { 38 | bottom_left_offset: f64, 39 | bottom_right_offset: f64, 40 | top_right_offset: f64, 41 | top_left_offset: f64, 42 | inner: WidgetPod>>, 43 | background: Option>, 44 | border: Option, 45 | } 46 | 47 | impl CutCorners { 48 | pub fn new( 49 | bottom_left_offset: f64, 50 | bottom_right_offset: f64, 51 | top_right_offset: f64, 52 | top_left_offset: f64, 53 | inner: impl Widget + 'static, 54 | ) -> Self { 55 | Self { 56 | bottom_left_offset, 57 | bottom_right_offset, 58 | top_right_offset, 59 | top_left_offset, 60 | inner: WidgetPod::new(inner).boxed(), 61 | background: None, 62 | border: None, 63 | } 64 | } 65 | 66 | pub fn new_sym(offset: f64, inner: impl Widget + 'static) -> Self { 67 | Self { 68 | bottom_left_offset: offset, 69 | bottom_right_offset: offset, 70 | top_right_offset: offset, 71 | top_left_offset: offset, 72 | inner: WidgetPod::new(inner).boxed(), 73 | background: None, 74 | border: None, 75 | } 76 | } 77 | 78 | pub fn with_background( 79 | mut self, 80 | background: impl Into>, 81 | ) -> Self { 82 | self.background = Some(background.into()); 83 | self 84 | } 85 | 86 | pub fn with_border( 87 | mut self, 88 | color: impl Into>, 89 | width: impl Into>, 90 | ) -> Self { 91 | self.border = Some(BorderStyle { 92 | color: color.into(), 93 | width: width.into(), 94 | }); 95 | self 96 | } 97 | 98 | fn _cut_corners(&self, rect: Rect) -> CutCornersRect { 99 | CutCornersRect::new( 100 | rect, 101 | self.bottom_left_offset, 102 | self.bottom_right_offset, 103 | self.top_right_offset, 104 | self.top_left_offset, 105 | ) 106 | } 107 | } 108 | 109 | impl Widget for CutCorners { 110 | fn event(&mut self, ctx: &mut druid::EventCtx, event: &druid::Event, data: &mut T, env: &Env) { 111 | self.inner.event(ctx, event, data, env); 112 | } 113 | 114 | fn lifecycle( 115 | &mut self, 116 | ctx: &mut druid::LifeCycleCtx, 117 | event: &druid::LifeCycle, 118 | data: &T, 119 | env: &Env, 120 | ) { 121 | self.inner.lifecycle(ctx, event, data, env); 122 | } 123 | 124 | fn update(&mut self, ctx: &mut druid::UpdateCtx, _old_data: &T, data: &T, env: &Env) { 125 | self.inner.update(ctx, data, env); 126 | } 127 | 128 | fn layout( 129 | &mut self, 130 | ctx: &mut druid::LayoutCtx, 131 | bc: &druid::BoxConstraints, 132 | data: &T, 133 | env: &Env, 134 | ) -> Size { 135 | bc.debug_check("CutCorners"); 136 | let size = self.inner.layout(ctx, bc, data, env); 137 | let my_size = size; 138 | let origin = Point::ZERO; 139 | self.inner.set_origin(ctx, data, env, origin); 140 | 141 | let my_insets = self.inner.compute_parent_paint_insets(my_size); 142 | ctx.set_paint_insets(my_insets); 143 | my_size 144 | } 145 | 146 | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { 147 | let panel = self._cut_corners(ctx.size().to_rect()); 148 | if let Some(background) = self.background.as_mut() { 149 | ctx.with_save(|ctx| { 150 | ctx.clip(panel.clone()); 151 | background.paint(ctx, data, env); 152 | }); 153 | } 154 | 155 | if let Some(border) = self.border.as_ref() { 156 | let border_width = border.width.resolve(env); 157 | ctx.with_save(|ctx| { 158 | ctx.clip(panel.clone()); 159 | ctx.stroke(panel, &border.color.resolve(env), border_width); 160 | }) 161 | } 162 | self.inner.paint(ctx, data, env); 163 | } 164 | } 165 | 166 | #[derive(Clone)] 167 | struct CutCornersRect { 168 | rect: Rect, 169 | bottom_left_offset: f64, 170 | bottom_right_offset: f64, 171 | top_right_offset: f64, 172 | top_left_offset: f64, 173 | } 174 | 175 | impl CutCornersRect { 176 | fn new( 177 | rect: Rect, 178 | bottom_left_offset: f64, 179 | bottom_right_offset: f64, 180 | top_right_offset: f64, 181 | top_left_offset: f64, 182 | ) -> Self { 183 | Self { 184 | rect, 185 | bottom_left_offset, 186 | bottom_right_offset, 187 | top_right_offset, 188 | top_left_offset, 189 | } 190 | } 191 | 192 | fn _offsets(&self) -> [&f64; 4] { 193 | [ 194 | &self.top_left_offset, 195 | &self.top_right_offset, 196 | &self.bottom_right_offset, 197 | &self.bottom_left_offset, 198 | ] 199 | } 200 | } 201 | 202 | struct CutCornersRectPathIter { 203 | idx: usize, 204 | points: [Point; 8], 205 | } 206 | 207 | impl Iterator for CutCornersRectPathIter { 208 | type Item = PathEl; 209 | 210 | fn next(&mut self) -> Option { 211 | let ret = if self.idx == 0 { 212 | Some(PathEl::MoveTo(self.points[0])) 213 | } else if self.idx == 8 { 214 | Some(PathEl::ClosePath) 215 | } else if self.idx >= 9 { 216 | None 217 | } else { 218 | Some(PathEl::LineTo(self.points[self.idx])) 219 | }; 220 | self.idx += 1; 221 | ret 222 | } 223 | } 224 | 225 | impl Shape for CutCornersRect { 226 | type PathElementsIter = CutCornersRectPathIter; 227 | 228 | fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter { 229 | CutCornersRectPathIter { 230 | idx: 0, 231 | points: [ 232 | Point::new(self.top_left_offset, 0.0), 233 | Point::new(self.rect.width() - self.top_right_offset, 0.0), 234 | Point::new(self.rect.width(), self.top_right_offset), 235 | Point::new( 236 | self.rect.width(), 237 | self.rect.height() - self.bottom_right_offset, 238 | ), 239 | Point::new( 240 | self.rect.width() - self.bottom_right_offset, 241 | self.rect.height(), 242 | ), 243 | Point::new(self.bottom_left_offset, self.rect.height()), 244 | Point::new(0.0, self.rect.height() - self.bottom_left_offset), 245 | Point::new(0.0, self.top_left_offset), 246 | ], 247 | } 248 | } 249 | 250 | fn area(&self) -> f64 { 251 | let rect_area = self.rect.area(); 252 | 253 | let triangles_areas = self 254 | ._offsets() 255 | .iter() 256 | .fold(0.0, |accum, item| accum + item.powi(2) / 2.0); 257 | rect_area - triangles_areas 258 | } 259 | 260 | fn perimeter(&self, accuracy: f64) -> f64 { 261 | let mut perim = self.rect.perimeter(accuracy); 262 | for offset in self._offsets() { 263 | perim -= 2.0 * offset; 264 | perim += offset * std::f64::consts::SQRT_2; 265 | } 266 | perim 267 | } 268 | 269 | fn winding(&self, _pt: Point) -> i32 { 270 | todo!("Is it even used?") 271 | } 272 | 273 | fn bounding_box(&self) -> Rect { 274 | self.rect 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accord-client" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["LoipesMas"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | accord = {path = ".."} 11 | tokio = {version = "1.15.0", features = ["full"]} 12 | bytes = "1.1" 13 | chrono = "0.4.19" 14 | rsa = "0.5.0" 15 | rand = "0.8.4" 16 | rand_chacha = "0.3.1" 17 | -------------------------------------------------------------------------------- /client/src/main.rs: -------------------------------------------------------------------------------- 1 | use chrono::TimeZone; 2 | use std::str::FromStr; 3 | use tokio::io::AsyncReadExt; 4 | use tokio::net::TcpStream; 5 | 6 | use accord::connection::*; 7 | 8 | use accord::packets::*; 9 | 10 | use accord::{ENC_TOK_LEN, SECRET_LEN}; 11 | 12 | use std::net::SocketAddr; 13 | 14 | use tokio::sync::oneshot; 15 | 16 | use rand::{rngs::OsRng, Rng, SeedableRng}; 17 | use rand_chacha::ChaCha20Rng; 18 | 19 | use rsa::PaddingScheme; 20 | use rsa::PublicKey; 21 | 22 | // TODO: config file? 23 | 24 | #[tokio::main(flavor = "current_thread")] 25 | async fn main() { 26 | //================================== 27 | // Parse args 28 | //================================== 29 | let mut args = std::env::args(); 30 | let addr = SocketAddr::from_str(&format!( 31 | "{}:{}", 32 | args.nth(1).unwrap_or_else(|| "127.0.0.1".to_string()), 33 | accord::DEFAULT_PORT 34 | )) 35 | .unwrap(); 36 | println!("Connecting to: {}", addr); 37 | let socket = TcpStream::connect(addr).await.unwrap(); 38 | 39 | println!("Connected!"); 40 | let connection = Connection::::new(socket); 41 | let (mut reader, mut writer) = connection.split(); 42 | 43 | //================================== 44 | // Encryption 45 | //================================== 46 | println!("Establishing encryption..."); 47 | let secret = None; 48 | let mut nonce_generator_write = None; 49 | let mut nonce_generator_read = None; 50 | 51 | // Request encryption 52 | writer 53 | .write_packet( 54 | ServerboundPacket::EncryptionRequest, 55 | &secret, 56 | nonce_generator_write.as_mut(), 57 | ) 58 | .await 59 | .unwrap(); 60 | 61 | // Handle encryption response 62 | let pub_key: rsa::RsaPublicKey; 63 | let token = if let Ok(Some(p)) = reader 64 | .read_packet(&secret, nonce_generator_read.as_mut()) 65 | .await 66 | { 67 | match p { 68 | ClientboundPacket::EncryptionResponse(pub_key_der, token_) => { 69 | println!("Encryption step 1 successful"); 70 | pub_key = rsa::pkcs8::FromPublicKey::from_public_key_der(&pub_key_der).unwrap(); 71 | assert_eq!(ENC_TOK_LEN, token_.len()); 72 | token_ 73 | } 74 | _ => { 75 | println!("Encryption failed. Server response: {:?}", p); 76 | std::process::exit(1) 77 | } 78 | } 79 | } else { 80 | println!("Failed to establish encryption"); 81 | std::process::exit(1) 82 | }; 83 | 84 | // Generate secret 85 | let mut secret = [0u8; SECRET_LEN]; 86 | OsRng.fill(&mut secret); 87 | 88 | // Encrypt and send 89 | let padding = PaddingScheme::new_pkcs1v15_encrypt(); 90 | let enc_secret = pub_key 91 | .encrypt(&mut OsRng, padding, &secret[..]) 92 | .expect("failed to encrypt"); 93 | let padding = PaddingScheme::new_pkcs1v15_encrypt(); 94 | let enc_token = pub_key 95 | .encrypt(&mut OsRng, padding, &token[..]) 96 | .expect("failed to encrypt"); 97 | writer 98 | .write_packet( 99 | ServerboundPacket::EncryptionConfirm(enc_secret, enc_token), 100 | &None, 101 | nonce_generator_write.as_mut(), 102 | ) 103 | .await 104 | .unwrap(); 105 | 106 | // From this point onward we assume everything is encrypted 107 | let secret = Some(secret.to_vec()); 108 | let mut seed = [0u8; accord::SECRET_LEN]; 109 | seed.copy_from_slice(&secret.as_ref().unwrap()[..]); 110 | nonce_generator_write = Some(ChaCha20Rng::from_seed(seed)); 111 | nonce_generator_read = Some(ChaCha20Rng::from_seed(seed)); 112 | 113 | // Expect EncryptionAck (should be encrypted) 114 | let p = reader 115 | .read_packet(&secret, nonce_generator_read.as_mut()) 116 | .await; 117 | match p { 118 | Ok(Some(ClientboundPacket::EncryptionAck)) => { 119 | println!("Encryption handshake successful!"); 120 | } 121 | Ok(_) => { 122 | println!("Failed encryption step 2. Server response: {:?}", p); 123 | std::process::exit(1); 124 | } 125 | Err(e) => { 126 | println!("{}", e); 127 | std::process::exit(1); 128 | } 129 | } 130 | 131 | //================================== 132 | // Get credentials 133 | //================================== 134 | let mut stdio = tokio::io::stdin(); 135 | let username = loop { 136 | println!("Username:"); 137 | let mut buf = bytes::BytesMut::new(); 138 | match stdio.read_buf(&mut buf).await { 139 | Ok(0 | 1) => println!("Username can't be empty!"), 140 | Ok(l) => { 141 | if l > 18 { 142 | println!("Username too long. (Max 17 characters)"); 143 | continue; 144 | } 145 | let s = String::from_utf8_lossy(buf.strip_suffix(b"\n").unwrap()).to_string(); 146 | if s.chars().any(|c| !c.is_alphanumeric()) { 147 | println!("Invalid characters in username."); 148 | } else { 149 | break s; 150 | } 151 | } 152 | Err(e) => println!("Error: {:?}", e), 153 | }; 154 | }; 155 | let password = loop { 156 | println!("Password:"); 157 | let mut buf = bytes::BytesMut::new(); 158 | match stdio.read_buf(&mut buf).await { 159 | Ok(0 | 1) => println!("Password can't be empty!"), 160 | Ok(_) => { 161 | let s = String::from_utf8_lossy(buf.strip_suffix(b"\n").unwrap()).to_string(); 162 | if s.chars().any(|c| !c.is_alphanumeric()) { 163 | println!("Invalid characters in password."); 164 | } else { 165 | break s; 166 | } 167 | } 168 | Err(e) => println!("Error: {:?}", e), 169 | }; 170 | }; 171 | 172 | //================================== 173 | // Login 174 | //================================== 175 | println!("Logging in..."); 176 | writer 177 | .write_packet( 178 | ServerboundPacket::Login { username, password }, 179 | &secret, 180 | nonce_generator_write.as_mut(), 181 | ) 182 | .await 183 | .unwrap(); 184 | 185 | // Next packet must be login related 186 | if let Ok(Some(p)) = reader 187 | .read_packet(&secret, nonce_generator_read.as_mut()) 188 | .await 189 | { 190 | match p { 191 | ClientboundPacket::LoginAck => { 192 | println!("Login successful"); 193 | } 194 | ClientboundPacket::LoginFailed(m) => { 195 | println!("{}", m); 196 | std::process::exit(1); 197 | } 198 | _ => { 199 | println!("Login failed. Server response: {:?}", p); 200 | std::process::exit(1); 201 | } 202 | } 203 | } else { 204 | println!("Failed to login ;/"); 205 | std::process::exit(1); 206 | } 207 | 208 | // Get player list on join 209 | writer 210 | .write_packet( 211 | ServerboundPacket::Command("list".to_string()), 212 | &secret, 213 | nonce_generator_write.as_mut(), 214 | ) 215 | .await 216 | .unwrap(); 217 | 218 | // Get last 20 messages 219 | writer 220 | .write_packet( 221 | ServerboundPacket::FetchMessages(0, 20), 222 | &secret, 223 | nonce_generator_write.as_mut(), 224 | ) 225 | .await 226 | .unwrap(); 227 | 228 | // To send close command when tcpstream is closed 229 | let (tx, rx) = oneshot::channel::<()>(); 230 | 231 | tokio::join!( 232 | reading_loop(reader, tx, secret.clone(), nonce_generator_read), 233 | writing_loop(writer, rx, secret.clone(), nonce_generator_write) 234 | ); 235 | } 236 | 237 | async fn reading_loop( 238 | mut reader: ConnectionReader, 239 | close_sender: oneshot::Sender<()>, 240 | secret: Option>, 241 | mut nonce_generator: Option, 242 | ) { 243 | 'l: loop { 244 | match reader.read_packet(&secret, nonce_generator.as_mut()).await { 245 | Ok(Some(ClientboundPacket::Message(Message { 246 | text, 247 | sender_id: _sender_id, 248 | sender, 249 | time, 250 | }))) => { 251 | let time = chrono::Local.timestamp(time as i64, 0); 252 | println!("{} ({}): {}", sender, time.format("%H:%M %d-%m"), text); 253 | } 254 | Ok(Some(ClientboundPacket::UserJoined(username))) => { 255 | println!("{} joined the channel", username); 256 | } 257 | Ok(Some(ClientboundPacket::UserLeft(username))) => { 258 | println!("{} left the channel", username); 259 | } 260 | Ok(Some(ClientboundPacket::UsersOnline(usernames))) => { 261 | println!("-------------"); 262 | println!("Users online:"); 263 | for username in &usernames { 264 | println!(" {}", username); 265 | } 266 | println!("-------------"); 267 | } 268 | Ok(Some(ClientboundPacket::ImageMessage(im))) => { 269 | let time = chrono::Local.timestamp(im.time as i64, 0); 270 | println!( 271 | "{} sent an image. ({})", 272 | im.sender, 273 | time.format("%H:%M %d-%m") 274 | ) 275 | } 276 | Ok(Some(p)) => { 277 | println!("!!Unhandled packet: {:?}", p); 278 | } 279 | Err(e) => { 280 | println!("{}", e); 281 | close_sender.send(()).unwrap(); 282 | break 'l; 283 | } 284 | _ => { 285 | println!("Connection closed(?)\nPress Enter to exit."); 286 | close_sender.send(()).unwrap(); 287 | break 'l; 288 | } 289 | } 290 | } 291 | } 292 | 293 | async fn writing_loop( 294 | mut writer: ConnectionWriter, 295 | mut close_receiver: oneshot::Receiver<()>, 296 | secret: Option>, 297 | mut nonce_generator: Option, 298 | ) { 299 | let mut stdio = tokio::io::stdin(); 300 | let mut buf = bytes::BytesMut::new(); 301 | loop { 302 | tokio::select!( 303 | r = stdio.read_buf(&mut buf) => { 304 | if r.is_ok() { 305 | let s = String::from_utf8_lossy(&buf).to_string(); 306 | 307 | if let Some(s) = s.strip_suffix('\n') { 308 | buf.clear(); 309 | // Clear input line 310 | print!("\r\u{1b}[A"); 311 | if s.chars().any(|c| c.is_control()) { 312 | println!("Invalid message text!"); 313 | continue; 314 | } 315 | 316 | if s.is_empty() { 317 | print!("\u{1b}[A\u{1b}[A"); 318 | continue; 319 | } 320 | 321 | let p = if let Some(command) = s.strip_prefix('/') { 322 | ServerboundPacket::Command(command.to_string()) 323 | } else { 324 | ServerboundPacket::Message(s.to_string()) 325 | }; 326 | writer.write_packet(p, &secret, nonce_generator.as_mut()).await.unwrap(); 327 | } 328 | } 329 | } 330 | _ = &mut close_receiver => { 331 | break; 332 | } 333 | ); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /encryption_outline.txt: -------------------------------------------------------------------------------- 1 | Server generates public+private key on startup (rsa) 2 | 3 | Client request encryption 4 | Server responds with public key and token 5 | Client generates secret 6 | Client sends encrypted secret and token 7 | Server decrypts secret and token and verifies token 8 | If it checks out, everything from this point on is encrypted 9 | Nonce generators are initialized on both ends with the secret as seed 10 | Server sends encrypted acknowledgement 11 | Login commences 12 | 13 | If the client doesn't send encryption request then encryption isn't enabled 14 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accord-server" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["LoipesMas"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | accord = {path = ".."} 11 | anyhow = "1.0" 12 | clap = { version = "3.1.15", features = ["derive"]} 13 | crossterm = {version="0.23", features = ["event-stream"]} 14 | tui = "0.18" 15 | flexi_logger = "0.22.3" 16 | log = "*" 17 | toml = "0.5.9" 18 | serde = "*" 19 | xdg = "2.4.1" 20 | tokio = {version = "1.15.0", features = ["full"]} 21 | tokio-postgres = "0.7.5" 22 | futures = "0.3.21" 23 | sha2 = "0.10.1" 24 | rand = "0.8.4" 25 | rsa = "0.5.0" 26 | rand_chacha = "0.3.1" 27 | base64 = "0.13.0" 28 | -------------------------------------------------------------------------------- /server/src/channel.rs: -------------------------------------------------------------------------------- 1 | use accord::packets::*; 2 | use accord::utils::verify_username; 3 | use accord::{ENC_TOK_LEN, RSA_BITS}; 4 | 5 | use std::collections::HashMap; 6 | use tokio::sync::mpsc::{Receiver, Sender}; 7 | 8 | use tokio_postgres::{Client as DBClient, NoTls}; 9 | 10 | use crate::config::{save_config, Config}; 11 | 12 | use super::commands::*; 13 | 14 | use rand::rngs::OsRng; 15 | use rand::Rng; 16 | use rand::RngCore; 17 | use rand::SeedableRng; 18 | use rand_chacha::ChaCha20Rng; 19 | use rsa::{pkcs8::ToPublicKey, PaddingScheme, RsaPrivateKey, RsaPublicKey}; 20 | 21 | use anyhow::{Context, Result}; 22 | 23 | /// Channel represents the server that the users connect to and send messages to. 24 | pub struct AccordChannel { 25 | receiver: Receiver, 26 | txs: HashMap>, 27 | connected_users: HashMap, 28 | salt_generator: ChaCha20Rng, 29 | db_client: DBClient, 30 | priv_key: RsaPrivateKey, 31 | pub_key: RsaPublicKey, 32 | config: Config, 33 | } 34 | 35 | impl AccordChannel { 36 | /// Generates private key, connects to the databse, sets up the database if needed, 37 | /// and spawns the channel loop. 38 | pub async fn spawn(receiver: Receiver, config: Config) -> Result<()> { 39 | // Setup 40 | let txs: HashMap> = HashMap::new(); 41 | let connected_users: HashMap = HashMap::new(); 42 | let mut rng = OsRng; 43 | let priv_key = 44 | RsaPrivateKey::new(&mut rng, RSA_BITS).with_context(|| "Failed to generate a key.")?; 45 | let pub_key = RsaPublicKey::from(&priv_key); 46 | 47 | let database_config = format!( 48 | "host='{}' port='{}' user='{}' password='{}' dbname='{}'", 49 | config.db_host, config.db_port, config.db_user, config.db_pass, config.db_dbname, 50 | ); 51 | 52 | let (db_client, db_connection) = tokio_postgres::connect(&database_config, NoTls) 53 | .await 54 | .with_context(|| format!("Postgres connection ({}) error.", database_config))?; 55 | 56 | tokio::spawn(async move { 57 | if let Err(e) = db_connection.await { 58 | log::error!("Database connection error: {}.", e); 59 | }; 60 | }); 61 | 62 | // Prepare Database, panic if it fails and gives us the reason. Without this, the server will be useless anyway, so it is ok to panic here. 63 | // Friendly reminder @LoipesMas never silence errors, otherwise debugging will be a pain. 64 | log::info!("Preparing database..."); 65 | 66 | // Create accord schema if not exists, handle errors 67 | let _ = db_client 68 | .execute("CREATE SCHEMA IF NOT EXISTS accord", &[]) 69 | .await 70 | .with_context(|| "Failed to create schema 'accord'.")?; 71 | 72 | // Create account table if not exists 73 | let _ = db_client 74 | .execute( 75 | "CREATE TABLE IF NOT EXISTS accord.accounts ( 76 | user_id serial8 NOT null PRIMARY KEY, 77 | username varchar(255) NOT NULL UNIQUE, 78 | password varchar(44) NOT NULL, 79 | salt varchar(88) NOT NULL, 80 | banned bool NOT NULL DEFAULT false, 81 | whitelisted bool NOT NULL DEFAULT false 82 | );", 83 | &[], 84 | ) 85 | .await 86 | .with_context(|| "Failed to create table 'accounts'.")?; 87 | 88 | // Create images table if not exists 89 | let _ = db_client 90 | .execute( 91 | "CREATE TABLE IF NOT EXISTS accord.images ( image_hash INT PRIMARY KEY, data BYTEA NOT NULL);", 92 | &[], 93 | ) 94 | .await 95 | .with_context(|| "Failed to create table 'images'.")?; 96 | 97 | // Create messages table if not exists 98 | let _ = db_client 99 | .execute( 100 | "CREATE TABLE IF NOT EXISTS accord.messages ( 101 | sender_id int8 NOT NULL, sender varchar(255) NOT NULL DEFAULT '*deleted_user*', content varchar(1023), send_time bigint NOT NULL, image_hash INT DEFAULT NULL, 102 | CONSTRAINT fk_image_hash FOREIGN KEY(image_hash) REFERENCES accord.images(image_hash) ON DELETE SET DEFAULT ON UPDATE CASCADE, 103 | CONSTRAINT fk_username FOREIGN KEY(sender) REFERENCES accord.accounts(username) ON DELETE SET DEFAULT ON UPDATE CASCADE 104 | );", 105 | &[], 106 | ).await 107 | .with_context(|| "Failed to create table 'messages'.")?; 108 | 109 | log::info!("DONE: Preparing database."); 110 | 111 | let s = Self { 112 | receiver, 113 | txs, 114 | connected_users, 115 | salt_generator: ChaCha20Rng::from_entropy(), 116 | db_client, 117 | priv_key, 118 | pub_key, 119 | config, 120 | }; 121 | // Launch channel loop 122 | tokio::spawn(s.channel_loop()); 123 | Ok(()) 124 | } 125 | 126 | /// Waits for [`ChannelCommand`]s on [`AccordChannel::receiver`] and handles them. 127 | async fn channel_loop(mut self) { 128 | loop { 129 | use ChannelCommand::*; 130 | let p = match self.receiver.recv().await { 131 | Some(p) => p, 132 | None => break, 133 | }; 134 | match p { 135 | Close => { 136 | break; 137 | } 138 | Write(p) => { 139 | match p { 140 | ClientboundPacket::ImageMessage(ref im) => { 141 | log::info!("Image from {}.", im.sender); 142 | } 143 | _ => log::info!("Message: {:?}.", &p), 144 | } 145 | match &p { 146 | ClientboundPacket::Message(message) => { 147 | self.insert_message(message).await; 148 | } 149 | ClientboundPacket::ImageMessage(im) => { 150 | self.insert_image_message(im).await; 151 | } 152 | _ => (), 153 | } 154 | for (addr, tx_) in &self.txs { 155 | // Only send to logged in users 156 | // Maybe there is a prettier way to achieve that? Seems suboptimal 157 | if self.connected_users.contains_key(addr) { 158 | tx_.send(ConnectionCommand::Write(p.clone())).await.ok(); 159 | } 160 | } 161 | } 162 | EncryptionRequest(tx, otx) => { 163 | let mut token = [0u8; ENC_TOK_LEN]; 164 | OsRng.fill(&mut token); 165 | tx.send(ConnectionCommand::Write( 166 | ClientboundPacket::EncryptionResponse( 167 | self.pub_key.to_public_key_der().unwrap().as_ref().to_vec(), 168 | token.to_vec(), 169 | ), 170 | )) 171 | .await 172 | .unwrap(); 173 | otx.send(token.to_vec()).unwrap(); 174 | } 175 | EncryptionConfirm(tx, otx, enc_s, enc_t, exp_t) => { 176 | let t = { 177 | let padding = PaddingScheme::new_pkcs1v15_encrypt(); 178 | self.priv_key 179 | .decrypt(padding, &enc_t) 180 | .expect("Failed to decrypt.") 181 | }; 182 | if t != exp_t { 183 | log::error!("Encryption handshake failed!"); 184 | tx.send(ConnectionCommand::Close).await.ok(); 185 | otx.send(Err(())).unwrap(); 186 | } else { 187 | let s = { 188 | let padding = PaddingScheme::new_pkcs1v15_encrypt(); 189 | self.priv_key 190 | .decrypt(padding, &enc_s) 191 | .expect("Failed to decrypt.") 192 | }; 193 | otx.send(Ok(s.clone())).unwrap(); 194 | tx.send(ConnectionCommand::SetSecret(Some(s.clone()))) 195 | .await 196 | .unwrap(); 197 | tx.send(ConnectionCommand::Write(ClientboundPacket::EncryptionAck)) 198 | .await 199 | .unwrap(); 200 | } 201 | } 202 | LoginAttempt { .. } => { 203 | self.handle_login(p).await; 204 | } 205 | UserJoined(username) => { 206 | for tx_ in self.txs.values() { 207 | tx_.send(ConnectionCommand::Write(ClientboundPacket::UserJoined( 208 | username.clone(), 209 | ))) 210 | .await 211 | .ok(); 212 | } 213 | } 214 | UserLeft(addr) => { 215 | self.txs.remove(&addr); 216 | if let Some(username) = self.connected_users.remove(&addr) { 217 | log::info!("Connection ended from: {} ({}).", username, addr); 218 | for tx_ in self.txs.values() { 219 | tx_.send(ConnectionCommand::Write(ClientboundPacket::UserLeft( 220 | username.clone(), 221 | ))) 222 | .await 223 | .ok(); 224 | } 225 | } else { 226 | log::info!("Connection ended from: {}", addr); 227 | } 228 | } 229 | UsersQueryTUI(otx) => { 230 | if otx 231 | .send(self.connected_users.values().cloned().collect()) 232 | .is_err() 233 | { 234 | log::error!("Error while getting user list in TUI"); 235 | } 236 | } 237 | UsersQuery(addr) => { 238 | let tx = self 239 | .txs 240 | .get(&addr) 241 | .unwrap_or_else(|| panic!("Wrong reply addr: {}.", addr)); 242 | tx.send(ConnectionCommand::Write(ClientboundPacket::UsersOnline( 243 | self.connected_users.values().cloned().collect(), 244 | ))) 245 | .await 246 | .unwrap(); 247 | } 248 | FetchMessages(o, n, otx) => { 249 | let n = n.min(64); // Clamp so we don't query and send too much 250 | let messages_rows = self.fetch_messages(o, n).await; 251 | let messages = messages_rows.iter().map(|r| async { 252 | if let Some(hash) = r.get::<_, Option>("image_hash") { 253 | let image_bytes = self.fetch_image(hash).await; 254 | ClientboundPacket::ImageMessage(accord::packets::ImageMessage { 255 | sender_id: r.get("sender_id"), 256 | sender: r.get("sender"), 257 | image_bytes, 258 | time: r.get::<_, i64>("send_time") as u64, 259 | }) 260 | } else { 261 | ClientboundPacket::Message(accord::packets::Message { 262 | sender_id: r.get("sender_id"), 263 | sender: r.get("sender"), 264 | text: r.get("content"), 265 | time: r.get::<_, i64>("send_time") as u64, 266 | }) 267 | } 268 | }); 269 | let messages = futures::future::join_all(messages).await; 270 | otx.send(messages).unwrap(); 271 | } 272 | CheckPermissions(username, otx) => { 273 | let perms = self.get_user_perms(&username).await; 274 | otx.send(perms).unwrap(); 275 | } 276 | KickUser(username) => { 277 | self.kick_user(&username).await; 278 | } 279 | BanUser(username, switch) => { 280 | if switch { 281 | self.kick_user(&username).await; 282 | } 283 | self.ban_user(&username, switch).await; 284 | } 285 | WhitelistUser(username, switch) => { 286 | self.whitelist_user(&username, switch).await; 287 | } 288 | SetWhitelist(state) => { 289 | self.config.whitelist_on = state; 290 | log::info!("Set whitelist: {}", state); 291 | save_config(&self.config).unwrap(); 292 | } 293 | SetAllowNewAccounts(state) => { 294 | self.config.allow_new_accounts = state; 295 | log::info!("Set allow_new_accounts: {}", state); 296 | save_config(&self.config).unwrap(); 297 | } 298 | }; 299 | } 300 | } 301 | 302 | /// Disconnects user from the channel. 303 | async fn kick_user(&mut self, username: &str) { 304 | log::info!("Kicked user {}", username); 305 | for (addr, un) in self.connected_users.iter() { 306 | if un == username { 307 | self.txs 308 | .get(addr) 309 | .unwrap() 310 | .send(ConnectionCommand::Close) 311 | .await 312 | .unwrap(); 313 | } 314 | } 315 | } 316 | 317 | /// Handles pretty much entire login process. 318 | async fn handle_login(&mut self, p: ChannelCommand) { 319 | if let ChannelCommand::LoginAttempt { 320 | username, 321 | password, 322 | addr, 323 | otx, 324 | tx, 325 | } = p 326 | { 327 | let perms = self.get_user_perms(&username).await; 328 | let res = if !verify_username(&username) { 329 | Err("Invalid username!".to_string()) 330 | } else if perms.banned { 331 | Err("User banned.".to_string()) 332 | } else if self.config.whitelist_on && !perms.whitelisted { 333 | Err("User not on whitelist.".to_string()) 334 | } else if let Some(row) = self.get_user(&username).await { 335 | // Account exists 336 | let salt_s: String = row.get("salt"); 337 | let salt = base64::decode(salt_s).unwrap(); 338 | let pass_hash = hash_password(password, salt); 339 | let acc_pass_s: String = row.get("password"); 340 | let acc_pass = base64::decode(acc_pass_s).unwrap(); 341 | if pass_hash == acc_pass.as_slice() { 342 | if self.connected_users.values().any(|u| u == &username) { 343 | Err("Already logged in.".to_string()) 344 | } else { 345 | let user_id: i64 = row.get("user_id"); 346 | let username: String = row.get("username"); 347 | log::info!( 348 | "Logged in: {} (user_id: {}) from {}.", 349 | username, 350 | user_id, 351 | addr 352 | ); 353 | Ok(format!("{}|{}", user_id, username)) 354 | } 355 | } else { 356 | Err("Incorrect password.".to_string()) 357 | } 358 | } else { 359 | // New account 360 | if self.config.allow_new_accounts { 361 | let mut salt = [0; 64]; 362 | self.salt_generator.fill_bytes(&mut salt); 363 | let pass_hash = hash_password(password, salt); 364 | 365 | if let Some(row) = self.insert_user(&username, &pass_hash, &salt).await { 366 | log::info!("New account: {}.", username); 367 | let user_id: i64 = row.get("user_id"); 368 | let username: String = row.get("username"); 369 | 370 | Ok(format!("{}|{}", user_id, username)) 371 | } else { 372 | Err("Failed to create account.".to_string()) 373 | } 374 | } else { 375 | Err("Account creation disabled.".to_string()) 376 | } 377 | }; 378 | if let Err(ref e) = res { 379 | log::info!("Failed to log in: {}, reason: {}", username, e); 380 | } else { 381 | self.connected_users.insert(addr, username); 382 | self.txs.insert(addr, tx); 383 | } 384 | otx.send(res).unwrap(); 385 | } else { 386 | panic!("Provided not login packet to handle_login.") 387 | } 388 | } 389 | 390 | /// Inserts new user into the database. 391 | async fn insert_user( 392 | &self, 393 | username: &str, 394 | pass_hash: &[u8], 395 | salt: &[u8], 396 | ) -> Option { 397 | self.db_client 398 | .query_opt( 399 | "INSERT INTO accord.accounts(username, password, salt) VALUES ($1, $2, $3) RETURNING *", 400 | &[&username, &base64::encode(pass_hash), &base64::encode(salt)], 401 | ) 402 | .await 403 | .unwrap() 404 | } 405 | 406 | /// Gets user from the database by the username. 407 | async fn get_user(&self, username: &str) -> Option { 408 | self.db_client 409 | .query_opt( 410 | "SELECT user_id, username, password, salt FROM accord.accounts WHERE username=$1", 411 | &[&username], 412 | ) 413 | .await 414 | .unwrap() 415 | } 416 | 417 | /// Inserts new text message into the database. 418 | async fn insert_message(&self, message: &accord::packets::Message) { 419 | self.db_client 420 | .execute( 421 | "INSERT INTO accord.messages(sender_id, sender, content, send_time) VALUES ($1, $2, $3, $4)", 422 | &[&message.sender_id, &message.sender, &message.text, &(message.time as i64)], 423 | ) 424 | .await 425 | .unwrap(); 426 | } 427 | 428 | /// Inserts new image message into the database. 429 | async fn insert_image_message(&self, message: &accord::packets::ImageMessage) { 430 | use sha2::{Digest, Sha256}; 431 | use tokio_postgres::types::private::read_be_i32; 432 | 433 | // Get hash of the image as i32 434 | let mut hasher = Sha256::new(); 435 | hasher.update(&message.image_bytes); 436 | let hash = read_be_i32(&mut &hasher.finalize()[..4]).unwrap(); 437 | 438 | // Insert image into db 439 | self.db_client 440 | .execute( 441 | "INSERT INTO accord.images VALUES ($1, $2) ON CONFLICT DO NOTHING", 442 | &[&hash, &message.image_bytes], 443 | ) 444 | .await 445 | .unwrap(); 446 | 447 | // Inser message with hash as a foreign key 448 | self.db_client 449 | .execute( 450 | "INSERT INTO accord.messages (sender_id, sender, content, send_time, image_hash) VALUES ($1, $2, '', $3, $4)", 451 | &[&message.sender_id, &message.sender, &(message.time as i64), &hash], 452 | ) 453 | .await 454 | .unwrap(); 455 | } 456 | 457 | /// Gets a range of messages from the database. 458 | async fn fetch_messages(&self, offset: i64, count: i64) -> Vec { 459 | self.db_client 460 | .query( 461 | "SELECT sender_id, sender, content, send_time, image_hash FROM accord.messages ORDER BY send_time DESC OFFSET $1 ROWS FETCH FIRST $2 ROW ONLY;", 462 | &[&offset, &count], 463 | ) 464 | .await 465 | .unwrap() 466 | } 467 | 468 | /// Given hash, fetch image bytes from db 469 | async fn fetch_image(&self, hash: i32) -> Vec { 470 | let r = self 471 | .db_client 472 | .query( 473 | "SELECT data FROM accord.images WHERE image_hash=$1", 474 | &[&hash], 475 | ) 476 | .await 477 | .unwrap(); 478 | r.get(0).unwrap().get::<_, Vec>("data") 479 | } 480 | 481 | /// Returns permissions of a user 482 | /// Default if user not in accounts 483 | async fn get_user_perms(&self, username: &str) -> UserPermissions { 484 | let r = self 485 | .db_client 486 | .query( 487 | "SELECT banned, whitelisted FROM accord.accounts WHERE username=$1", 488 | &[&username], 489 | ) 490 | .await 491 | .unwrap(); 492 | 493 | r.get(0) 494 | .map(|r| UserPermissions { 495 | operator: self.config.operators.contains(username), 496 | banned: r.get::<_, bool>("banned"), 497 | whitelisted: r.get::<_, bool>("whitelisted"), 498 | }) 499 | .unwrap_or_default() 500 | } 501 | 502 | /// Bans (or unbans) a user 503 | async fn ban_user(&self, username: &str, switch: bool) { 504 | if switch { 505 | log::info!("Banned user {}", username); 506 | } else { 507 | log::info!("Unbanned user {}", username); 508 | } 509 | self.db_client 510 | .execute( 511 | "UPDATE accord.accounts SET banned = $1 WHERE username = $2", 512 | &[&switch, &username], 513 | ) 514 | .await 515 | .unwrap(); 516 | } 517 | 518 | /// Whitelists (or unwhitelists) a user 519 | async fn whitelist_user(&self, username: &str, switch: bool) { 520 | let n = self 521 | .db_client 522 | .execute( 523 | "UPDATE accord.accounts SET whitelisted = $1 WHERE username = $2", 524 | &[&switch, &username], 525 | ) 526 | .await 527 | .unwrap(); 528 | if n == 0 { 529 | log::warn!("User {} not in database!", &username); 530 | } else if switch { 531 | log::info!("Whitelisted user {}", username); 532 | } else { 533 | log::info!("Unwhitelisted user {}", username); 534 | } 535 | } 536 | } 537 | 538 | #[inline] 539 | fn hash_password, S: AsRef<[u8]>>(pass: P, salt: S) -> [u8; 32] { 540 | use sha2::{Digest, Sha256}; 541 | let mut hasher = Sha256::new(); 542 | hasher.update(pass); 543 | hasher.update(salt); 544 | let mut ret = [0; 32]; 545 | ret.copy_from_slice(&hasher.finalize()[..32]); 546 | ret 547 | } 548 | -------------------------------------------------------------------------------- /server/src/commands.rs: -------------------------------------------------------------------------------- 1 | //! Commands used internally for communication between connections and channel loop 2 | use accord::packets::*; 3 | use std::net::SocketAddr; 4 | 5 | use tokio::sync::{mpsc::Sender, oneshot::Sender as OSender}; 6 | 7 | /// Fetched permissions of the user. 8 | #[derive(Debug, Default)] 9 | pub struct UserPermissions { 10 | pub operator: bool, 11 | pub whitelisted: bool, 12 | pub banned: bool, 13 | } 14 | 15 | /// Commands sent to client-server connection handlers. 16 | #[derive(Debug)] 17 | pub enum ConnectionCommand { 18 | Write(ClientboundPacket), 19 | SetSecret(Option>), 20 | Close, 21 | } 22 | 23 | /// Commands sent to [`AccordChannel`](`crate::channel::AccordChannel`) 24 | #[derive(Debug)] 25 | pub enum ChannelCommand { 26 | Close, 27 | Write(ClientboundPacket), 28 | EncryptionRequest(Sender, OSender>), 29 | // Maybe this should be a struct? 30 | EncryptionConfirm( 31 | Sender, 32 | OSender, ()>>, 33 | Vec, 34 | Vec, 35 | Vec, 36 | ), // encrypted secret, encrypted token and expected token 37 | LoginAttempt { 38 | username: String, 39 | password: String, 40 | addr: SocketAddr, 41 | otx: OSender, 42 | tx: Sender, 43 | }, 44 | UserJoined(String), 45 | UserLeft(SocketAddr), 46 | UsersQuery(SocketAddr), 47 | UsersQueryTUI(OSender>), 48 | FetchMessages(i64, i64, OSender>), 49 | CheckPermissions(String, OSender), 50 | KickUser(String), 51 | BanUser(String, bool), 52 | WhitelistUser(String, bool), 53 | SetWhitelist(bool), 54 | SetAllowNewAccounts(bool), 55 | } 56 | 57 | pub type LoginResult = Result; 58 | -------------------------------------------------------------------------------- /server/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::path::PathBuf; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Represents config file loaded into memory 7 | #[derive(Serialize, Deserialize)] 8 | pub struct Config { 9 | pub db_host: String, 10 | pub db_port: String, 11 | pub db_user: String, 12 | pub db_pass: String, 13 | pub db_dbname: String, 14 | pub port: Option, 15 | pub operators: HashSet, 16 | pub whitelist_on: bool, 17 | pub allow_new_accounts: bool, 18 | } 19 | 20 | impl Default for Config { 21 | fn default() -> Self { 22 | Self { 23 | db_host: Default::default(), 24 | db_port: Default::default(), 25 | db_user: Default::default(), 26 | db_pass: Default::default(), 27 | db_dbname: Default::default(), 28 | port: Some(accord::DEFAULT_PORT), 29 | operators: Default::default(), 30 | whitelist_on: false, 31 | allow_new_accounts: true, 32 | } 33 | } 34 | } 35 | 36 | const CONFIG_FILE: &str = "config.toml"; 37 | 38 | fn config_path() -> PathBuf { 39 | let mut path = config_path_dir(); 40 | path.push(CONFIG_FILE); 41 | log::info!("Config path: {:?}.", path); 42 | path 43 | } 44 | 45 | #[cfg(unix)] 46 | fn config_path_dir() -> PathBuf { 47 | let xdg_dirs = xdg::BaseDirectories::with_prefix("accord-server").unwrap(); 48 | xdg_dirs.get_config_home() 49 | } 50 | 51 | #[cfg(windows)] 52 | fn config_path_dir() -> PathBuf { 53 | let local_app_data = std::env::var("LOCALAPPDATA").unwrap(); 54 | let mut path = PathBuf::from(local_app_data); 55 | path.push("accord-server"); 56 | path 57 | } 58 | 59 | pub fn save_config(config: &Config) -> std::io::Result<()> { 60 | log::info!("Saving config."); 61 | let config_path = config_path(); 62 | std::fs::create_dir_all(config_path_dir()).unwrap(); 63 | 64 | let toml = toml::to_string(config).unwrap(); 65 | std::fs::write(config_path, &toml) 66 | } 67 | 68 | pub fn load_config() -> Config { 69 | log::info!("Loading config."); 70 | let config_path = config_path(); 71 | let toml = std::fs::read_to_string(config_path); 72 | let config = if let Ok(toml) = toml { 73 | match toml::from_str(&toml) { 74 | Ok(config) => config, 75 | Err(e) => { 76 | log::error!("Failed to parse config: {e}."); 77 | std::process::exit(-1) 78 | } 79 | } 80 | } else { 81 | log::info!("Failed to load config, using default and saving default."); 82 | save_config(&Config::default()).unwrap(); 83 | Config::default() 84 | }; 85 | config 86 | } 87 | -------------------------------------------------------------------------------- /server/src/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::*; 2 | use accord::connection::*; 3 | use accord::packets::*; 4 | use accord::utils::verify_message; 5 | use tokio::sync::mpsc::{self, Receiver, Sender}; 6 | use tokio::sync::oneshot; 7 | 8 | use rand::SeedableRng; 9 | use rand_chacha::ChaCha20Rng; 10 | 11 | /// A wrapper for incoming connection to the channel. 12 | pub struct ConnectionWrapper; // Maybe this shouldn't be a struct? 13 | 14 | impl ConnectionWrapper { 15 | /// Handles incoming connection and spawns reading and writing loops. 16 | pub async fn spawn( 17 | socket: tokio::net::TcpStream, 18 | addr: std::net::SocketAddr, 19 | ctx: Sender, 20 | ) { 21 | let (tx, rx) = mpsc::channel::(32); 22 | log::info!("Connection from: {:?}", addr); 23 | let connection = Connection::::new(socket); 24 | let (reader, writer) = connection.split(); 25 | let reader_wrapped = ConnectionReaderWrapper::new(reader, addr, tx, ctx); 26 | tokio::spawn(reader_wrapped.spawn_loop()); 27 | let writer_wrapped = ConnectionWriterWrapper::new(writer, rx); 28 | tokio::spawn(writer_wrapped.spawn_loop()); 29 | } 30 | } 31 | 32 | pub struct ConnectionReaderWrapper { 33 | reader: ConnectionReader, 34 | addr: std::net::SocketAddr, 35 | connection_sender: Sender, 36 | channel_sender: Sender, 37 | user_id: Option, 38 | username: Option, 39 | secret: Option>, 40 | nonce_generator: Option, 41 | } 42 | 43 | impl ConnectionReaderWrapper { 44 | fn new( 45 | reader: ConnectionReader, 46 | addr: std::net::SocketAddr, 47 | connection_sender: Sender, 48 | channel_sender: Sender, 49 | ) -> Self { 50 | Self { 51 | reader, 52 | addr, 53 | connection_sender, 54 | channel_sender, 55 | user_id: None, 56 | username: None, 57 | secret: None, 58 | nonce_generator: None, 59 | } 60 | } 61 | 62 | async fn handle_login(&mut self, un: String, password: String) { 63 | let (otx, orx) = oneshot::channel(); 64 | self.channel_sender 65 | .send(ChannelCommand::LoginAttempt { 66 | username: un.clone(), 67 | password, 68 | addr: self.addr, 69 | otx, 70 | tx: self.connection_sender.clone(), 71 | }) 72 | .await 73 | .unwrap(); 74 | match orx.await.unwrap() { 75 | Ok(response) => { 76 | let mut response_split = response.split('|'); 77 | self.user_id = Some(response_split.next().unwrap().parse().unwrap()); 78 | self.username = Some(response_split.next().unwrap().parse().unwrap()); 79 | 80 | self.connection_sender 81 | .send(ConnectionCommand::Write(ClientboundPacket::LoginAck)) 82 | .await 83 | .unwrap(); 84 | self.channel_sender 85 | .send(ChannelCommand::UserJoined(self.username.clone().unwrap())) 86 | .await 87 | .unwrap(); 88 | } 89 | Err(m) => { 90 | self.connection_sender 91 | .send(ConnectionCommand::Write(ClientboundPacket::LoginFailed(m))) 92 | .await 93 | .unwrap(); 94 | self.connection_sender 95 | .send(ConnectionCommand::Close) 96 | .await 97 | .unwrap(); 98 | } 99 | } 100 | } 101 | 102 | async fn handle_encryption_request(&mut self) { 103 | use ServerboundPacket::*; 104 | // To send back the token 105 | let (otx, orx) = oneshot::channel(); 106 | self.channel_sender 107 | .send(ChannelCommand::EncryptionRequest( 108 | self.connection_sender.clone(), 109 | otx, 110 | )) 111 | .await 112 | .unwrap(); 113 | 114 | let expect_token = orx.await.unwrap(); 115 | 116 | // Now we expect EncryptionConfirm with encrypted secret and token 117 | match self 118 | .reader 119 | .read_packet(&self.secret, self.nonce_generator.as_mut()) 120 | .await 121 | { 122 | Ok(Some(EncryptionConfirm(s, t))) => { 123 | let (otx, orx) = oneshot::channel(); 124 | self.channel_sender 125 | .send(ChannelCommand::EncryptionConfirm( 126 | self.connection_sender.clone(), 127 | otx, 128 | s.clone(), 129 | t, 130 | expect_token, 131 | )) 132 | .await 133 | .unwrap(); 134 | 135 | // Get decrypted secret back from channel 136 | match orx.await.unwrap() { 137 | Ok(s) => { 138 | self.secret = Some(s.clone()); 139 | let mut seed = [0u8; accord::SECRET_LEN]; 140 | seed.copy_from_slice(&s); 141 | 142 | self.nonce_generator = Some(ChaCha20Rng::from_seed(seed)); 143 | } 144 | Err(_) => { 145 | self.connection_sender 146 | .send(ConnectionCommand::Close) 147 | .await 148 | .ok(); // it's ok if already closed 149 | } 150 | } 151 | } 152 | Ok(_) => { 153 | log::warn!("Client sent wrong packet during encryption handshake."); 154 | self.connection_sender 155 | .send(ConnectionCommand::Close) 156 | .await 157 | .ok(); // it's ok if already closed 158 | } 159 | Err(_) => { 160 | log::warn!("Error during encryption handshake."); 161 | self.connection_sender 162 | .send(ConnectionCommand::Close) 163 | .await 164 | .ok(); // it's ok if already closed 165 | } 166 | }; 167 | } 168 | 169 | async fn handle_packet(&mut self, packet: ServerboundPacket) { 170 | use ServerboundPacket::*; 171 | match packet { 172 | // ping 173 | Ping => { 174 | // pong 175 | let com = ConnectionCommand::Write(ClientboundPacket::Pong); 176 | self.connection_sender.send(com).await.unwrap(); 177 | } 178 | // User tries to log in 179 | Login { 180 | username: un, 181 | password, 182 | } => { 183 | if self.username.is_some() { 184 | log::warn!("{} tried to log in while already logged in, ignoring.", un); 185 | } else { 186 | self.handle_login(un, password).await; 187 | } 188 | } 189 | // Users requests encryption 190 | EncryptionRequest => self.handle_encryption_request().await, 191 | // rest is only for logged in users 192 | p => { 193 | if self.username.is_some() { 194 | match p { 195 | // User wants to send a message 196 | Message(m) => { 197 | if verify_message(&m) { 198 | let p = ClientboundPacket::Message(accord::packets::Message { 199 | sender_id: self.user_id.unwrap(), 200 | sender: self.username.clone().unwrap(), 201 | text: m, 202 | time: current_time_as_sec(), 203 | }); 204 | self.channel_sender 205 | .send(ChannelCommand::Write(p)) 206 | .await 207 | .unwrap(); 208 | } else { 209 | log::info!("Invalid message from {:?}: {}", self.username, m); 210 | } 211 | } 212 | // User sends an image 213 | ImageMessage(im) => { 214 | let p = 215 | ClientboundPacket::ImageMessage(accord::packets::ImageMessage { 216 | image_bytes: im, 217 | sender_id: self.user_id.unwrap(), 218 | sender: self.username.clone().unwrap(), 219 | time: current_time_as_sec(), 220 | }); 221 | self.channel_sender 222 | .send(ChannelCommand::Write(p)) 223 | .await 224 | .unwrap(); 225 | } 226 | // User issued a commend (i.e "/list") 227 | Command(command) => { 228 | //TODO: abstract this code more 229 | let mut split = command.as_str().split(' '); 230 | if let Some(command) = split.next() { 231 | match command { 232 | "list" => { 233 | self.channel_sender 234 | .send(ChannelCommand::UsersQuery(self.addr)) 235 | .await 236 | .unwrap(); 237 | } 238 | "kick" => { 239 | let m = if let Some(target) = split.next() { 240 | let perms = self 241 | .get_perms(self.username.to_owned().unwrap()) 242 | .await; 243 | if let Ok(perms) = perms { 244 | if perms.operator { 245 | self.channel_sender 246 | .send(ChannelCommand::KickUser( 247 | target.to_owned(), 248 | )) 249 | .await 250 | .unwrap(); 251 | format!("{} kicked.", target) 252 | } else { 253 | "Not permitted.".to_owned() 254 | } 255 | } else { 256 | "Error.".to_owned() 257 | } 258 | } else { 259 | "No target provided".to_owned() 260 | }; 261 | self.respond(m).await; 262 | } 263 | "ban" => { 264 | self.ban_command(split.next(), true).await; 265 | } 266 | "unban" => { 267 | self.ban_command(split.next(), false).await; 268 | } 269 | "whitelist" => { 270 | self.whitelist_command(split.next(), true).await; 271 | } 272 | "unwhitelist" => { 273 | self.whitelist_command(split.next(), false).await; 274 | } 275 | "set_whitelist" => { 276 | let m = if let Some(arg) = split.next() { 277 | match arg { 278 | "on" | "true" => { 279 | self.channel_sender 280 | .send(ChannelCommand::SetWhitelist(true)) 281 | .await 282 | .unwrap(); 283 | "Whitelist on.".to_string() 284 | } 285 | "off" | "false" => { 286 | self.channel_sender 287 | .send(ChannelCommand::SetWhitelist(false)) 288 | .await 289 | .unwrap(); 290 | "Whitelist off.".to_string() 291 | } 292 | _ => { 293 | format!("Invalid argument: {}.\nExpected \"on\"/\"off\"", arg) 294 | } 295 | } 296 | } else { 297 | "No argument provided".to_string() 298 | }; 299 | self.respond(m).await; 300 | } 301 | "set_allow_new_accounts" => { 302 | let m = if let Some(arg) = split.next() { 303 | match arg { 304 | "on" | "true" => { 305 | self.channel_sender 306 | .send(ChannelCommand::SetAllowNewAccounts( 307 | true, 308 | )) 309 | .await 310 | .unwrap(); 311 | "Allow new accounts on.".to_string() 312 | } 313 | "off" | "false" => { 314 | self.channel_sender 315 | .send(ChannelCommand::SetAllowNewAccounts( 316 | false, 317 | )) 318 | .await 319 | .unwrap(); 320 | "Allow new accounts off.".to_string() 321 | } 322 | _ => { 323 | format!("Invalid argument: {}.\nExpected \"on\"/\"off\"", arg) 324 | } 325 | } 326 | } else { 327 | "No argument provided".to_string() 328 | }; 329 | self.respond(m).await; 330 | } 331 | c => { 332 | self.respond(format!("Unknown command: {}", c)).await; 333 | } 334 | } 335 | } 336 | } 337 | FetchMessages(o, n) => { 338 | let (otx, orx) = oneshot::channel(); 339 | self.channel_sender 340 | .send(ChannelCommand::FetchMessages(o, n, otx)) 341 | .await 342 | .unwrap(); 343 | let mut messages = orx.await.unwrap(); 344 | for m in messages.drain(..).rev() { 345 | self.connection_sender 346 | .send(ConnectionCommand::Write(m)) 347 | .await 348 | .unwrap(); 349 | } 350 | } 351 | p => { 352 | unreachable!("{:?} should have been handled!", p); 353 | } 354 | } 355 | } else { 356 | log::warn!("Someone tried to do something without being logged in"); 357 | } 358 | } 359 | }; 360 | } 361 | 362 | /// Listens for incoming packets from user and handles them. 363 | async fn spawn_loop(mut self) { 364 | loop { 365 | match self 366 | .reader 367 | .read_packet(&self.secret, self.nonce_generator.as_mut()) 368 | .await 369 | { 370 | Ok(p) => { 371 | match p { 372 | Some(ServerboundPacket::ImageMessage(_)) => { 373 | log::trace!("Got image packet"); 374 | } 375 | _ => log::trace!("Got packet: {:?}", p), 376 | } 377 | if let Some(p) = p { 378 | self.handle_packet(p).await; 379 | } 380 | } 381 | Err(e) => { 382 | self.channel_sender 383 | .send(ChannelCommand::UserLeft(self.addr)) 384 | .await 385 | .unwrap(); 386 | self.connection_sender 387 | .send(ConnectionCommand::Close) 388 | .await 389 | .ok(); // it's ok if already closed 390 | 391 | // This "error" is expected 392 | if e == "Connection reset by peer" { 393 | log::info!("{}", e); 394 | } else { 395 | log::error!("Err: {:?}", e); 396 | } 397 | break; 398 | } 399 | } 400 | } 401 | } 402 | 403 | /// Gets permissions of user identified by username 404 | async fn get_perms( 405 | &mut self, 406 | username: String, 407 | ) -> Result { 408 | let (otx, orx) = oneshot::channel(); 409 | self.channel_sender 410 | .send(ChannelCommand::CheckPermissions(username, otx)) 411 | .await 412 | .unwrap(); 413 | orx.await 414 | } 415 | 416 | /// switch == true => ban 417 | /// switch == false => unban 418 | async fn ban_command(&mut self, target: Option<&str>, switch: bool) { 419 | let m = if let Some(target) = target { 420 | let perms = self.get_perms(self.username.to_owned().unwrap()).await; 421 | if let Ok(perms) = perms { 422 | if perms.operator { 423 | self.channel_sender 424 | .send(ChannelCommand::BanUser(target.to_owned(), switch)) 425 | .await 426 | .unwrap(); 427 | let prefix = if switch { "" } else { "un" }; 428 | format!("{} {}banned.", target, prefix) 429 | } else { 430 | "Not permitted.".to_owned() 431 | } 432 | } else { 433 | "Error.".to_owned() 434 | } 435 | } else { 436 | "No target provided".to_owned() 437 | }; 438 | self.respond(m).await; 439 | } 440 | 441 | /// switch == true => add to whitelist 442 | /// switch == false => remove form whitelist 443 | async fn whitelist_command(&mut self, target: Option<&str>, switch: bool) { 444 | let m = if let Some(target) = target { 445 | let perms = self.get_perms(self.username.to_owned().unwrap()).await; 446 | if let Ok(perms) = perms { 447 | if perms.operator { 448 | self.channel_sender 449 | .send(ChannelCommand::WhitelistUser(target.to_owned(), switch)) 450 | .await 451 | .unwrap(); 452 | let prefix = if switch { "" } else { "un" }; 453 | format!("{} {}whitelisted.", target, prefix) 454 | } else { 455 | "Not permitted.".to_owned() 456 | } 457 | } else { 458 | "Error.".to_owned() 459 | } 460 | } else { 461 | "No target provided".to_owned() 462 | }; 463 | self.respond(m).await; 464 | } 465 | 466 | /// Sends `message` to the user of this channel as a reply from the server. 467 | async fn respond(&mut self, message: String) { 468 | let p = ClientboundPacket::Message(accord::packets::Message { 469 | sender_id: 0, 470 | sender: "#SERVER#".to_string(), 471 | text: message, 472 | time: current_time_as_sec(), 473 | }); 474 | self.connection_sender 475 | .send(ConnectionCommand::Write(p)) 476 | .await 477 | .unwrap(); 478 | } 479 | } 480 | 481 | pub struct ConnectionWriterWrapper { 482 | writer: ConnectionWriter, 483 | connection_receiver: Receiver, 484 | secret: Option>, 485 | nonce_generator: Option, 486 | } 487 | impl ConnectionWriterWrapper { 488 | fn new( 489 | writer: ConnectionWriter, 490 | connection_receiver: Receiver, 491 | ) -> Self { 492 | Self { 493 | writer, 494 | connection_receiver, 495 | secret: None, 496 | nonce_generator: None, 497 | } 498 | } 499 | 500 | /// Listens for commands and sends packets to user. 501 | async fn spawn_loop(mut self) { 502 | loop { 503 | if let Some(com) = self.connection_receiver.recv().await { 504 | use ConnectionCommand::*; 505 | match com { 506 | Close => break, 507 | SetSecret(s) => { 508 | self.secret = s.clone(); 509 | let mut seed = [0u8; accord::SECRET_LEN]; 510 | seed.copy_from_slice(&s.unwrap()); 511 | 512 | self.nonce_generator = Some(ChaCha20Rng::from_seed(seed)); 513 | } 514 | Write(p) => self 515 | .writer 516 | .write_packet(p, &self.secret, self.nonce_generator.as_mut()) 517 | .await 518 | .unwrap(), 519 | } 520 | } 521 | } 522 | } 523 | } 524 | 525 | /// Current time since unix epoch in seconds 526 | #[inline] 527 | fn current_time_as_sec() -> u64 { 528 | use std::time::{SystemTime, UNIX_EPOCH}; 529 | SystemTime::now() 530 | .duration_since(UNIX_EPOCH) 531 | .unwrap() 532 | .as_secs() 533 | } 534 | -------------------------------------------------------------------------------- /server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod channel; 2 | pub mod commands; 3 | pub mod config; 4 | pub mod connection; 5 | -------------------------------------------------------------------------------- /server/src/logging.rs: -------------------------------------------------------------------------------- 1 | use flexi_logger::{writers::LogWriter, DeferredNow, FormatFunction}; 2 | use log::Record; 3 | use tokio::sync::mpsc; 4 | 5 | /// A single entry in the logs. 6 | pub struct LogEntry { 7 | pub level: log::Level, 8 | pub target: String, 9 | pub args: String, 10 | } 11 | 12 | impl From<&Record<'_>> for LogEntry { 13 | fn from(record: &Record) -> Self { 14 | Self { 15 | level: record.level(), 16 | target: record.target().to_string(), 17 | args: record.args().to_string(), 18 | } 19 | } 20 | } 21 | 22 | /// Sends incoming logs to TUI. 23 | pub struct LogRouter { 24 | logs_tx: mpsc::Sender, 25 | } 26 | 27 | impl LogRouter { 28 | pub fn new(logs_tx: mpsc::Sender) -> Self { 29 | Self { logs_tx } 30 | } 31 | } 32 | 33 | impl LogWriter for LogRouter { 34 | fn max_log_level(&self) -> log::LevelFilter { 35 | log::LevelFilter::Trace 36 | } 37 | 38 | fn format(&mut self, format: FormatFunction) { 39 | let _ = format; 40 | } 41 | 42 | fn shutdown(&self) {} 43 | 44 | fn write(&self, _now: &mut DeferredNow, record: &Record) -> std::io::Result<()> { 45 | let s = record.into(); 46 | self.logs_tx 47 | .try_send(s) 48 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) 49 | } 50 | 51 | fn flush(&self) -> std::io::Result<()> { 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | use tokio::net::TcpListener; 2 | 3 | use tokio::sync::mpsc; 4 | 5 | use accord_server::channel::AccordChannel; 6 | use accord_server::connection::ConnectionWrapper; 7 | 8 | use clap::Parser; 9 | 10 | use flexi_logger::{writers::LogWriter, FileSpec, Logger}; 11 | //TODO: pad message for security/privacy (so length isn't obvious)? 12 | 13 | mod logging; 14 | mod tui; 15 | 16 | #[derive(Parser)] 17 | #[clap(author, version, about, long_about = None)] 18 | struct Args { 19 | /// Disable TUI (just output to stdout, no commandline) 20 | #[clap(short, long)] 21 | no_tui: bool, 22 | 23 | /// Log to file as well 24 | #[clap(short, long)] 25 | log_to_file: bool, 26 | } 27 | 28 | fn init_logger_tui(writer: Box, log_to_file: bool) { 29 | let logger = Logger::try_with_env_or_str("info").unwrap(); 30 | 31 | let logger = if log_to_file { 32 | logger.log_to_file_and_writer(FileSpec::default(), writer) 33 | } else { 34 | logger.log_to_writer(writer) 35 | }; 36 | if let Err(e) = logger.start() { 37 | eprintln!("Error while setting up logger: {}", e); 38 | } 39 | } 40 | 41 | fn init_logger_stdout(log_to_file: bool) { 42 | let logger = Logger::try_with_env_or_str("info").unwrap(); 43 | 44 | let logger = if log_to_file { 45 | logger 46 | .log_to_file(FileSpec::default()) 47 | .duplicate_to_stdout(flexi_logger::Duplicate::All) 48 | } else { 49 | logger 50 | }; 51 | if let Err(e) = logger.start() { 52 | eprintln!("Error while setting up logger: {}", e); 53 | } 54 | } 55 | 56 | #[tokio::main] 57 | async fn main() { 58 | let args = Args::parse(); 59 | 60 | let (ctx, crx) = mpsc::channel(32); 61 | let tui = !args.no_tui; 62 | let mut tui_handle = None; 63 | if tui { 64 | let (logs_tx, logs_rx) = mpsc::channel(128); 65 | let writer = logging::LogRouter::new(logs_tx); 66 | init_logger_tui(Box::new(writer), args.log_to_file); 67 | tui_handle = Some(tui::Tui::new(logs_rx, ctx.clone()).launch()); 68 | } else { 69 | init_logger_stdout(args.log_to_file); 70 | } 71 | 72 | let config = accord_server::config::load_config(); 73 | 74 | let port = config.port.unwrap_or(accord::DEFAULT_PORT); 75 | let listener = match TcpListener::bind(("0.0.0.0", port)).await { 76 | Ok(listener) => listener, 77 | Err(e) => { 78 | log::error!("Failed to bind to port {}. Error: {}", port, e); 79 | if let Some(tui_handle) = tui_handle { 80 | log::info!("Enter `exit` command to exit."); 81 | if let Err(e) = tui_handle.await { 82 | eprintln!("Error while joining tui_handle: {}", e); 83 | } 84 | return; 85 | } 86 | return; 87 | } 88 | }; 89 | 90 | log::info!("Listening on port {}.", port); 91 | 92 | let result = AccordChannel::spawn(crx, config).await; 93 | match result { 94 | Err(e) => { 95 | log::error!("Failed to start server. Error: {}", e); 96 | if let Some(tui_handle) = tui_handle { 97 | log::info!("Enter `exit` command to exit."); 98 | if let Err(e) = tui_handle.await { 99 | eprintln!("Error while joining tui_handle: {}", e); 100 | } 101 | } 102 | } 103 | Ok(_) => { 104 | log::info!("Server ready!"); 105 | if let Some(mut tui_handle2) = tui_handle { 106 | loop { 107 | tokio::select! { 108 | res = listener.accept() => { 109 | let (socket, addr) = res.unwrap(); 110 | ConnectionWrapper::spawn(socket, addr, ctx.clone()).await; 111 | }, 112 | _ = &mut tui_handle2 => { 113 | break; 114 | } 115 | } 116 | } 117 | } else { 118 | #[cfg(unix)] 119 | tokio::spawn(async move { 120 | tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) 121 | .unwrap() 122 | .recv() 123 | .await; 124 | std::process::exit(0); 125 | }); 126 | 127 | loop { 128 | let (socket, addr) = listener.accept().await.unwrap(); 129 | ConnectionWrapper::spawn(socket, addr, ctx.clone()).await; 130 | } 131 | }; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /server/src/tui.rs: -------------------------------------------------------------------------------- 1 | use accord_server::commands::ChannelCommand; 2 | use futures::{FutureExt, StreamExt}; 3 | use tokio::sync::mpsc; 4 | 5 | use crossterm::{ 6 | event::{ 7 | DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEvent, 8 | KeyModifiers, 9 | }, 10 | execute, 11 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 12 | }; 13 | 14 | use std::io::{self, Stdout}; 15 | use tui::{ 16 | backend::CrosstermBackend, 17 | layout::{Constraint, Direction, Layout}, 18 | style::{Color, Modifier, Style}, 19 | text::{Span, Spans}, 20 | widgets::{Block, Borders, List, ListItem, Paragraph}, 21 | Frame, Terminal, 22 | }; 23 | 24 | use crate::logging::LogEntry; 25 | 26 | /// Main TUI struct 27 | pub struct Tui { 28 | logs_rx: mpsc::Receiver, 29 | logs: Vec, 30 | scroll: usize, 31 | event_stream: EventStream, 32 | commandline: String, 33 | channel_sender: mpsc::Sender, 34 | terminal: Option>>, 35 | } 36 | 37 | impl Drop for Tui { 38 | fn drop(&mut self) { 39 | // Restore terminal on drop 40 | disable_raw_mode().unwrap(); 41 | if let Some(terminal) = &mut self.terminal { 42 | execute!( 43 | terminal.backend_mut(), 44 | LeaveAlternateScreen, 45 | DisableMouseCapture 46 | ) 47 | .unwrap(); 48 | } 49 | } 50 | } 51 | 52 | impl Tui { 53 | pub fn new( 54 | logs_rx: mpsc::Receiver, 55 | channel_sender: mpsc::Sender, 56 | ) -> Self { 57 | Self { 58 | logs_rx, 59 | channel_sender, 60 | logs: Vec::new(), 61 | scroll: 0, 62 | event_stream: EventStream::new(), 63 | commandline: String::new(), 64 | terminal: None, 65 | } 66 | } 67 | 68 | /// Launches the TUI, starting the main loop in new thread 69 | /// and returns a handle to that task. 70 | pub fn launch(mut self) -> tokio::task::JoinHandle<()> { 71 | tokio::spawn(async move { 72 | enable_raw_mode().unwrap(); 73 | 74 | let mut stdout = io::stdout(); 75 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); 76 | let backend = CrosstermBackend::new(stdout); 77 | let terminal = Terminal::new(backend).unwrap(); 78 | self.terminal.replace(terminal); 79 | loop { 80 | if self.main_loop().await { 81 | break; 82 | }; 83 | } 84 | drop(self); 85 | }) 86 | } 87 | 88 | /// Main loop of TUI 89 | /// Handles incoming terminal events and log updates. 90 | /// 91 | /// Returns whether the loop should be stopped. 92 | async fn main_loop(&mut self) -> bool { 93 | let incoming_log = self.logs_rx.recv(); 94 | let event = self.event_stream.next().fuse(); 95 | let exit_event = KeyEvent { 96 | code: KeyCode::Char('c'), 97 | modifiers: KeyModifiers::CONTROL, 98 | }; 99 | tokio::select! { 100 | maybe_log = incoming_log => { 101 | match maybe_log { 102 | Some(log_entry) => { 103 | self.logs.push(log_entry); 104 | } 105 | None => panic!("Log writer dropped before TUI!"), 106 | } 107 | }, 108 | maybe_event = event => { 109 | match maybe_event { 110 | Some(Ok(event)) => { 111 | if let Event::Key(kevent) = event { 112 | if kevent == exit_event { 113 | self.respond("Enter 'exit' command to exit."); 114 | return false; 115 | } 116 | if let KeyEvent{code: KeyCode::Char(c), modifiers: _} = kevent { 117 | self.commandline.push(c); 118 | } 119 | if kevent == KeyCode::Backspace.into() { 120 | self.commandline.pop(); 121 | } 122 | if kevent == KeyCode::Enter.into() { 123 | return self.try_command().await; 124 | } 125 | if kevent == KeyCode::Up.into() { 126 | self.scroll = self.scroll.saturating_sub(1); 127 | } 128 | if kevent == KeyCode::Down.into() { 129 | self.scroll = self.scroll.saturating_add(1).min(self.logs.len()-1); 130 | } 131 | if kevent == KeyCode::PageUp.into() { 132 | self.scroll = self.scroll.saturating_sub(10); 133 | } 134 | if kevent == KeyCode::PageDown.into() { 135 | self.scroll = self.scroll.saturating_add(10).min(self.logs.len()-1); 136 | } 137 | if kevent == KeyCode::Home.into() { 138 | self.scroll = 0; 139 | } 140 | if kevent == KeyCode::End.into() { 141 | self.scroll = self.logs.len().saturating_sub(1); 142 | } 143 | if kevent == KeyCode::Up.into() { 144 | self.scroll = self.scroll.saturating_sub(1); 145 | } 146 | 147 | } 148 | } 149 | Some(Err(e)) => log::error!("Error while getting event: {}", e), 150 | None => return true, 151 | } 152 | } 153 | }; 154 | 155 | if let Some(mut terminal) = self.terminal.take() { 156 | terminal.draw(|f| self.draw(f)).unwrap(); 157 | self.terminal.replace(terminal); 158 | } 159 | 160 | false 161 | } 162 | 163 | /// Draws TUI 164 | fn draw(&mut self, frame: &mut Frame>) { 165 | let chunks = Layout::default() 166 | .direction(Direction::Vertical) 167 | .constraints( 168 | [ 169 | Constraint::Length(frame.size().height - 3), 170 | Constraint::Min(3), 171 | ] 172 | .as_ref(), 173 | ) 174 | .split(frame.size()); 175 | 176 | // Log items 177 | let items: Vec = self 178 | .logs 179 | .iter() 180 | .skip(self.scroll) 181 | .map(|l| { 182 | let mut spans = vec![]; 183 | let style = style_from_level(l.level); 184 | let def_style = Style::default().fg(Color::Gray); 185 | spans.push(Span::styled( 186 | l.level.to_string(), 187 | style.add_modifier(Modifier::BOLD), 188 | )); 189 | spans.push(Span::styled(" [", def_style)); 190 | spans.push(Span::styled(&l.target, def_style)); 191 | spans.push(Span::styled("] ", def_style)); 192 | spans.push(Span::styled(&l.args, style)); 193 | let spans = Spans::from(spans); 194 | ListItem::new(spans) 195 | }) 196 | .collect(); 197 | let items = List::new(items).block( 198 | Block::default() 199 | .borders(Borders::ALL.difference(Borders::BOTTOM)) 200 | .title("Log"), 201 | ); 202 | frame.render_widget(items, chunks[0]); 203 | let input = Paragraph::new(self.commandline.as_str()) 204 | .block(Block::default().borders(Borders::ALL).title("Commandline")); 205 | frame.set_cursor( 206 | chunks[1].x + 1 + self.commandline.len() as u16, 207 | chunks[1].y + 1, 208 | ); 209 | frame.render_widget(input, chunks[1]); 210 | } 211 | 212 | /// Consumes the commandline input and tries to use it as a command. 213 | /// 214 | /// Returns whether the command was an exit command. 215 | async fn try_command(&mut self) -> bool { 216 | if self.commandline.is_empty() { 217 | return false; 218 | } 219 | let mut command = String::new(); 220 | std::mem::swap(&mut command, &mut self.commandline); 221 | let command = command.trim_start_matches('/'); 222 | //TODO: abstract this code more 223 | let mut split = command.split(' '); 224 | if let Some(command) = split.next() { 225 | match command { 226 | "exit" => { 227 | log::info!("Exiting..."); 228 | return true; 229 | } 230 | "list" => { 231 | let (otx, orx) = tokio::sync::oneshot::channel(); 232 | 233 | self.channel_sender 234 | .send(ChannelCommand::UsersQueryTUI(otx)) 235 | .await 236 | .unwrap(); 237 | 238 | match orx.await { 239 | Ok(list) => log::info!("Connected users: {:?}", list), 240 | Err(e) => log::error!("Error while receiving user list in TUI: {}", e), 241 | } 242 | } 243 | "kick" => { 244 | let m = if let Some(target) = split.next() { 245 | self.channel_sender 246 | .send(ChannelCommand::KickUser(target.to_owned())) 247 | .await 248 | .unwrap(); 249 | format!("Kicking {}.", target) 250 | } else { 251 | "No target provided".to_owned() 252 | }; 253 | self.respond(m); 254 | } 255 | "ban" => { 256 | self.ban_command(split.next(), true).await; 257 | } 258 | "unban" => { 259 | self.ban_command(split.next(), false).await; 260 | } 261 | "whitelist" => { 262 | self.whitelist_command(split.next(), true).await; 263 | } 264 | "unwhitelist" => { 265 | self.whitelist_command(split.next(), false).await; 266 | } 267 | "set_whitelist" => { 268 | let m = if let Some(arg) = split.next() { 269 | match arg { 270 | "on" | "true" => { 271 | self.channel_sender 272 | .send(ChannelCommand::SetWhitelist(true)) 273 | .await 274 | .unwrap(); 275 | "Whitelist on.".to_string() 276 | } 277 | "off" | "false" => { 278 | self.channel_sender 279 | .send(ChannelCommand::SetWhitelist(false)) 280 | .await 281 | .unwrap(); 282 | "Whitelist off.".to_string() 283 | } 284 | _ => { 285 | format!("Invalid argument: {}.\nExpected \"on\"/\"off\"", arg) 286 | } 287 | } 288 | } else { 289 | "No argument provided".to_string() 290 | }; 291 | self.respond(m); 292 | } 293 | "set_allow_new_accounts" => { 294 | let m = if let Some(arg) = split.next() { 295 | match arg { 296 | "on" | "true" => { 297 | self.channel_sender 298 | .send(ChannelCommand::SetAllowNewAccounts(true)) 299 | .await 300 | .unwrap(); 301 | "Allow new accounts on.".to_string() 302 | } 303 | "off" | "false" => { 304 | self.channel_sender 305 | .send(ChannelCommand::SetAllowNewAccounts(false)) 306 | .await 307 | .unwrap(); 308 | "Allow new accounts off.".to_string() 309 | } 310 | _ => { 311 | format!("Invalid argument: {}.\nExpected \"on\"/\"off\"", arg) 312 | } 313 | } 314 | } else { 315 | "No argument provided".to_string() 316 | }; 317 | self.respond(m); 318 | } 319 | c => { 320 | self.respond(format!("Unknown command: {}", c)); 321 | } 322 | } 323 | }; 324 | false 325 | } 326 | 327 | /// switch == true => ban 328 | /// switch == false => unban 329 | async fn ban_command(&mut self, target: Option<&str>, switch: bool) { 330 | let m = if let Some(target) = target { 331 | self.channel_sender 332 | .send(ChannelCommand::BanUser(target.to_owned(), switch)) 333 | .await 334 | .unwrap(); 335 | if switch { 336 | format!("Banning {}", target) 337 | } else { 338 | format!("Unbanning {}.", target) 339 | } 340 | } else { 341 | "No target provided".to_owned() 342 | }; 343 | self.respond(m); 344 | } 345 | 346 | /// switch == true => add to whitelist 347 | /// switch == false => remove from whitelist 348 | async fn whitelist_command(&mut self, target: Option<&str>, switch: bool) { 349 | let m = if let Some(target) = target { 350 | self.channel_sender 351 | .send(ChannelCommand::WhitelistUser(target.to_owned(), switch)) 352 | .await 353 | .unwrap(); 354 | if switch { 355 | format!("Whitelisting {}.", target) 356 | } else { 357 | format!("Unwhitelisting {}.", target) 358 | } 359 | } else { 360 | "No target provided".to_owned() 361 | }; 362 | self.respond(m); 363 | } 364 | 365 | // I don't remember why does this exist 366 | fn respond(&mut self, s: T) { 367 | log::info!("{}", s); 368 | } 369 | } 370 | 371 | fn style_from_level(level: log::Level) -> Style { 372 | match level { 373 | flexi_logger::Level::Error => Style::default().fg(Color::Red), 374 | flexi_logger::Level::Warn => Style::default().fg(Color::Yellow), 375 | flexi_logger::Level::Info => Style::default(), 376 | flexi_logger::Level::Debug => Style::default().fg(Color::Green), 377 | flexi_logger::Level::Trace => Style::default().fg(Color::Cyan), 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use bytes::BytesMut; 4 | use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter}; 5 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 6 | use tokio::net::TcpStream; 7 | 8 | use crate::packets::*; 9 | 10 | use rand::RngCore; 11 | use rand_chacha::ChaCha20Rng; 12 | 13 | use encryption::*; 14 | 15 | /// Connection that is later split into separate reader and writer. 16 | /// 17 | /// I = Incoming Packets 18 | /// O = Outgoing Packets 19 | pub struct Connection { 20 | stream: TcpStream, 21 | _marker: PhantomData<(I, O)>, 22 | } 23 | 24 | /// Reading half of the connection. 25 | pub struct ConnectionReader { 26 | stream: OwnedReadHalf, 27 | buffer: BytesMut, 28 | _marker: PhantomData

, 29 | } 30 | 31 | /// Writing half of the connection. 32 | pub struct ConnectionWriter { 33 | stream: BufWriter, 34 | _marker: PhantomData

, 35 | } 36 | 37 | impl Connection 38 | where 39 | I: Packet, 40 | O: Packet, 41 | { 42 | /// New connection over TCP stream. 43 | pub fn new(stream: TcpStream) -> Self { 44 | Self { 45 | stream, 46 | _marker: PhantomData, 47 | } 48 | } 49 | 50 | /// Splits stream to separate handles so they can be used in separate threads. 51 | pub fn split(self) -> (ConnectionReader, ConnectionWriter) { 52 | let (read, write) = self.stream.into_split(); 53 | let read = ConnectionReader:: { 54 | stream: read, 55 | buffer: BytesMut::with_capacity(4096), 56 | _marker: PhantomData, 57 | }; 58 | let write = ConnectionWriter:: { 59 | stream: BufWriter::new(write), 60 | _marker: PhantomData, 61 | }; 62 | (read, write) 63 | } 64 | } 65 | 66 | impl ConnectionReader

{ 67 | /// Tries to read incoming packet on TCP stream 68 | /// and decrypts if secret and nonce_generator are `Some` 69 | pub async fn read_packet( 70 | &mut self, 71 | secret: &Option>, 72 | nonce_generator: Option<&mut ChaCha20Rng>, 73 | ) -> Result, String> { 74 | let secret_and_nonce = if let Some(secret) = secret { 75 | let mut buf = [0u8; crate::SECRET_LEN]; 76 | buf.copy_from_slice(&secret[..]); 77 | let mut nonce = [0u8; crate::NONCE_LEN]; 78 | nonce_generator 79 | .expect("Expected `nonce_generator` to be `Some` because `secret` was `Some`.") 80 | .fill_bytes(&mut nonce); 81 | Some((buf, nonce)) 82 | } else { 83 | None 84 | }; 85 | loop { 86 | if let Some((secret, nonce)) = secret_and_nonce { 87 | if let Ok((p, b)) = 88 | decrypt_frame(&mut self.buffer.as_ref(), &secret, &nonce) 89 | { 90 | self.buffer = BytesMut::from(b); 91 | if let Ok((p, _)) = P::deserialized(&p) { 92 | return Ok(Some(p)); 93 | } 94 | } 95 | } else if let Ok((p, b)) = P::deserialized(&self.buffer) { 96 | // Effectively move buffer past what we already read 97 | self.buffer = BytesMut::from(b); 98 | return Ok(Some(p)); 99 | } 100 | 101 | if 0 == self 102 | .stream 103 | .read_buf(&mut self.buffer) 104 | .await 105 | .map_err(|e| e.to_string())? 106 | { 107 | return Err("Connection reset by peer".into()); 108 | } 109 | } 110 | } 111 | } 112 | 113 | impl ConnectionWriter

{ 114 | /// Tries to write the packet to TCP stream 115 | /// and encrypts it if secret and nonce_generator are `Some` 116 | pub async fn write_packet( 117 | &mut self, 118 | packet: P, 119 | secret: &Option>, 120 | nonce_generator: Option<&mut ChaCha20Rng>, 121 | ) -> std::io::Result<()> { 122 | let secret_and_nonce = if let Some(secret) = secret { 123 | let mut buf = [0u8; crate::SECRET_LEN]; 124 | buf.copy_from_slice(&secret[..]); 125 | let mut nonce = [0u8; crate::NONCE_LEN]; 126 | nonce_generator 127 | .expect("Expected `nonce_generator` to be `Some` because `secret` was `Some`.") 128 | .fill_bytes(&mut nonce); 129 | Some((buf, nonce)) 130 | } else { 131 | None 132 | }; 133 | let mut p = packet.serialized(); 134 | if let Some((secret, nonce)) = secret_and_nonce { 135 | p = encrypt_frame(&p, &secret, &nonce); 136 | } 137 | self.stream.write_all(&p).await?; 138 | self.stream.flush().await 139 | } 140 | } 141 | 142 | mod encryption { 143 | use chacha20poly1305::{ 144 | aead::{Aead, NewAead}, 145 | XChaCha20Poly1305, 146 | }; 147 | 148 | use crate::{NONCE_LEN, SECRET_LEN}; 149 | 150 | /// Encrypts the packet using [`XChaCha20Poly1305`]. 151 | /// 152 | /// [u8; n] -> [u8;n+4] (1st 4 bytes is len) 153 | pub fn encrypt_frame( 154 | packet_bytes: &[u8], 155 | key: &[u8; SECRET_LEN], 156 | nonce: &[u8; NONCE_LEN], 157 | ) -> Vec { 158 | // This maybe could use some unsafe pointer magic to be more optimal? 159 | let cipher = XChaCha20Poly1305::new(key.into()); 160 | let len: u32 = packet_bytes.len().try_into().expect("Packet too big!"); 161 | let mut buf = vec![0; len as usize + 4]; 162 | buf[0..4].copy_from_slice(&len.to_be_bytes()); 163 | debug_assert_eq!(buf[4..].len(), len as usize); 164 | let mut buf = cipher.encrypt(nonce.into(), packet_bytes).unwrap(); 165 | let mut ret = vec![0u8; 4]; 166 | let len: u32 = buf.len().try_into().expect("Packet too big!"); 167 | ret.copy_from_slice(&len.to_be_bytes()); 168 | ret.append(&mut buf); 169 | ret 170 | } 171 | 172 | /// Decrypts the packet using [`XChaCha20Poly1305`]. 173 | /// 174 | /// [u8; n] -> [u8;n+4] (1st 4 bytes is len) 175 | pub fn decrypt_frame<'a>( 176 | encrypted_bytes: &mut &'a [u8], 177 | key: &[u8; SECRET_LEN], 178 | nonce: &[u8; NONCE_LEN], 179 | ) -> Result<(Vec, &'a [u8]), String> { 180 | if encrypted_bytes.len() < 4 { 181 | return Err("Too short".to_string()); 182 | } 183 | 184 | let data_len: u32 = super::read_be_u32(encrypted_bytes); 185 | if data_len as usize > encrypted_bytes.len() { 186 | return Err("Not full frame".to_string()); 187 | } 188 | 189 | // This maybe could use some unsafe pointer magic to be more optimal? 190 | let cipher = XChaCha20Poly1305::new(key.into()); 191 | let (packet_bytes, rest) = encrypted_bytes.split_at(data_len as usize); 192 | let ret = cipher.decrypt(nonce.into(), packet_bytes).unwrap(); 193 | Ok((ret, rest)) 194 | } 195 | } 196 | 197 | /// Reads big endian u32 from bytes, advancing input head by the size of u32 198 | fn read_be_u32(input: &mut &[u8]) -> u32 { 199 | let (int_bytes, rest) = input.split_at(std::mem::size_of::()); 200 | *input = rest; 201 | u32::from_be_bytes(int_bytes.try_into().unwrap()) 202 | } 203 | 204 | #[cfg(test)] 205 | mod test { 206 | use super::encryption::*; 207 | use crate::packets::*; 208 | use crate::{NONCE_LEN, SECRET_LEN}; 209 | #[test] 210 | fn encrypt_packet_test() { 211 | let key = [0u8; SECRET_LEN]; 212 | let nonce = [0u8; NONCE_LEN]; 213 | 214 | let packet = ServerboundPacket::Message("test".to_string()); 215 | let packet_data = packet.serialized(); 216 | let encrypted = encrypt_frame(&packet_data, &key, &nonce); 217 | let exp_encrypted = [ 218 | 0, 0, 0, 30, 249, 57, 219, 236, 150, 83, 236, 24, 188, 69, 135, 160, 198, 64, 126, 155, 219 | 247, 135, 6, 132, 161, 45, 1, 86, 75, 207, 109, 177, 135, 228, 220 | ]; 221 | assert_eq!(exp_encrypted, &encrypted[..]); 222 | } 223 | 224 | #[test] 225 | fn decrypt_packet_test() { 226 | let key = [0u8; SECRET_LEN]; 227 | let nonce = [0u8; NONCE_LEN]; 228 | 229 | let encrypted = [ 230 | 0, 0, 0, 30, 249, 57, 219, 236, 150, 83, 236, 24, 188, 69, 135, 160, 198, 64, 126, 155, 231 | 247, 135, 6, 132, 161, 45, 1, 86, 75, 207, 109, 177, 135, 228, 232 | ]; 233 | 234 | let decrypted = decrypt_frame(&mut &encrypted[..], &key, &nonce); 235 | assert_eq!( 236 | ServerboundPacket::Message("test".to_string()), 237 | ServerboundPacket::deserialized(&decrypted.unwrap().0) 238 | .unwrap() 239 | .0 240 | ); 241 | } 242 | 243 | #[test] 244 | fn encrypt_and_decrypt_packet_test() { 245 | let key = [0u8; SECRET_LEN]; 246 | let nonce = [0u8; NONCE_LEN]; 247 | 248 | let packet = ServerboundPacket::Message("test".to_string()); 249 | 250 | let packet_data = packet.serialized(); 251 | let encrypted = encrypt_frame(&packet_data, &key, &nonce); 252 | 253 | let decrypted = decrypt_frame(&mut &encrypted[..], &key, &nonce); 254 | assert_eq!( 255 | packet, 256 | ServerboundPacket::deserialized(&decrypted.unwrap().0) 257 | .unwrap() 258 | .0 259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | pub mod packets; 3 | pub mod utils; 4 | 5 | pub const DEFAULT_PORT: u16 = 13723; 6 | 7 | pub const RSA_BITS: usize = 1024; 8 | /// Length of the confirmation token sent by the server 9 | pub const ENC_TOK_LEN: usize = 32; 10 | pub const SECRET_LEN: usize = 32; 11 | pub const NONCE_LEN: usize = 24; 12 | -------------------------------------------------------------------------------- /src/packets.rs: -------------------------------------------------------------------------------- 1 | use rmp_serde::{Deserializer, Serializer}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// A text message 5 | #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] 6 | pub struct Message { 7 | pub sender_id: i64, 8 | pub sender: String, 9 | pub text: String, 10 | pub time: u64, 11 | } 12 | 13 | /// A message with an image 14 | #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] 15 | pub struct ImageMessage { 16 | pub sender_id: i64, 17 | pub sender: String, 18 | pub time: u64, 19 | pub image_bytes: Vec, 20 | } 21 | 22 | pub trait Packet { 23 | fn serialized(&self) -> Vec; 24 | fn deserialized(buf: &[u8]) -> Result<(Self, &[u8]), rmp_serde::decode::Error> 25 | where 26 | Self: std::marker::Sized; 27 | } 28 | 29 | /// Packets going from client to the server. 30 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] 31 | pub enum ServerboundPacket { 32 | Ping, 33 | EncryptionRequest, 34 | EncryptionConfirm(Vec, Vec), // encrypted secret and token 35 | Login { username: String, password: String }, 36 | Message(String), 37 | ImageMessage(Vec), 38 | Command(String), 39 | FetchMessages(i64, i64), 40 | } 41 | 42 | impl Packet for ServerboundPacket { 43 | fn serialized(&self) -> Vec { 44 | let mut buf = Vec::new(); 45 | self.serialize(&mut Serializer::new(&mut buf)).unwrap(); 46 | buf 47 | } 48 | 49 | fn deserialized(buf: &[u8]) -> Result<(Self, &[u8]), rmp_serde::decode::Error> { 50 | let mut d = Deserializer::new(buf); 51 | Self::deserialize(&mut d).map(|p| (p, d.into_inner())) 52 | } 53 | } 54 | 55 | /// Packets going from the server to client. 56 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] 57 | pub enum ClientboundPacket { 58 | Pong, 59 | EncryptionResponse(Vec, Vec), // channel's public key and token 60 | EncryptionAck, 61 | LoginAck, 62 | LoginFailed(String), 63 | UserJoined(String), 64 | UserLeft(String), 65 | UsersOnline(Vec), 66 | Message(Message), 67 | ImageMessage(ImageMessage), 68 | } 69 | 70 | impl Packet for ClientboundPacket { 71 | fn serialized(&self) -> Vec { 72 | let mut buf = Vec::new(); 73 | self.serialize(&mut Serializer::new(&mut buf)).unwrap(); 74 | buf 75 | } 76 | 77 | fn deserialized(buf: &[u8]) -> Result<(Self, &[u8]), rmp_serde::decode::Error> { 78 | let mut d = Deserializer::new(buf); 79 | Self::deserialize(&mut d).map(|p| (p, d.into_inner())) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /// Checks for incorrect characters (i.e. control characters) 2 | #[inline] 3 | pub fn verify_message>(m: T) -> bool { 4 | let m = m.as_ref(); 5 | !m.chars().any(|c| c.is_control()) && !m.is_empty() 6 | } 7 | 8 | /// Checks length and characters 9 | #[inline] 10 | pub fn verify_username>(u: T) -> bool { 11 | let u = u.as_ref(); 12 | !((u.len() > 18) || u.is_empty() || u.chars().any(|c| !c.is_alphanumeric())) 13 | } 14 | --------------------------------------------------------------------------------