├── .gitignore ├── test_data ├── cert_key.crt └── cert.crt ├── Cargo.toml ├── src ├── help.rs ├── main.rs ├── utils.rs ├── state │ └── mod.rs └── config.rs ├── README.md ├── config-example.toml └── COPYING /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | *~ 4 | -------------------------------------------------------------------------------- /test_data/cert_key.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgk9WMwfKng5i4gmjq 3 | lgCZcf5/VGOs9Om/VkSxmklPrlmhRANCAARSuf8inC7Cn3KgPwZ0O+/0IH5/bXt6 4 | rT+CyzT28LUTAhnWz0fYvh88tc0LLgPlUtlkbTCWJnlinZkdfwtL8a52 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test_data/cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBZzCCAQ6gAwIBAgIJAOl4KwrWAc0+MAoGCCqGSM49BAMCMCExHzAdBgNVBAMM 3 | FnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAx 4 | MDEwMDAwMDBaMCExHzAdBgNVBAMMFnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwWTAT 5 | BgcqhkjOPQIBBggqhkjOPQMBBwNCAARSuf8inC7Cn3KgPwZ0O+/0IH5/bXt6rT+C 6 | yzT28LUTAhnWz0fYvh88tc0LLgPlUtlkbTCWJnlinZkdfwtL8a52oy0wKzApBgNV 7 | HREEIjAgghNoZWxsby53b3JsZC5leGFtcGxlgglsb2NhbGhvc3QwCgYIKoZIzj0E 8 | AwIDRwAwRAIgPlVUc13K0X/A4hRHBVrI9yB7oG4kmcdXKfUNxkRm0AsCIHFxK8XW 9 | hxNYL9B8v9JT6qBxz/YVxWFd7AAmhSjKolR9 10 | -----END CERTIFICATE----- 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-irc-server" 3 | authors = ["Mateusz Szpakowski"] 4 | license = "LGPL-2.1-or-later" 5 | version = "0.1.8" 6 | edition = "2018" 7 | rust-version = "1.57" 8 | description = "Simple IRC server" 9 | repository = "https://github.com/matszpk/simple-irc-server" 10 | categories = ["network-programming"] 11 | keywords = ["irc"] 12 | 13 | [dependencies] 14 | tokio = { version = "1.0", features = [ "full" ] } 15 | tokio-util = { version = "0.7.0", features = [ "codec" ] } 16 | tokio-stream = "0.1" 17 | tracing = "0.1" 18 | futures = "0.3.0" 19 | toml = "0.5" 20 | serde = "1.0.0" 21 | serde_derive = "1.0.0" 22 | chrono = { version = "0.4", features = ["serde"] } 23 | clap = { version = "3.0.0", features = ["derive"] } 24 | validator = { version = "0.14", features = [ "derive" ] } 25 | bytes = "1.1.0" 26 | const-table = "0.1.0" 27 | flagset = "0.4" 28 | argon2 = "0.4" 29 | rpassword = "6.0" 30 | tokio-rustls = { version = "0.23", optional = true } 31 | rustls = { version = "0.20", optional = true } 32 | rustls-pemfile = { version = "1.0", optional = true } 33 | tokio-openssl = { version = "0.6", optional = true } 34 | openssl = { version = "0.10", optional = true } 35 | lazy_static = "1.4" 36 | trust-dns-resolver = { version = "0.21", optional = true } 37 | 38 | [features] 39 | default = [] 40 | # prefer rustls for almost systems 41 | tls_rustls = [ "rustls", "tokio-rustls", "rustls-pemfile" ] 42 | # use openssl if rustls with ring doesn't work - for example old non-SSE2 machines 43 | tls_openssl = [ "openssl", "tokio-openssl" ] 44 | dns_lookup = [ "trust-dns-resolver" ] 45 | 46 | [dependencies.tracing-subscriber] 47 | version = "0.3.1" 48 | default-features = false 49 | features = ["fmt", "ansi", "env-filter", "tracing-log"] 50 | 51 | [dev-dependencies] 52 | tokio = { version = "1.0", features = [ "full", "test-util" ] } 53 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | // srv_query_cmds.rs - main state 2 | // 3 | // simple-irc-server - simple IRC server 4 | // Copyright (C) 2022 Mateusz Szpakowski 5 | // 6 | // This library is free software; you can redistribute it and/or 7 | // modify it under the terms of the GNU Lesser General Public 8 | // License as published by the Free Software Foundation; either 9 | // version 2.1 of the License, or (at your option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // Lesser General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public 17 | // License along with this library; if not, write to the Free Software 18 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | // help topics list with its content. 21 | pub(crate) static HELP_TOPICS: [(&str, &str); 2] = [ 22 | ( 23 | "COMMANDS", 24 | r##"List of commands: 25 | ADMIN 26 | AUTHENTICATE - unsupported 27 | AWAY 28 | CAP 29 | CONNECT - unsupported 30 | DIE 31 | HELP 32 | INFO 33 | INVITE 34 | ISON 35 | JOIN 36 | KICK 37 | KILL 38 | LINKS 39 | LIST 40 | LUSERS 41 | MODE 42 | MOTD 43 | NAMES 44 | NICK 45 | NOTICE 46 | OPER 47 | PART 48 | PASS 49 | PING 50 | PONG 51 | PRIVMSG 52 | QUIT 53 | REHASH 54 | RESTART 55 | SQUIT 56 | STATS 57 | TIME 58 | TOPIC 59 | USER 60 | USERHOST 61 | VERSION 62 | WALLOPS 63 | WHO 64 | WHOIS 65 | WHOWAS"##, 66 | ), 67 | ( 68 | "MAIN", 69 | r##"This is Simple IRC Server. 70 | Use 'HELP COMMANDS' to list of commands. 71 | If you want get HELP about commands please refer to https://modern.ircdocs.horse/ 72 | or https://datatracker.ietf.org/doc/html/rfc1459."##, 73 | ), 74 | ]; 75 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // main.rs - main program 2 | // 3 | // simple-irc-server - simple IRC server 4 | // Copyright (C) 2022 Mateusz Szpakowski 5 | // 6 | // This library is free software; you can redistribute it and/or 7 | // modify it under the terms of the GNU Lesser General Public 8 | // License as published by the Free Software Foundation; either 9 | // version 2.1 of the License, or (at your option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // Lesser General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public 17 | // License along with this library; if not, write to the Free Software 18 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | mod command; 21 | mod config; 22 | mod help; 23 | mod reply; 24 | mod state; 25 | mod utils; 26 | 27 | use clap::Parser; 28 | use rpassword::prompt_password; 29 | use std::error::Error; 30 | 31 | use command::*; 32 | use config::*; 33 | use state::*; 34 | use utils::*; 35 | 36 | #[tokio::main] 37 | async fn main() -> Result<(), Box> { 38 | let cli = Cli::parse(); 39 | if cli.gen_password_hash { 40 | let password = if let Some(pwd) = cli.password { 41 | pwd 42 | } else { 43 | prompt_password("Enter password:")? 44 | }; 45 | println!("Password Hash: {}", argon2_hash_password(&password)); 46 | } else { 47 | let config = MainConfig::new(cli)?; 48 | initialize_logging(&config); 49 | // get handle of server 50 | let (_, handle) = run_server(config).await?; 51 | // and await for end 52 | handle.await?; 53 | } 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple IRC Server 2 | 3 | [![LGPL 2.1 License](https://img.shields.io/badge/License-LGPL--2.1-brightgreen)](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) 4 | [![Crate version](https://img.shields.io/crates/v/simple-irc-server)](https://crates.io/crates/simple-irc-server) 5 | 6 | This is a simple IRC server written in the Rust languange. This server supports 7 | basic set of IRC commands and basic user's mode and channel's modes. 8 | This server have following features: 9 | 10 | * an asynchronous design in the Tokio framework. 11 | * a simple configuration in TOML format. 12 | * a password hashing thanks Argon2 password hash. 13 | * ability to predefine users, operators and channels. 14 | * a domain name lookup for client hosts. 15 | * TLS connections support thanks RusTLS and OpenSSL. 16 | 17 | Because it is simple IRC server, unfortunatelly some commands have not been supported like: 18 | CONNECT, REHASH, RESTART, AUTHENTICATE. This server should be used to simple 19 | installation only with local IRC environment. 20 | 21 | ## Build 22 | 23 | A server can be easily build with Cargo package system. Following features can be enabled: 24 | 25 | * dns_lookup - enable DNS lookup, it is uses Trust DNS resolver package. 26 | * tls_rustls - enable TLS connection support. 27 | * tls_openssl - enable TLS connection support thanks native OpenSSL library. 28 | 29 | A tls_openssl should be used in old machines that doesn't support SSE2 instructions. 30 | A rustls uses 'ring' crate that need newer instruction set in X86 processors. 31 | 32 | To build server with full support just enter: 33 | 34 | ``` 35 | cargo build --release --features=dns_lookup,tls_rustls 36 | ``` 37 | 38 | or 39 | 40 | ``` 41 | cargo build --release --features=dns_lookup,tls_openssl 42 | ``` 43 | 44 | You can build server without DNS lookup and TLS support by using simple command: 45 | 46 | ``` 47 | cargo build --release 48 | ``` 49 | 50 | To increase security you can specify environment variable PASSWORD_SALT during building 51 | to provide own salt for Argon2 password hashing. It can be just some text. 52 | 53 | ## Configuration 54 | 55 | The sample configuration is in config-example.toml that describes any field configuration. 56 | Passwords should be filled by password hashes generated by using command: 57 | 58 | ``` 59 | simple-irc-server -g 60 | ``` 61 | 62 | or 63 | 64 | ``` 65 | simple-irc-server -g -P password 66 | ``` 67 | 68 | Text after `Password hash:` should be put into password field. 69 | 70 | ## Run 71 | 72 | You can run server by using command: 73 | 74 | ``` 75 | simple-irc-server -c your-config-file.toml 76 | ``` 77 | 78 | An IRC server can prints some logs to terminal output if no log file specified 79 | in configuration. 80 | -------------------------------------------------------------------------------- /config-example.toml: -------------------------------------------------------------------------------- 1 | # Name of your IRC server. 2 | name = "irci.localhost" 3 | # Administrative info 1 about your server. 4 | admin_info = "IRCI is local IRC server" 5 | # Administrative info 2 about your server. 6 | admin_info2 = "IRCI is good server" 7 | # Info about your server. 8 | info = "This is IRCI server" 9 | # Address from your server is listening connections. 10 | listen = "127.0.0.1" 11 | # TCP port from your server is listening connections. Typically can be 6667 or 12 | # for 6669 TLS connections. 13 | port = 6667 14 | # Optional password required to login to your IRC server. It should be password hash 15 | # by using server binary: './simple-irc-server -g [-P password]'. 16 | password = "kABc9xjQBSwgV2WfM02/AV8rQhEpTzRjn+fYC1x3ab0hRul9S9EkxGS/GMckLQjn0gYEEX3ISmXDfetwTUwhpQ" 17 | # Name of IRC network. 18 | network = "IRCInetwork" 19 | # Maximal number of acceptable connections. 20 | max_connections = 4000 21 | # Maximal number of channels that user can join. 22 | max_joins = 100 23 | # Ping timeout. Maximal time between consecutive PING's in secods. 24 | ping_timeout = 100 25 | # Pong timeout. Maximal time between PING and PONG in seconds. 26 | pong_timeout = 30 27 | # MOTD - Message of the Day. 28 | motd = "Hello, guys!" 29 | # DNS Lookup. If true then server try to get domain name of the client from DNS. 30 | dns_lookup = true 31 | # Minimal log level. Log Levels from lowest: 32 | # TRACE, DEBUG, INFO, WARN, ERROR. 33 | log_level = "INFO" 34 | # Optional. Log to specified file. 35 | log_file = "irc.log" 36 | 37 | # Optional. Set secure TLS connection. 38 | [tls] 39 | # Certificate file. 40 | cert_file = "cert.crt" 41 | # Certificate key file. 42 | cert_key_file = "cert_key.crt" 43 | 44 | # Default user's mode that will be given after log in. 45 | [default_user_modes] 46 | # Invisible mode. 47 | invisible = false 48 | # Operator mode. 49 | oper = false 50 | # Local operator mode. 51 | local_oper = false 52 | # Registered mode. 53 | registered = true 54 | # Wallops mode. 55 | wallops = false 56 | 57 | # Optional. List of operators 58 | [[operators]] 59 | # Name of operator. It should be used in OPER as the first parameter. 60 | name = "matszpk" 61 | # Required password to authenticate an operator. It should be password hash 62 | # by using server binary: './simple-irc-server -g [-P password]'. 63 | password = "xVfVq4pvFosvOQb0IVaPMkK22u0ZF2Ki7XB9yRsGbnEbL6WXhQCdpOwDV62HZ0MS5RjqmfrQJT7lV3aohUa3uw" 64 | # Optional. Source mask of source that will be checked for user source. 65 | # Source is combination of the nick, name and host. 66 | mask = "*!*@localhost" 67 | 68 | # Optional. List of registered users. 69 | [[users]] 70 | # Name of user. 71 | name = "matszpk" 72 | # Nick of user. 73 | nick = "matszpk" 74 | # Required password to authenticate an user. It should be password hash 75 | # by using server binary: './simple-irc-server -g [-P password]'. 76 | password = "xVfVq4pvFosvOQb0IVaPMkK22u0ZF2Ki7XB9yRsGbnEbL6WXhQCdpOwDV62HZ0MS5RjqmfrQJT7lV3aohUa3uw" 77 | # Optional. Source mask of source that will be checked for user source. 78 | # Source is combination of the nick, name and host. 79 | mask = "*!*@localhost" 80 | 81 | # Optional. List of preconfigured channels. 82 | [[channels]] 83 | # Name of channel. 84 | name = "#maintopic" 85 | # Optional. Topic of channel. 86 | topic = "Some topic" 87 | 88 | # Channel modes. 89 | [channels.modes] 90 | # Optional. List of bans. It is list of source mask to sources that will be banned. 91 | ban = [ "*!*@localhost" ] 92 | # Optional. List of ban exceptions. It is list of source mask to sources that will be ignored 93 | # during determining bans. 94 | excpetion = [ "*!*@localhost" ] 95 | # Optional. List of invite exceptions. It is list of source mask to sources that 96 | # will be ignored during determining who must have invitation. 97 | invite_exception = [ "*!*@localhost" ] 98 | # Optional. Channel key that required to join. 99 | key = "blabla" 100 | # Optional. List of founders. It is list of nicks. 101 | founders = [ "matszpk" ] 102 | # Optional. List of protected users. It is list of nicks. 103 | protecteds = [ "matszpk" ] 104 | # Optional. List of operators. It is list of nicks. 105 | operators = [ "matszpk" ] 106 | # Optional. List of half operators. It is list of nicks. 107 | half_operators = [ "matszpk" ] 108 | # Optional. List of voices. It is list of nicks. 109 | voices = [ "matszpk" ] 110 | # If true then channel is moderated and user need voice channel's mode. 111 | moderated = false 112 | # If true then user need invitation to join channel. 113 | invite_only = false 114 | # If true then channel is secret. 115 | secret = false 116 | # If true then channel have protected topic. 117 | protected_topic = false 118 | # If true then nobody outside channel can send message to channel. 119 | no_external_messages = true 120 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // utils.rs - commands 2 | // 3 | // simple-irc-server - simple IRC server 4 | // Copyright (C) 2022 Mateusz Szpakowski 5 | // 6 | // This library is free software; you can redistribute it and/or 7 | // modify it under the terms of the GNU Lesser General Public 8 | // License as published by the Free Software Foundation; either 9 | // version 2.1 of the License, or (at your option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // Lesser General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public 17 | // License along with this library; if not, write to the Free Software 18 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | use argon2::password_hash; 21 | use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; 22 | use argon2::{self, Argon2}; 23 | use bytes::{BufMut, BytesMut}; 24 | use futures::task::{Context, Poll}; 25 | use futures::{SinkExt, Stream}; 26 | use lazy_static::lazy_static; 27 | use std::convert::TryFrom; 28 | use std::error::Error; 29 | use std::io; 30 | use std::pin::Pin; 31 | use tokio::io::ReadBuf; 32 | use tokio::io::{AsyncRead, AsyncWrite}; 33 | use tokio::net::TcpStream; 34 | #[cfg(feature = "tls_openssl")] 35 | use tokio_openssl::SslStream; 36 | #[cfg(feature = "tls_rustls")] 37 | use tokio_rustls::server::TlsStream; 38 | use tokio_util::codec::{Decoder, Encoder, Framed, LinesCodec, LinesCodecError}; 39 | use validator::ValidationError; 40 | 41 | use crate::command::CommandError; 42 | use crate::command::CommandError::*; 43 | use crate::command::CommandId::*; 44 | 45 | #[derive(Debug)] 46 | pub(crate) enum DualTcpStream { 47 | PlainStream(TcpStream), 48 | #[cfg(feature = "tls_rustls")] 49 | SecureStream(Box>), 50 | #[cfg(feature = "tls_openssl")] 51 | SecureStream(SslStream), 52 | } 53 | 54 | impl DualTcpStream { 55 | pub(crate) fn is_secure(&self) -> bool { 56 | !matches!(*self, DualTcpStream::PlainStream(_)) 57 | } 58 | } 59 | 60 | impl AsyncRead for DualTcpStream { 61 | fn poll_read( 62 | self: Pin<&mut Self>, 63 | cx: &mut Context<'_>, 64 | buf: &mut ReadBuf<'_>, 65 | ) -> Poll> { 66 | match self.get_mut() { 67 | DualTcpStream::PlainStream(ref mut t) => Pin::new(t).poll_read(cx, buf), 68 | #[cfg(any(feature = "tls_openssl", feature = "tls_rustls"))] 69 | DualTcpStream::SecureStream(ref mut t) => Pin::new(t).poll_read(cx, buf), 70 | } 71 | } 72 | } 73 | 74 | impl AsyncWrite for DualTcpStream { 75 | fn poll_write( 76 | self: Pin<&mut Self>, 77 | cx: &mut Context<'_>, 78 | buf: &[u8], 79 | ) -> Poll> { 80 | match self.get_mut() { 81 | DualTcpStream::PlainStream(ref mut t) => Pin::new(t).poll_write(cx, buf), 82 | #[cfg(any(feature = "tls_openssl", feature = "tls_rustls"))] 83 | DualTcpStream::SecureStream(ref mut t) => Pin::new(t).poll_write(cx, buf), 84 | } 85 | } 86 | 87 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 88 | match self.get_mut() { 89 | DualTcpStream::PlainStream(ref mut t) => Pin::new(t).poll_flush(cx), 90 | #[cfg(any(feature = "tls_openssl", feature = "tls_rustls"))] 91 | DualTcpStream::SecureStream(ref mut t) => Pin::new(t).poll_flush(cx), 92 | } 93 | } 94 | 95 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 96 | match self.get_mut() { 97 | DualTcpStream::PlainStream(ref mut t) => Pin::new(t).poll_shutdown(cx), 98 | #[cfg(any(feature = "tls_openssl", feature = "tls_rustls"))] 99 | DualTcpStream::SecureStream(ref mut t) => Pin::new(t).poll_shutdown(cx), 100 | } 101 | } 102 | } 103 | 104 | // BufferedStream - to avoid deadlocks if no immediately data sent 105 | #[derive(Debug)] 106 | pub(crate) struct BufferedLineStream { 107 | stream: Framed, 108 | buffer: Vec, 109 | } 110 | 111 | impl BufferedLineStream { 112 | pub(crate) fn new(stream: Framed) -> Self { 113 | BufferedLineStream { 114 | stream, 115 | buffer: vec![], 116 | } 117 | } 118 | 119 | pub(crate) async fn feed(&mut self, msg: String) -> Result<(), LinesCodecError> { 120 | self.buffer.push(msg); 121 | Ok(()) 122 | } 123 | 124 | pub(crate) async fn flush(&mut self) -> Result<(), LinesCodecError> { 125 | for msg in self.buffer.drain(..) { 126 | self.stream.feed(msg).await?; 127 | } 128 | self.stream.flush().await?; 129 | Ok(()) 130 | } 131 | 132 | pub(crate) fn get_ref(&self) -> &DualTcpStream { 133 | self.stream.get_ref() 134 | } 135 | } 136 | 137 | impl Stream for BufferedLineStream { 138 | type Item = Result; 139 | 140 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 141 | Pin::new(&mut self.get_mut().stream).poll_next(cx) 142 | } 143 | 144 | fn size_hint(&self) -> (usize, Option) { 145 | self.stream.size_hint() 146 | } 147 | } 148 | 149 | // special LinesCodec for IRC - encode with "\r\n". 150 | #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 151 | pub(crate) struct IRCLinesCodec(LinesCodec); 152 | 153 | impl IRCLinesCodec { 154 | pub(crate) fn new_with_max_length(max_length: usize) -> IRCLinesCodec { 155 | IRCLinesCodec(LinesCodec::new_with_max_length(max_length)) 156 | } 157 | } 158 | 159 | impl Encoder for IRCLinesCodec { 160 | type Error = LinesCodecError; 161 | 162 | fn encode(&mut self, line: String, buf: &mut BytesMut) -> Result<(), Self::Error> { 163 | buf.reserve(line.len() + 1); 164 | buf.put(line.as_bytes()); 165 | // put "\r\n" 166 | buf.put_u8(b'\r'); 167 | buf.put_u8(b'\n'); 168 | Ok(()) 169 | } 170 | } 171 | 172 | impl Decoder for IRCLinesCodec { 173 | type Item = String; 174 | type Error = LinesCodecError; 175 | 176 | fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { 177 | self.0.decode(buf) 178 | } 179 | } 180 | 181 | pub(crate) fn validate_source(s: &str) -> bool { 182 | if s.contains(':') { 183 | // if have ':' then is not source 184 | false 185 | } else { 186 | // must be in format nick[!username[@host]] 187 | let excl = s.find('!'); 188 | let atchar = s.find('@'); 189 | if let Some(excl_pos) = excl { 190 | if let Some(atchar_pos) = atchar { 191 | return excl_pos < atchar_pos; 192 | } 193 | } 194 | true 195 | } 196 | } 197 | 198 | pub(crate) fn validate_username(username: &str) -> Result<(), ValidationError> { 199 | if !username.is_empty() && (username.as_bytes()[0] == b'#' || username.as_bytes()[0] == b'&') { 200 | Err(ValidationError::new( 201 | "Username must not have channel prefix.", 202 | )) 203 | } else if !username.contains('.') && !username.contains(':') && !username.contains(',') { 204 | Ok(()) 205 | } else { 206 | Err(ValidationError::new( 207 | "Username must not contains '.', ',' or ':'.", 208 | )) 209 | } 210 | } 211 | 212 | pub(crate) fn validate_channel(channel: &str) -> Result<(), ValidationError> { 213 | if !channel.is_empty() 214 | && !channel.contains(':') 215 | && !channel.contains(',') 216 | && (channel.as_bytes()[0] == b'#' || channel.as_bytes()[0] == b'&') 217 | { 218 | Ok(()) 219 | } else { 220 | Err(ValidationError::new( 221 | "Channel name must have '#' or '&' at start and \ 222 | must not contains ',' or ':'.", 223 | )) 224 | } 225 | } 226 | 227 | pub(crate) fn validate_server(s: &str, e: E) -> Result<(), E> { 228 | if s.contains('.') { 229 | Ok(()) 230 | } else { 231 | Err(e) 232 | } 233 | } 234 | 235 | pub(crate) fn validate_server_mask(s: &str, e: E) -> Result<(), E> { 236 | if s.contains('.') | s.contains('*') { 237 | Ok(()) 238 | } else { 239 | Err(e) 240 | } 241 | } 242 | 243 | pub(crate) fn validate_prefixed_channel(channel: &str, e: E) -> Result<(), E> { 244 | if !channel.is_empty() && !channel.contains(':') && !channel.contains(',') { 245 | let mut is_channel = false; 246 | let mut last_amp = false; 247 | for (i, c) in channel.bytes().enumerate() { 248 | match c { 249 | b'~' | b'@' | b'%' | b'+' => (), 250 | b'&' => (), 251 | b'#' => { 252 | is_channel = i + 1 < channel.len(); 253 | break; 254 | } 255 | _ => { 256 | // if last special character is & - then local channel 257 | is_channel = last_amp; 258 | break; 259 | } 260 | } 261 | last_amp = c == b'&'; 262 | } 263 | if is_channel { 264 | Ok(()) 265 | } else { 266 | Err(e) 267 | } 268 | } else { 269 | Err(e) 270 | } 271 | } 272 | 273 | pub(crate) fn validate_usermodes<'a>( 274 | modes: &[(&'a str, Vec<&'a str>)], 275 | ) -> Result<(), CommandError> { 276 | let mut param_idx = 1; 277 | modes.iter().try_for_each(|(ms, margs)| { 278 | if !ms.is_empty() { 279 | if ms 280 | .find(|c| { 281 | c != '+' && c != '-' && c != 'i' && c != 'o' && c != 'O' && c != 'r' && c != 'w' 282 | }) 283 | .is_some() 284 | { 285 | Err(UnknownUModeFlag(param_idx)) 286 | } else if !margs.is_empty() { 287 | Err(WrongParameter(MODEId, param_idx)) 288 | } else { 289 | param_idx += 1; 290 | Ok(()) 291 | } 292 | } else { 293 | // if empty 294 | Err(WrongParameter(MODEId, param_idx)) 295 | } 296 | }) 297 | } 298 | 299 | pub(crate) fn validate_channelmodes<'a>( 300 | target: &'a str, 301 | modes: &[(&'a str, Vec<&'a str>)], 302 | ) -> Result<(), CommandError> { 303 | let mut param_idx = 1; 304 | modes.iter().try_for_each(|(ms, margs)| { 305 | if !ms.is_empty() { 306 | let mut mode_set = false; 307 | let mut arg_param_idx = param_idx + 1; 308 | 309 | let mut margs_it = margs.iter(); 310 | 311 | ms.chars().try_for_each(|c| { 312 | match c { 313 | '+' => { 314 | mode_set = true; 315 | } 316 | '-' => { 317 | mode_set = false; 318 | } 319 | 'b' | 'e' | 'I' => { 320 | margs_it.next(); // consume argument 321 | arg_param_idx += 1; 322 | } 323 | 'o' | 'v' | 'h' | 'q' | 'a' => { 324 | if let Some(arg) = margs_it.next() { 325 | validate_username(arg).map_err(|e| InvalidModeParam { 326 | target: target.to_string(), 327 | modechar: c, 328 | param: arg.to_string(), 329 | description: e.to_string(), 330 | })?; 331 | arg_param_idx += 1; 332 | } else { 333 | return Err(InvalidModeParam { 334 | target: target.to_string(), 335 | modechar: c, 336 | param: "".to_string(), 337 | description: "No argument".to_string(), 338 | }); 339 | } 340 | } 341 | 'l' => { 342 | if mode_set { 343 | if let Some(arg) = margs_it.next() { 344 | if let Err(e) = arg.parse::() { 345 | // if argument is not number, then error 346 | return Err(InvalidModeParam { 347 | target: target.to_string(), 348 | modechar: c, 349 | param: arg.to_string(), 350 | description: e.to_string(), 351 | }); 352 | } 353 | arg_param_idx += 1; 354 | } else { 355 | return Err(InvalidModeParam { 356 | target: target.to_string(), 357 | modechar: c, 358 | param: "".to_string(), 359 | description: "No argument".to_string(), 360 | }); 361 | } 362 | } else if let Some(arg) = margs_it.next() { 363 | return Err(InvalidModeParam { 364 | target: target.to_string(), 365 | modechar: c, 366 | param: arg.to_string(), 367 | description: "Unexpected argument".to_string(), 368 | }); 369 | } 370 | } 371 | 'k' => { 372 | if mode_set { 373 | if margs_it.next().is_some() { 374 | arg_param_idx += 1; 375 | } else { 376 | // no argument 377 | return Err(InvalidModeParam { 378 | target: target.to_string(), 379 | modechar: c, 380 | param: "".to_string(), 381 | description: "No argument".to_string(), 382 | }); 383 | } 384 | } else if let Some(arg) = margs_it.next() { 385 | return Err(InvalidModeParam { 386 | target: target.to_string(), 387 | modechar: c, 388 | param: arg.to_string(), 389 | description: "Unexpected argument".to_string(), 390 | }); 391 | } 392 | } 393 | 'i' | 'm' | 't' | 'n' | 's' => {} 394 | c => { 395 | return Err(UnknownMode(param_idx, c, target.to_string())); 396 | } 397 | } 398 | Ok(()) 399 | })?; 400 | 401 | param_idx += margs.len() + 1; 402 | 403 | Ok(()) 404 | } else { 405 | // if empty 406 | Err(WrongParameter(MODEId, param_idx)) 407 | } 408 | }) 409 | } 410 | 411 | fn starts_single_wilcards<'a>(pattern: &'a str, text: &'a str) -> bool { 412 | if pattern.len() <= text.len() { 413 | pattern 414 | .bytes() 415 | .enumerate() 416 | .all(|(i, c)| c == b'?' || c == text.as_bytes()[i]) 417 | } else { 418 | false 419 | } 420 | } 421 | 422 | pub(crate) fn match_wildcard<'a>(pattern: &'a str, text: &'a str) -> bool { 423 | let mut pat = pattern; 424 | let mut t = text; 425 | let mut asterisk = false; 426 | while !pat.is_empty() { 427 | let (newpat, m, cur_ast) = if let Some(i) = pat.find('*') { 428 | (&pat[i + 1..], &pat[..i], true) 429 | } else { 430 | (&pat[pat.len()..pat.len()], pat, false) 431 | }; 432 | 433 | if !m.is_empty() { 434 | if !asterisk { 435 | // if first match 436 | if !starts_single_wilcards(m, t) { 437 | return false; 438 | } 439 | t = &t[m.len()..]; 440 | } else if cur_ast || !newpat.is_empty() { 441 | // after asterisk. only if some rest in pattern and 442 | // if last current character is asterisk 443 | let mut i = 0; 444 | // find first single wildcards occurrence. 445 | while i <= t.len() - m.len() && !starts_single_wilcards(m, &t[i..]) { 446 | i += 1; 447 | } 448 | if i <= t.len() - m.len() { 449 | // if found 450 | t = &t[i + m.len()..]; 451 | } else { 452 | return false; 453 | } 454 | } else { 455 | // if last pattern is not asterisk 456 | if !starts_single_wilcards(m, &t[t.len() - m.len()..]) { 457 | return false; 458 | } 459 | t = &t[t.len()..t.len()]; 460 | } 461 | } 462 | 463 | asterisk = true; 464 | pat = newpat; 465 | } 466 | // if last character in pattern is '*' or text has been fully consumed 467 | (!pattern.is_empty() && pattern.as_bytes()[pattern.len() - 1] == b'*') || t.is_empty() 468 | } 469 | 470 | // normalize source mask - for example '*' to '*!*@*' 471 | pub(crate) fn normalize_sourcemask(mask: &str) -> String { 472 | let mut out = String::new(); 473 | if let Some(p) = mask.find('!') { 474 | out += mask; // normalized 475 | if mask[p + 1..].find('@').is_none() { 476 | out += "@*"; 477 | } 478 | } else if let Some(p2) = mask.find('@') { 479 | out += &mask[..p2]; 480 | out += "!*"; 481 | out += &mask[p2..]; 482 | } else { 483 | out += mask; // normalized 484 | out += "!*@*"; 485 | } 486 | out 487 | } 488 | 489 | // argon2 490 | 491 | static ARGON2_M_COST: u32 = 2048; 492 | static ARGON2_T_COST: u32 = 2; 493 | static ARGON2_P_COST: u32 = 1; 494 | static ARGON2_OUT_LEN: usize = 64; 495 | 496 | lazy_static! { 497 | static ref ARGON2_SALT: SaltString = SaltString::b64_encode( 498 | option_env!("PASSWORD_SALT") 499 | .unwrap_or("br8f4efc3F4heecdsdS") 500 | .as_bytes() 501 | ) 502 | .unwrap(); 503 | static ref ARGON2: Argon2<'static> = Argon2::new( 504 | argon2::Algorithm::Argon2id, 505 | argon2::Version::V0x13, 506 | argon2::Params::new( 507 | ARGON2_M_COST, 508 | ARGON2_T_COST, 509 | ARGON2_P_COST, 510 | Some(ARGON2_OUT_LEN) 511 | ) 512 | .unwrap() 513 | ); 514 | } 515 | 516 | pub(crate) fn argon2_hash_password(password: &str) -> String { 517 | ARGON2 518 | .hash_password(password.as_bytes(), ARGON2_SALT.as_str()) 519 | .unwrap() 520 | .hash 521 | .unwrap() 522 | .to_string() 523 | } 524 | 525 | pub(crate) fn argon2_verify_password<'a>( 526 | password: &'a str, 527 | hash_str: &'a str, 528 | ) -> password_hash::errors::Result<()> { 529 | let password_hash = PasswordHash { 530 | algorithm: argon2::Algorithm::Argon2id.ident(), 531 | version: Some(argon2::Version::V0x13.into()), 532 | params: password_hash::ParamsString::try_from(ARGON2.params()).unwrap(), 533 | salt: Some(ARGON2_SALT.as_salt()), 534 | hash: Some(password_hash::Output::b64_decode(hash_str)?), 535 | }; 536 | ARGON2.verify_password(password.as_bytes(), &password_hash) 537 | } 538 | 539 | pub(crate) async fn argon2_verify_password_async( 540 | password: String, 541 | hash_str: String, 542 | ) -> password_hash::errors::Result<()> { 543 | tokio::task::spawn_blocking(move || argon2_verify_password(&password, &hash_str)) 544 | .await 545 | .unwrap() 546 | } 547 | 548 | pub(crate) fn validate_password_hash(hash_str: &str) -> Result<(), ValidationError> { 549 | match password_hash::Output::b64_decode(hash_str) { 550 | Ok(o) => { 551 | if o.len() == ARGON2_OUT_LEN { 552 | Ok(()) 553 | } else { 554 | Err(ValidationError::new("Wrong password hash length")) 555 | } 556 | } 557 | Err(_) => Err(ValidationError::new("Wrong base64 password hash")), 558 | } 559 | } 560 | 561 | #[cfg(test)] 562 | mod test { 563 | use super::*; 564 | 565 | #[test] 566 | fn test_irc_lines_codec() { 567 | let mut codec = IRCLinesCodec::new_with_max_length(2000); 568 | let mut buf = BytesMut::new(); 569 | codec.encode("my line".to_string(), &mut buf).unwrap(); 570 | assert_eq!("my line\r\n".as_bytes(), buf); 571 | let mut buf = BytesMut::from("my line 2\n"); 572 | assert_eq!( 573 | codec.decode(&mut buf).map_err(|e| e.to_string()), 574 | Ok(Some("my line 2".to_string())) 575 | ); 576 | assert_eq!(buf, BytesMut::new()); 577 | let mut buf = BytesMut::from("my line 2\r\n"); 578 | assert_eq!( 579 | codec.decode(&mut buf).map_err(|e| e.to_string()), 580 | Ok(Some("my line 2".to_string())) 581 | ); 582 | assert_eq!(buf, BytesMut::new()); 583 | } 584 | 585 | #[test] 586 | fn test_validate_source() { 587 | assert_eq!(true, validate_source("bob!bobby@host.com")); 588 | assert_eq!(true, validate_source("bobby@host.com")); 589 | assert_eq!(true, validate_source("bob!bobby")); 590 | assert_eq!(true, validate_source("host.com")); 591 | assert_eq!(false, validate_source("bob@bobby!host.com")); 592 | } 593 | 594 | #[test] 595 | fn test_validate_username() { 596 | assert_eq!(true, validate_username("ala").is_ok()); 597 | assert_eq!(false, validate_username("#ala").is_ok()); 598 | assert_eq!(false, validate_username("&ala").is_ok()); 599 | assert_eq!(false, validate_username("a.la").is_ok()); 600 | assert_eq!(false, validate_username("a,la").is_ok()); 601 | assert_eq!(false, validate_username("aL:a").is_ok()); 602 | } 603 | 604 | #[test] 605 | fn test_validate_channel() { 606 | assert_eq!(true, validate_channel("#ala").is_ok()); 607 | assert_eq!(true, validate_channel("&ala").is_ok()); 608 | assert_eq!(false, validate_channel("&al:a").is_ok()); 609 | assert_eq!(false, validate_channel("&al,a").is_ok()); 610 | assert_eq!(false, validate_channel("#al:a").is_ok()); 611 | assert_eq!(false, validate_channel("#al,a").is_ok()); 612 | assert_eq!(false, validate_channel("ala").is_ok()); 613 | } 614 | 615 | #[test] 616 | fn test_validate_server() { 617 | assert_eq!( 618 | true, 619 | validate_server("somebody.org", WrongParameter(PINGId, 0)).is_ok() 620 | ); 621 | assert_eq!( 622 | false, 623 | validate_server("somebodyorg", WrongParameter(PINGId, 0)).is_ok() 624 | ); 625 | } 626 | 627 | #[test] 628 | fn test_validate_server_mask() { 629 | assert_eq!( 630 | true, 631 | validate_server_mask("somebody.org", WrongParameter(PINGId, 0)).is_ok() 632 | ); 633 | assert_eq!( 634 | true, 635 | validate_server_mask("*org", WrongParameter(PINGId, 0)).is_ok() 636 | ); 637 | assert_eq!( 638 | false, 639 | validate_server_mask("somebodyorg", WrongParameter(PINGId, 0)).is_ok() 640 | ); 641 | } 642 | 643 | #[test] 644 | fn test_validate_prefixed_channel() { 645 | assert_eq!( 646 | true, 647 | validate_prefixed_channel("#ala", WrongParameter(PINGId, 0)).is_ok() 648 | ); 649 | assert_eq!( 650 | true, 651 | validate_prefixed_channel("&ala", WrongParameter(PINGId, 0)).is_ok() 652 | ); 653 | assert_eq!( 654 | false, 655 | validate_prefixed_channel("&al:a", WrongParameter(PINGId, 0)).is_ok() 656 | ); 657 | assert_eq!( 658 | false, 659 | validate_prefixed_channel("&al,a", WrongParameter(PINGId, 0)).is_ok() 660 | ); 661 | assert_eq!( 662 | false, 663 | validate_prefixed_channel("#al:a", WrongParameter(PINGId, 0)).is_ok() 664 | ); 665 | assert_eq!( 666 | false, 667 | validate_prefixed_channel("#al,a", WrongParameter(PINGId, 0)).is_ok() 668 | ); 669 | assert_eq!( 670 | false, 671 | validate_prefixed_channel("ala", WrongParameter(PINGId, 0)).is_ok() 672 | ); 673 | 674 | assert_eq!( 675 | true, 676 | validate_prefixed_channel("~#ala", WrongParameter(PINGId, 0)).is_ok() 677 | ); 678 | assert_eq!( 679 | true, 680 | validate_prefixed_channel("+#ala", WrongParameter(PINGId, 0)).is_ok() 681 | ); 682 | assert_eq!( 683 | true, 684 | validate_prefixed_channel("%#ala", WrongParameter(PINGId, 0)).is_ok() 685 | ); 686 | assert_eq!( 687 | true, 688 | validate_prefixed_channel("&#ala", WrongParameter(PINGId, 0)).is_ok() 689 | ); 690 | assert_eq!( 691 | true, 692 | validate_prefixed_channel("@#ala", WrongParameter(PINGId, 0)).is_ok() 693 | ); 694 | assert_eq!( 695 | true, 696 | validate_prefixed_channel("~&ala", WrongParameter(PINGId, 0)).is_ok() 697 | ); 698 | assert_eq!( 699 | true, 700 | validate_prefixed_channel("+&ala", WrongParameter(PINGId, 0)).is_ok() 701 | ); 702 | assert_eq!( 703 | true, 704 | validate_prefixed_channel("%&ala", WrongParameter(PINGId, 0)).is_ok() 705 | ); 706 | assert_eq!( 707 | true, 708 | validate_prefixed_channel("&&ala", WrongParameter(PINGId, 0)).is_ok() 709 | ); 710 | assert_eq!( 711 | true, 712 | validate_prefixed_channel("@&ala", WrongParameter(PINGId, 0)).is_ok() 713 | ); 714 | assert_eq!( 715 | false, 716 | validate_prefixed_channel("*#ala", WrongParameter(PINGId, 0)).is_ok() 717 | ); 718 | assert_eq!( 719 | false, 720 | validate_prefixed_channel("*&ala", WrongParameter(PINGId, 0)).is_ok() 721 | ); 722 | } 723 | 724 | #[test] 725 | fn test_validate_usermodes() { 726 | assert_eq!( 727 | Ok(()), 728 | validate_usermodes(&vec![("+io-rw", vec![]), ("-O", vec![])]) 729 | .map_err(|e| e.to_string()) 730 | ); 731 | assert_eq!( 732 | Ok(()), 733 | validate_usermodes(&vec![("+io", vec![]), ("-rO", vec![]), ("-w", vec![])]) 734 | .map_err(|e| e.to_string()) 735 | ); 736 | assert_eq!( 737 | Err("Wrong parameter 1 in command 'MODE'".to_string()), 738 | validate_usermodes(&vec![("+io-rw", vec!["xx"]), ("-O", vec![])]) 739 | .map_err(|e| e.to_string()) 740 | ); 741 | assert_eq!( 742 | Err("Unknown umode flag in parameter 2".to_string()), 743 | validate_usermodes(&vec![("+io-rw", vec![]), ("-x", vec![])]) 744 | .map_err(|e| e.to_string()) 745 | ); 746 | } 747 | 748 | #[test] 749 | fn test_validate_channelmodes() { 750 | assert_eq!( 751 | Ok(()), 752 | validate_channelmodes("#xchan", &vec![("+nt", vec![]), ("-sm", vec![])]) 753 | .map_err(|e| e.to_string()) 754 | ); 755 | assert_eq!( 756 | Ok(()), 757 | validate_channelmodes( 758 | "#xchan", 759 | &vec![("+nlt", vec!["22"]), ("-s+km", vec!["xxyy"])] 760 | ) 761 | .map_err(|e| e.to_string()) 762 | ); 763 | assert_eq!( 764 | Ok(()), 765 | validate_channelmodes( 766 | "#xchan", 767 | &vec![("+ibl-h", vec!["*dudu.com", "22", "derek"])] 768 | ) 769 | .map_err(|e| e.to_string()) 770 | ); 771 | assert_eq!( 772 | Ok(()), 773 | validate_channelmodes("#xchan", &vec![("-nlt", vec![]), ("+s-km", vec![])]) 774 | .map_err(|e| e.to_string()) 775 | ); 776 | assert_eq!( 777 | Ok(()), 778 | validate_channelmodes( 779 | "#xchan", 780 | &vec![ 781 | ("+ot", vec!["barry"]), 782 | ("-nh", vec!["guru"]), 783 | ("+vm", vec!["jerry"]) 784 | ] 785 | ) 786 | .map_err(|e| e.to_string()) 787 | ); 788 | assert_eq!( 789 | Ok(()), 790 | validate_channelmodes( 791 | "#xchan", 792 | &vec![ 793 | ("-to", vec!["barry"]), 794 | ("+hn", vec!["guru"]), 795 | ("-mv", vec!["jerry"]) 796 | ] 797 | ) 798 | .map_err(|e| e.to_string()) 799 | ); 800 | assert_eq!( 801 | Ok(()), 802 | validate_channelmodes( 803 | "#xchan", 804 | &vec![ 805 | ("-tb", vec!["barry"]), 806 | ("+iI", vec!["guru"]), 807 | ("-es", vec!["eagle"]) 808 | ] 809 | ) 810 | .map_err(|e| e.to_string()) 811 | ); 812 | assert_eq!( 813 | Ok(()), 814 | validate_channelmodes( 815 | "#xchan", 816 | &vec![ 817 | ("+tb", vec!["barry"]), 818 | ("-iI", vec!["guru"]), 819 | ("+es", vec!["eagle"]) 820 | ] 821 | ) 822 | .map_err(|e| e.to_string()) 823 | ); 824 | assert_eq!( 825 | Ok(()), 826 | validate_channelmodes( 827 | "#xchan", 828 | &vec![ 829 | ("-to", vec!["barry"]), 830 | ("+an", vec!["guru"]), 831 | ("-mq", vec!["jerry"]) 832 | ] 833 | ) 834 | .map_err(|e| e.to_string()) 835 | ); 836 | assert_eq!( 837 | Err("Unknown mode u in parameter 2 for #xchan".to_string()), 838 | validate_channelmodes("#xchan", &vec![("+nt", vec![]), ("-sum", vec![])]) 839 | .map_err(|e| e.to_string()) 840 | ); 841 | assert_eq!( 842 | Err("Invalid mode parameter: #xchan l No argument".to_string()), 843 | validate_channelmodes("#xchan", &vec![("+nlt", vec![]), ("-s+km", vec!["xxyy"])]) 844 | .map_err(|e| e.to_string()) 845 | ); 846 | assert_eq!( 847 | Err( 848 | "Invalid mode parameter: #xchan v jer:ry Validation error: Username \ 849 | must not contains '.', ',' or ':'. [{}]" 850 | .to_string() 851 | ), 852 | validate_channelmodes( 853 | "#xchan", 854 | &vec![ 855 | ("+ot", vec!["barry"]), 856 | ("-nh", vec!["guru"]), 857 | ("+vm", vec!["jer:ry"]) 858 | ] 859 | ) 860 | .map_err(|e| e.to_string()) 861 | ); 862 | assert_eq!( 863 | Err( 864 | "Invalid mode parameter: #xchan h gu:ru Validation error: Username \ 865 | must not contains '.', ',' or ':'. [{}]" 866 | .to_string() 867 | ), 868 | validate_channelmodes( 869 | "#xchan", 870 | &vec![ 871 | ("+ot", vec!["barry"]), 872 | ("-nh", vec!["gu:ru"]), 873 | ("+vm", vec!["jerry"]) 874 | ] 875 | ) 876 | .map_err(|e| e.to_string()) 877 | ); 878 | assert_eq!( 879 | Err( 880 | "Invalid mode parameter: #xchan o b,arry Validation error: Username \ 881 | must not contains '.', ',' or ':'. [{}]" 882 | .to_string() 883 | ), 884 | validate_channelmodes( 885 | "#xchan", 886 | &vec![ 887 | ("+ot", vec!["b,arry"]), 888 | ("-nh", vec!["guru"]), 889 | ("+vm", vec!["jerry"]) 890 | ] 891 | ) 892 | .map_err(|e| e.to_string()) 893 | ); 894 | } 895 | 896 | #[test] 897 | fn test_match_wildcard() { 898 | assert!(match_wildcard("somebody", "somebody")); 899 | assert!(!match_wildcard("somebody", "somebady")); 900 | assert!(match_wildcard("s?meb?dy", "samebady")); 901 | assert!(!match_wildcard("s?mec?dy", "samebady")); 902 | assert!(!match_wildcard("somebody", "somebod")); 903 | assert!(!match_wildcard("somebody", "somebodyis")); 904 | assert!(match_wildcard("so*body", "somebody")); 905 | assert!(match_wildcard("so**body", "somebody")); 906 | assert!(match_wildcard("so*body", "sobody")); 907 | assert!(match_wildcard("so*body*", "sobody")); 908 | assert!(match_wildcard("*so*body*", "sobody")); 909 | assert!(!match_wildcard("so*body", "sbody")); 910 | assert!(!match_wildcard("*so*body*", "sbody")); 911 | assert!(match_wildcard("so*body", "something body")); 912 | assert!(match_wildcard("so*bo*", "somebody")); 913 | assert!(match_wildcard("*", "Alice and Others")); 914 | assert!(!match_wildcard("", "Alice and Others")); 915 | assert!(match_wildcard("", "")); 916 | assert!(match_wildcard("*", "")); 917 | assert!(match_wildcard("***", "")); 918 | assert!(match_wildcard("* and Others", "Alice and Others")); 919 | assert!(!match_wildcard("* and Others", "Alice and others")); 920 | assert!(!match_wildcard("* and Others", "Aliceand Others")); 921 | assert!(match_wildcard("* and *", "Alice and Others")); 922 | assert!(match_wildcard("*** and **", "Alice and Others")); 923 | assert!(!match_wildcard("* and *", "Aliceand Others")); 924 | assert!(!match_wildcard("* and *", "Alice andOthers")); 925 | assert!(!match_wildcard("*** and ***", "Aliceand Others")); 926 | assert!(!match_wildcard("*** and ***", "Alice andOthers")); 927 | assert!(match_wildcard("*?and *", "Aliceand Others")); 928 | assert!(match_wildcard("* and?*", "Alice andOthers")); 929 | assert!(!match_wildcard("*?and *", "Aliceund Others")); 930 | assert!(!match_wildcard("* and?*", "Alice undOthers")); 931 | assert!(match_wildcard("lu*na*Xna*Y", "lulu and nanaXnaY")); 932 | assert!(match_wildcard( 933 | "lu*Xlu*Wlu*Zlu*B", 934 | "lulululuYlululuXlululuWluluZluluAluluB" 935 | )); 936 | assert!(match_wildcard( 937 | "lu*?lu*?lu*?lu*?", 938 | "lulululuYlululuXlululuWluluZluluAluluB" 939 | )); 940 | assert!(match_wildcard( 941 | "*lu*Xlu*Wlu*Zlu*B*", 942 | "XXXlulululuYlululuXlululuWluluZluluAluluBlululu" 943 | )); 944 | assert!(match_wildcard("la*la", "labulabela")); 945 | assert!(!match_wildcard("la*la", "labulabele")); 946 | assert!(match_wildcard("la*la*la", "labulalabela")); 947 | assert!(!match_wildcard("la*la*la", "labulalabele")); 948 | assert!(match_wildcard("la*l?", "labulabela")); 949 | assert!(!match_wildcard("la*?a", "labulabele")); 950 | assert!(!match_wildcard("la*l?", "labulabeka")); 951 | assert!(match_wildcard("greg*@somehere*", "greg-guru@somehere.net")); 952 | assert!(match_wildcard("greg*@somehere*", "greg@@@@somehere@@@")); 953 | assert!(!match_wildcard("greg*@somehere*", "greg.somehere@@@")); 954 | } 955 | 956 | #[test] 957 | fn test_normalize_sourcemask() { 958 | assert_eq!("ax*!*bob*@*.com", &normalize_sourcemask("ax*!*bob*@*.com")); 959 | assert_eq!("ax*!*@*.com", &normalize_sourcemask("ax*@*.com")); 960 | assert_eq!("ax*!bo*@*", &normalize_sourcemask("ax*!bo*")); 961 | assert_eq!("*ax!*@*.com", &normalize_sourcemask("*ax@*.com")); 962 | assert_eq!("u*xn!b*o@*", &normalize_sourcemask("u*xn!b*o")); 963 | assert_eq!("*!*@*", &normalize_sourcemask("*")); 964 | assert_eq!("bob.com!*@*", &normalize_sourcemask("bob.com")); 965 | } 966 | 967 | #[test] 968 | fn test_test_argon2_verify_password() { 969 | let phash = argon2_hash_password("lalalaXX"); 970 | assert!(argon2_verify_password("lalalaXX", &phash).is_ok()); 971 | assert!(argon2_verify_password("lalalaXY", &phash).is_err()); 972 | } 973 | 974 | #[tokio::test] 975 | async fn test_argon2_verify_password_async() { 976 | let phash = argon2_hash_password("lalalaXX"); 977 | assert!( 978 | argon2_verify_password_async("lalalaXX".to_string(), phash.clone()) 979 | .await 980 | .is_ok() 981 | ); 982 | assert!(argon2_verify_password_async("lalalaXY".to_string(), phash) 983 | .await 984 | .is_err()); 985 | } 986 | 987 | #[test] 988 | fn test_validate_password_hash() { 989 | assert!(validate_password_hash( 990 | "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJ\ 991 | ntk8HEZDgh4ctFei3bqt1r0O1VIyOV7dL+w" 992 | ) 993 | .is_ok()); 994 | assert_eq!( 995 | Err("Validation error: Wrong password hash length [{}]".to_string()), 996 | validate_password_hash( 997 | "zXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJ\ 998 | ntk8HEZDgh4ctFei3bqt1r0O1VIyOV7dL+w" 999 | ) 1000 | .map_err(|e| e.to_string()) 1001 | ); 1002 | assert_eq!( 1003 | Err("Validation error: Wrong base64 password hash [{}]".to_string()), 1004 | validate_password_hash("xxxxxxxxx").map_err(|e| e.to_string()) 1005 | ); 1006 | } 1007 | } 1008 | -------------------------------------------------------------------------------- /src/state/mod.rs: -------------------------------------------------------------------------------- 1 | // mod.rs - main state 2 | // 3 | // simple-irc-server - simple IRC server 4 | // Copyright (C) 2022 Mateusz Szpakowski 5 | // 6 | // This library is free software; you can redistribute it and/or 7 | // modify it under the terms of the GNU Lesser General Public 8 | // License as published by the Free Software Foundation; either 9 | // version 2.1 of the License, or (at your option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // Lesser General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public 17 | // License along with this library; if not, write to the Free Software 18 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | use chrono::prelude::*; 21 | use futures::future::Fuse; 22 | #[cfg(feature = "dns_lookup")] 23 | use lazy_static::lazy_static; 24 | #[cfg(feature = "tls_openssl")] 25 | use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod}; 26 | use std::collections::HashMap; 27 | use std::error::Error; 28 | use std::fmt; 29 | use std::fs::File; 30 | use std::io; 31 | #[cfg(feature = "tls_rustls")] 32 | use std::io::BufReader; 33 | use std::net::{IpAddr, SocketAddr}; 34 | use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; 35 | use std::sync::Arc; 36 | use tokio::net::TcpListener; 37 | #[cfg(any(feature = "tls_rustls", feature = "tls_openssl"))] 38 | use tokio::net::TcpStream; 39 | use tokio::sync::{oneshot, RwLock}; 40 | use tokio::task::JoinHandle; 41 | #[cfg(feature = "tls_openssl")] 42 | use tokio_openssl::SslStream; 43 | #[cfg(feature = "tls_rustls")] 44 | use tokio_rustls::rustls::{Certificate, PrivateKey}; 45 | #[cfg(feature = "tls_rustls")] 46 | use tokio_rustls::TlsAcceptor; 47 | use tokio_stream::StreamExt; 48 | use tokio_util::codec::{Framed, LinesCodecError}; 49 | use tracing::*; 50 | #[cfg(feature = "dns_lookup")] 51 | use trust_dns_resolver::{TokioAsyncResolver, TokioHandle}; 52 | 53 | use crate::command::*; 54 | use crate::config::*; 55 | use crate::reply::*; 56 | use crate::utils::*; 57 | 58 | use Reply::*; 59 | 60 | mod structs; 61 | pub(crate) use structs::*; 62 | 63 | pub(crate) struct MainState { 64 | config: MainConfig, 65 | // key is user name 66 | user_config_idxs: HashMap, 67 | // key is oper name 68 | oper_config_idxs: HashMap, 69 | conns_count: Arc, 70 | state: RwLock, 71 | created: String, 72 | created_time: DateTime, 73 | command_counts: [AtomicU64; NUM_COMMANDS], 74 | } 75 | 76 | impl MainState { 77 | pub(crate) fn new_from_config(config: MainConfig) -> MainState { 78 | // create indexes for configured users and operators. 79 | let mut user_config_idxs = HashMap::new(); 80 | if let Some(ref users) = config.users { 81 | users.iter().enumerate().for_each(|(i, u)| { 82 | user_config_idxs.insert(u.name.clone(), i); 83 | }); 84 | } 85 | let mut oper_config_idxs = HashMap::new(); 86 | if let Some(ref opers) = config.operators { 87 | opers.iter().enumerate().for_each(|(i, o)| { 88 | oper_config_idxs.insert(o.name.clone(), i); 89 | }); 90 | } 91 | let state = RwLock::new(VolatileState::new_from_config(&config)); 92 | let now = Local::now(); 93 | MainState { 94 | config, 95 | user_config_idxs, 96 | oper_config_idxs, 97 | state, 98 | conns_count: Arc::new(AtomicUsize::new(0)), 99 | created: now.to_rfc2822(), 100 | created_time: now, 101 | command_counts: [ 102 | AtomicU64::new(0), 103 | AtomicU64::new(0), 104 | AtomicU64::new(0), 105 | AtomicU64::new(0), 106 | AtomicU64::new(0), 107 | AtomicU64::new(0), 108 | AtomicU64::new(0), 109 | AtomicU64::new(0), 110 | AtomicU64::new(0), 111 | AtomicU64::new(0), 112 | AtomicU64::new(0), 113 | AtomicU64::new(0), 114 | AtomicU64::new(0), 115 | AtomicU64::new(0), 116 | AtomicU64::new(0), 117 | AtomicU64::new(0), 118 | AtomicU64::new(0), 119 | AtomicU64::new(0), 120 | AtomicU64::new(0), 121 | AtomicU64::new(0), 122 | AtomicU64::new(0), 123 | AtomicU64::new(0), 124 | AtomicU64::new(0), 125 | AtomicU64::new(0), 126 | AtomicU64::new(0), 127 | AtomicU64::new(0), 128 | AtomicU64::new(0), 129 | AtomicU64::new(0), 130 | AtomicU64::new(0), 131 | AtomicU64::new(0), 132 | AtomicU64::new(0), 133 | AtomicU64::new(0), 134 | AtomicU64::new(0), 135 | AtomicU64::new(0), 136 | AtomicU64::new(0), 137 | AtomicU64::new(0), 138 | AtomicU64::new(0), 139 | AtomicU64::new(0), 140 | AtomicU64::new(0), 141 | AtomicU64::new(0), 142 | AtomicU64::new(0), 143 | ], 144 | } 145 | } 146 | 147 | fn count_command(&self, cmd: &Command) { 148 | self.command_counts[cmd.index()].fetch_add(1, Ordering::SeqCst); 149 | } 150 | 151 | // try to register connection state - print error if too many connections. 152 | pub(crate) fn register_conn_state( 153 | &self, 154 | ip_addr: IpAddr, 155 | stream: Framed, 156 | ) -> Option { 157 | if let Some(max_conns) = self.config.max_connections { 158 | // increment counter of connections count. 159 | if self.conns_count.fetch_add(1, Ordering::SeqCst) < max_conns { 160 | Some(ConnState::new(ip_addr, stream, self.conns_count.clone())) 161 | } else { 162 | self.conns_count.fetch_sub(1, Ordering::SeqCst); 163 | error!("Too many connections for IP {}", ip_addr); 164 | None 165 | } 166 | } else { 167 | self.conns_count.fetch_add(1, Ordering::SeqCst); 168 | Some(ConnState::new(ip_addr, stream, self.conns_count.clone())) 169 | } 170 | } 171 | 172 | pub(crate) async fn remove_user(&self, conn_state: &ConnState) { 173 | if let Some(ref nick) = conn_state.user_state.nick { 174 | let mut state = self.state.write().await; 175 | state.remove_user(nick); 176 | } 177 | } 178 | 179 | pub(crate) async fn process(&self, conn_state: &mut ConnState) -> Result<(), String> { 180 | // use conversion error to string to avoid problems with thread safety 181 | let res = self 182 | .process_internal(conn_state) 183 | .await 184 | .map_err(|e| e.to_string()); 185 | conn_state.stream.flush().await.map_err(|e| e.to_string())?; 186 | res 187 | } 188 | 189 | pub(crate) async fn get_quit_receiver(&self) -> Fuse> { 190 | let mut state = self.state.write().await; 191 | state.quit_receiver.take().unwrap() 192 | } 193 | 194 | async fn process_internal(&self, conn_state: &mut ConnState) -> Result<(), Box> { 195 | tokio::select! { 196 | Some(msg) = conn_state.receiver.recv() => { 197 | conn_state.stream.feed(msg).await?; 198 | Ok(()) 199 | }, 200 | Some(_) = conn_state.ping_receiver.recv() => { 201 | self.feed_msg(&mut conn_state.stream, "PING :LALAL").await?; 202 | conn_state.run_pong_timeout(&self.config); 203 | Ok(()) 204 | } 205 | Some(_) = conn_state.timeout_receiver.recv() => { 206 | info!("Pong timeout for {}", conn_state.user_state.source); 207 | self.feed_msg(&mut conn_state.stream, 208 | "ERROR :Pong timeout, connection will be closed.").await?; 209 | conn_state.quit.store(1, Ordering::SeqCst); 210 | Ok(()) 211 | } 212 | Ok((killer, comment)) = &mut conn_state.quit_receiver => { 213 | info!("User {} killed by {}: {}", conn_state.user_state.source, 214 | killer, comment); 215 | self.feed_msg(&mut conn_state.stream, 216 | format!("ERROR :User killed by {}: {}", killer, comment)).await?; 217 | conn_state.quit.store(1, Ordering::SeqCst); 218 | Ok(()) 219 | } 220 | Ok(hostname_opt) = &mut conn_state.dns_lookup_receiver => { 221 | #[cfg(feature = "dns_lookup")] 222 | if let Some(hostname) = hostname_opt { 223 | conn_state.user_state.set_hostname(hostname); 224 | if let Some(nick) = &conn_state.user_state.nick { 225 | let mut state = self.state.write().await; 226 | if let Some(user) = state.users.get_mut(nick) { 227 | user.update_hostname(&conn_state.user_state); 228 | } 229 | } 230 | } 231 | #[cfg(not(feature = "dns_lookup"))] 232 | info!("Unexpected dns lookup: {:?}", hostname_opt); 233 | Ok(()) 234 | } 235 | 236 | msg_str_res = conn_state.stream.next() => { 237 | let msg = match msg_str_res { 238 | Some(Ok(ref msg_str)) => { 239 | // try parse message from this line. 240 | match Message::from_shared_str(msg_str) { 241 | Ok(msg) => msg, 242 | Err(e) => { 243 | match e { 244 | MessageError::Empty => { 245 | return Ok(()) // ignore empties 246 | } 247 | MessageError::WrongSource => { 248 | self.feed_msg(&mut conn_state.stream, 249 | "ERROR :Wrong source").await?; 250 | } 251 | MessageError::NoCommand => { 252 | self.feed_msg(&mut conn_state.stream, 253 | "ERROR :No command supplied").await?; 254 | } 255 | } 256 | return Err(Box::new(e)); 257 | } 258 | } 259 | } 260 | // if line is longer than max line length. 261 | Some(Err(LinesCodecError::MaxLineLengthExceeded)) => { 262 | let client = conn_state.user_state.client_name(); 263 | self.feed_msg(&mut conn_state.stream, 264 | ErrInputTooLong417{ client }).await?; 265 | return Ok(()) 266 | }, 267 | Some(Err(e)) => return Err(Box::new(e)), 268 | // if end of stream 269 | None => { 270 | conn_state.quit.store(1, Ordering::SeqCst); 271 | return Err(Box::new(io::Error::new( 272 | io::ErrorKind::UnexpectedEof, "unexpected eof"))) 273 | } 274 | }; 275 | 276 | let cmd = match Command::from_message(&msg) { 277 | Ok(cmd) => cmd, 278 | // handle errors while parsing command. 279 | Err(e) => { 280 | use crate::CommandError::*; 281 | let client = conn_state.user_state.client_name(); 282 | match e { 283 | UnknownCommand(ref cmd_name) => { 284 | self.feed_msg(&mut conn_state.stream, 285 | ErrUnknownCommand421{ client, 286 | command: cmd_name }).await?; 287 | } 288 | UnknownSubcommand(_, _)|ParameterDoesntMatch(_, _)| 289 | WrongParameter(_, _) => { 290 | self.feed_msg(&mut conn_state.stream, 291 | format!("ERROR :{}", e)).await?; 292 | } 293 | NeedMoreParams(command) => { 294 | self.feed_msg(&mut conn_state.stream, 295 | ErrNeedMoreParams461{ client, 296 | command: command.name }).await?; 297 | } 298 | UnknownMode(_, modechar, ref channel) => { 299 | self.feed_msg(&mut conn_state.stream, 300 | ErrUnknownMode472{ client, 301 | modechar, channel }).await?; 302 | } 303 | UnknownUModeFlag(_) => { 304 | self.feed_msg(&mut conn_state.stream, 305 | ErrUmodeUnknownFlag501{ client }) 306 | .await?; 307 | } 308 | InvalidModeParam{ ref target, modechar, ref param, 309 | ref description } => { 310 | self.feed_msg(&mut conn_state.stream, 311 | ErrInvalidModeParam696{ client, 312 | target, modechar, param, description }).await?; 313 | } 314 | } 315 | return Err(Box::new(e)); 316 | } 317 | }; 318 | 319 | self.count_command(&cmd); 320 | 321 | use crate::Command::*; 322 | // if user not authenticated 323 | match cmd { 324 | CAP{ .. } | AUTHENTICATE{ } | PASS{ .. } | NICK{ .. } | 325 | USER{ .. } | QUIT{ } => {}, 326 | _ => { 327 | // expect CAP, AUTHENTICATE, PASS, NICK, USER, QUIT - 328 | // other commands need authenication. 329 | if !conn_state.user_state.authenticated { 330 | self.feed_msg(&mut conn_state.stream, ErrNotRegistered451{ 331 | client: conn_state.user_state.client_name() }).await?; 332 | return Ok(()) 333 | } 334 | } 335 | } 336 | 337 | match cmd { 338 | CAP{ subcommand, caps, version } => 339 | self.process_cap(conn_state, subcommand, caps, version).await, 340 | AUTHENTICATE{ } => 341 | self.process_authenticate(conn_state).await, 342 | PASS{ password } => 343 | self.process_pass(conn_state, password).await, 344 | NICK{ nickname } => 345 | self.process_nick(conn_state, nickname, &msg).await, 346 | USER{ username, hostname, servername, realname } => 347 | self.process_user(conn_state, username, hostname, 348 | servername, realname).await, 349 | PING{ token } => self.process_ping(conn_state, token).await, 350 | PONG{ token } => self.process_pong(conn_state, token).await, 351 | OPER{ name, password } => 352 | self.process_oper(conn_state, name, password).await, 353 | QUIT{ } => self.process_quit(conn_state).await, 354 | JOIN{ channels, keys } => 355 | self.process_join(conn_state, channels, keys).await, 356 | PART{ channels, reason } => 357 | self.process_part(conn_state, channels, reason).await, 358 | TOPIC{ channel, topic } => 359 | self.process_topic(conn_state, channel, topic, &msg).await, 360 | NAMES{ channels } => 361 | self.process_names(conn_state, channels).await, 362 | LIST{ channels, server } => 363 | self.process_list(conn_state, channels, server).await, 364 | INVITE{ nickname, channel } => 365 | self.process_invite(conn_state, nickname, channel, &msg).await, 366 | KICK{ channel, users, comment } => 367 | self.process_kick(conn_state, channel, users, comment).await, 368 | MOTD{ target } => 369 | self.process_motd(conn_state, target).await, 370 | VERSION{ target } => 371 | self.process_version(conn_state, target).await, 372 | ADMIN{ target } => 373 | self.process_admin(conn_state, target).await, 374 | CONNECT{ target_server, port, remote_server } => 375 | self.process_connect(conn_state, target_server, port, 376 | remote_server).await, 377 | LUSERS{ } => self.process_lusers(conn_state).await, 378 | TIME{ server } => 379 | self.process_time(conn_state, server).await, 380 | STATS{ query, server } => 381 | self.process_stats(conn_state, query, server).await, 382 | LINKS{ remote_server, server_mask } => 383 | self.process_links(conn_state, remote_server, server_mask).await, 384 | HELP{ subject } => 385 | self.process_help(conn_state, subject).await, 386 | INFO{ } => self.process_info(conn_state).await, 387 | MODE{ target, modes } => 388 | self.process_mode(conn_state, target, modes).await, 389 | PRIVMSG{ targets, text } => 390 | self.process_privmsg(conn_state, targets, text).await, 391 | NOTICE{ targets, text } => 392 | self.process_notice(conn_state, targets, text).await, 393 | WHO{ mask } => self.process_who(conn_state, mask).await, 394 | WHOIS{ target, nickmasks } => 395 | self.process_whois(conn_state, target, nickmasks).await, 396 | WHOWAS{ nickname, count, server } => 397 | self.process_whowas(conn_state, nickname, count, server).await, 398 | KILL{ nickname, comment } => 399 | self.process_kill(conn_state, nickname, comment).await, 400 | REHASH{ } => self.process_rehash(conn_state).await, 401 | RESTART{ } => self.process_restart(conn_state).await, 402 | SQUIT{ server, comment } => 403 | self.process_squit(conn_state, server, comment).await, 404 | AWAY{ text } => 405 | self.process_away(conn_state, text).await, 406 | USERHOST{ nicknames } => 407 | self.process_userhost(conn_state, nicknames).await, 408 | WALLOPS{ .. } => 409 | self.process_wallops(conn_state, &msg).await, 410 | ISON{ nicknames } => 411 | self.process_ison(conn_state, nicknames).await, 412 | DIE{ message } => 413 | self.process_die(conn_state, message).await, 414 | } 415 | }, 416 | } 417 | } 418 | 419 | // helper to feed messages 420 | async fn feed_msg( 421 | &self, 422 | stream: &mut BufferedLineStream, 423 | t: T, 424 | ) -> Result<(), LinesCodecError> { 425 | stream.feed(format!(":{} {}", self.config.name, t)).await 426 | } 427 | 428 | // helper to feed messages 429 | async fn feed_msg_source( 430 | &self, 431 | stream: &mut BufferedLineStream, 432 | source: &str, 433 | t: T, 434 | ) -> Result<(), LinesCodecError> { 435 | stream.feed(format!(":{} {}", source, t)).await 436 | } 437 | } 438 | 439 | // main process to handle commands from client. 440 | async fn user_state_process(main_state: Arc, stream: DualTcpStream, addr: SocketAddr) { 441 | let line_stream = Framed::new(stream, IRCLinesCodec::new_with_max_length(2000)); 442 | if let Some(mut conn_state) = main_state.register_conn_state(addr.ip(), line_stream) { 443 | #[cfg(feature = "dns_lookup")] 444 | if main_state.config.dns_lookup { 445 | conn_state.run_dns_lookup(); 446 | } 447 | #[cfg(not(feature = "dns_lookup"))] 448 | if main_state.config.dns_lookup { 449 | error!("DNS lookup is not enabled!"); 450 | } 451 | 452 | while !conn_state.is_quit() { 453 | if let Err(e) = main_state.process(&mut conn_state).await { 454 | error!("Error for {}: {}", conn_state.user_state.source, e); 455 | } 456 | } 457 | info!( 458 | "User {} gone from from server", 459 | conn_state.user_state.source 460 | ); 461 | main_state.remove_user(&conn_state).await; 462 | } 463 | } 464 | 465 | #[cfg(feature = "tls_rustls")] 466 | async fn user_state_process_tls( 467 | main_state: Arc, 468 | stream: TcpStream, 469 | acceptor: TlsAcceptor, 470 | addr: SocketAddr, 471 | ) { 472 | match acceptor.accept(stream).await { 473 | Ok(tls_stream) => { 474 | user_state_process( 475 | main_state, 476 | DualTcpStream::SecureStream(Box::new(tls_stream)), 477 | addr, 478 | ) 479 | .await 480 | } 481 | Err(e) => error!("Can't accept TLS connection: {}", e), 482 | } 483 | } 484 | 485 | #[cfg(feature = "tls_openssl")] 486 | async fn user_state_process_tls_prepare( 487 | stream: TcpStream, 488 | acceptor: Arc, 489 | ) -> Result, String> { 490 | let ssl = Ssl::new(acceptor.context()).map_err(|e| e.to_string())?; 491 | let mut tls_stream = SslStream::new(ssl, stream).map_err(|e| e.to_string())?; 492 | use std::pin::Pin; 493 | Pin::new(&mut tls_stream) 494 | .accept() 495 | .await 496 | .map_err(|e| e.to_string())?; 497 | Ok(tls_stream) 498 | } 499 | 500 | #[cfg(feature = "tls_openssl")] 501 | async fn user_state_process_tls( 502 | main_state: Arc, 503 | stream: TcpStream, 504 | acceptor: Arc, 505 | addr: SocketAddr, 506 | ) { 507 | match user_state_process_tls_prepare(stream, acceptor).await { 508 | Ok(stream) => { 509 | user_state_process(main_state, DualTcpStream::SecureStream(stream), addr).await 510 | } 511 | Err(e) => error!("Can't accept TLS connection: {}", e), 512 | }; 513 | } 514 | 515 | pub(crate) fn initialize_logging(config: &MainConfig) { 516 | use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; 517 | let s = tracing_subscriber::fmt() 518 | .with_env_filter(EnvFilter::from_default_env().add_directive(config.log_level.into())) 519 | .with_span_events(FmtSpan::FULL) 520 | .with_file(true) 521 | .with_line_number(true) 522 | .with_thread_ids(true) 523 | // disable ansi color for files 524 | .with_ansi(config.log_file.is_none()); 525 | if let Some(ref log_file) = config.log_file { 526 | if let Ok(f) = File::create(log_file) { 527 | s.with_writer(f).init(); 528 | } else { 529 | error!("No log file {}", log_file); 530 | s.init() 531 | } 532 | } else { 533 | s.init(); 534 | } 535 | } 536 | 537 | #[cfg(feature = "dns_lookup")] 538 | lazy_static! { 539 | static ref DNS_RESOLVER: std::sync::RwLock>> = 540 | std::sync::RwLock::new(None); 541 | } 542 | 543 | #[cfg(feature = "dns_lookup")] 544 | fn initialize_dns_resolver() { 545 | let mut r = DNS_RESOLVER.write().unwrap(); 546 | if r.is_none() { 547 | *r = Some(Arc::new( 548 | { 549 | // for windows or linux 550 | #[cfg(any(unix, windows))] 551 | { 552 | // use the system resolver configuration 553 | TokioAsyncResolver::from_system_conf(TokioHandle) 554 | } 555 | 556 | // for other 557 | #[cfg(not(any(unix, windows)))] 558 | { 559 | // Directly reference the config types 560 | use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; 561 | 562 | // Get a new resolver with the google nameservers as 563 | // the upstream recursive resolvers 564 | TokioAsyncResolver::tokio(ResolverConfig::google(), ResolverOpts::default()) 565 | } 566 | } 567 | .expect("failed to create resolver"), 568 | )); 569 | } 570 | } 571 | 572 | #[cfg(feature = "dns_lookup")] 573 | pub(self) fn dns_lookup(sender: oneshot::Sender>, ip: IpAddr) { 574 | let r = DNS_RESOLVER.read().unwrap(); 575 | let resolver = (*r).clone().unwrap(); 576 | tokio::spawn(dns_lookup_process(resolver, sender, ip)); 577 | } 578 | 579 | #[cfg(feature = "dns_lookup")] 580 | async fn dns_lookup_process( 581 | resolver: Arc, 582 | sender: oneshot::Sender>, 583 | ip: IpAddr, 584 | ) { 585 | let r = match resolver.reverse_lookup(ip).await { 586 | Ok(lookup) => { 587 | if let Some(x) = lookup.iter().next() { 588 | let namex = x.to_string(); 589 | let name = if namex.as_bytes()[namex.len() - 1] == b'.' { 590 | namex[..namex.len() - 1].to_string() 591 | } else { 592 | namex 593 | }; 594 | sender.send(Some(name)) 595 | } else { 596 | sender.send(None) 597 | } 598 | } 599 | Err(_) => sender.send(None), 600 | }; 601 | if r.is_err() { 602 | error!("Error while sending dns lookup"); 603 | } 604 | } 605 | 606 | // main routine to run server 607 | pub(crate) async fn run_server( 608 | config: MainConfig, 609 | ) -> Result<(Arc, JoinHandle<()>), Box> { 610 | #[cfg(feature = "dns_lookup")] 611 | if config.dns_lookup { 612 | initialize_dns_resolver(); 613 | } 614 | let listener = TcpListener::bind((config.listen, config.port)).await?; 615 | let cloned_tls = config.tls.clone(); 616 | let main_state = Arc::new(MainState::new_from_config(config)); 617 | let main_state_to_return = main_state.clone(); 618 | let handle = if cloned_tls.is_some() { 619 | #[cfg(feature = "tls_rustls")] 620 | { 621 | let config = { 622 | let tlsconfig = cloned_tls.unwrap(); 623 | let certs = 624 | rustls_pemfile::certs(&mut BufReader::new(File::open(tlsconfig.cert_file)?)) 625 | .map(|mut certs| certs.drain(..).map(Certificate).collect())?; 626 | let mut keys: Vec = rustls_pemfile::pkcs8_private_keys( 627 | &mut BufReader::new(File::open(tlsconfig.cert_key_file)?), 628 | ) 629 | .map(|mut keys| keys.drain(..).map(PrivateKey).collect())?; 630 | 631 | rustls::ServerConfig::builder() 632 | .with_safe_defaults() 633 | .with_no_client_auth() 634 | .with_single_cert(certs, keys.remove(0)) 635 | .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))? 636 | }; 637 | 638 | let acceptor = TlsAcceptor::from(Arc::new(config)); 639 | tokio::spawn(async move { 640 | let mut quit_receiver = main_state.get_quit_receiver().await; 641 | let mut do_quit = false; 642 | while !do_quit { 643 | tokio::select! { 644 | res = listener.accept() => { 645 | match res { 646 | Ok((stream, addr)) => { 647 | tokio::spawn(user_state_process_tls(main_state.clone(), 648 | stream, acceptor.clone(), addr)); 649 | } 650 | Err(e) => { error!("Accept connection error: {}", e); } 651 | }; 652 | } 653 | Ok(msg) = &mut quit_receiver => { 654 | info!("Server quit: {}", msg); 655 | do_quit = true; 656 | } 657 | }; 658 | } 659 | }) 660 | } 661 | 662 | #[cfg(feature = "tls_openssl")] 663 | { 664 | let tlsconfig = cloned_tls.unwrap(); 665 | let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; 666 | acceptor.set_private_key_file(tlsconfig.cert_key_file, SslFiletype::PEM)?; 667 | acceptor.set_certificate_chain_file(tlsconfig.cert_file)?; 668 | let acceptor = Arc::new(acceptor.build()); 669 | 670 | tokio::spawn(async move { 671 | let mut quit_receiver = main_state.get_quit_receiver().await; 672 | let mut do_quit = false; 673 | while !do_quit { 674 | tokio::select! { 675 | res = listener.accept() => { 676 | match res { 677 | Ok((stream, addr)) => { 678 | tokio::spawn(user_state_process_tls(main_state.clone(), 679 | stream, acceptor.clone(), addr)); 680 | } 681 | Err(e) => { error!("Accept connection error: {}", e); } 682 | }; 683 | } 684 | Ok(msg) = &mut quit_receiver => { 685 | info!("Server quit: {}", msg); 686 | do_quit = true; 687 | } 688 | }; 689 | } 690 | }) 691 | } 692 | 693 | #[cfg(not(any(feature = "tls_rustls", feature = "tls_openssl")))] 694 | tokio::spawn(async move { error!("Unsupported TLS") }) 695 | } else { 696 | tokio::spawn(async move { 697 | let mut quit_receiver = main_state.get_quit_receiver().await; 698 | let mut do_quit = false; 699 | while !do_quit { 700 | tokio::select! { 701 | res = listener.accept() => { 702 | match res { 703 | Ok((stream, addr)) => { 704 | tokio::spawn(user_state_process(main_state.clone(), 705 | DualTcpStream::PlainStream(stream), addr)); } 706 | Err(e) => { error!("Accept connection error: {}", e); } 707 | }; 708 | } 709 | Ok(msg) = &mut quit_receiver => { 710 | info!("Server quit: {}", msg); 711 | do_quit = true; 712 | } 713 | }; 714 | } 715 | }) 716 | }; 717 | Ok((main_state_to_return, handle)) 718 | } 719 | 720 | #[cfg(test)] 721 | mod test { 722 | use super::*; 723 | pub(crate) use futures::SinkExt; 724 | pub(crate) use std::collections::HashSet; 725 | pub(crate) use std::iter::FromIterator; 726 | pub(crate) use std::time::Duration; 727 | use tokio::net::TcpStream; 728 | pub(crate) use tokio::time; 729 | 730 | use std::sync::atomic::AtomicU16; 731 | 732 | static PORT_COUNTER: AtomicU16 = AtomicU16::new(7888); 733 | //use std::sync::Once; 734 | //static LOGGING_START: Once = Once::new(); 735 | 736 | pub(crate) async fn run_test_server( 737 | config: MainConfig, 738 | ) -> (Arc, JoinHandle<()>, u16) { 739 | //LOGGING_START.call_once(|| { 740 | // initialize_logging(&MainConfig::default()); 741 | //}); 742 | let mut config = config; 743 | config.port = PORT_COUNTER.fetch_add(1, Ordering::SeqCst); 744 | let port = config.port; 745 | let (main_state, handle) = run_server(config).await.unwrap(); 746 | (main_state, handle, port) 747 | } 748 | 749 | pub(crate) async fn quit_test_server(main_state: Arc, handle: JoinHandle<()>) { 750 | main_state 751 | .state 752 | .write() 753 | .await 754 | .quit_sender 755 | .take() 756 | .unwrap() 757 | .send("Test".to_string()) 758 | .unwrap(); 759 | handle.await.unwrap(); 760 | } 761 | 762 | pub(crate) async fn connect_to_test(port: u16) -> Framed { 763 | let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); 764 | Framed::new(stream, IRCLinesCodec::new_with_max_length(2000)) 765 | } 766 | 767 | pub(crate) async fn login_to_test<'a>( 768 | port: u16, 769 | nick: &'a str, 770 | name: &'a str, 771 | realname: &'a str, 772 | ) -> Framed { 773 | let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); 774 | let mut line_stream = Framed::new(stream, IRCLinesCodec::new_with_max_length(2000)); 775 | line_stream.send(format!("NICK {}", nick)).await.unwrap(); 776 | line_stream 777 | .send(format!("USER {} 8 * :{}", name, realname)) 778 | .await 779 | .unwrap(); 780 | line_stream 781 | } 782 | 783 | pub(crate) async fn login_to_test_and_skip<'a>( 784 | port: u16, 785 | nick: &'a str, 786 | name: &'a str, 787 | realname: &'a str, 788 | ) -> Framed { 789 | let mut line_stream = login_to_test(port, nick, name, realname).await; 790 | for _ in 0..18 { 791 | line_stream.next().await.unwrap().unwrap(); 792 | } 793 | line_stream 794 | } 795 | 796 | #[cfg(any(feature = "tls_rustls", feature = "openssl"))] 797 | use std::path::PathBuf; 798 | 799 | #[cfg(any(feature = "tls_rustls", feature = "openssl"))] 800 | fn get_cert_file_path() -> String { 801 | let mut path = PathBuf::new(); 802 | path.push(env!("CARGO_MANIFEST_DIR")); 803 | path.push("test_data"); 804 | path.push("cert.crt"); 805 | path.to_string_lossy().to_string() 806 | } 807 | 808 | #[cfg(any(feature = "tls_rustls", feature = "openssl"))] 809 | fn get_cert_key_file_path() -> String { 810 | let mut path = PathBuf::new(); 811 | path.push(env!("CARGO_MANIFEST_DIR")); 812 | path.push("test_data"); 813 | path.push("cert_key.crt"); 814 | path.to_string_lossy().to_string() 815 | } 816 | 817 | #[cfg(any(feature = "tls_rustls", feature = "openssl"))] 818 | pub(crate) async fn run_test_tls_server( 819 | config: MainConfig, 820 | ) -> (Arc, JoinHandle<()>, u16) { 821 | //LOGGING_START.call_once(|| { 822 | // initialize_logging(&MainConfig::default()); 823 | //}); 824 | let mut config = config; 825 | config.tls = Some(TLSConfig { 826 | cert_file: get_cert_file_path(), 827 | cert_key_file: get_cert_key_file_path(), 828 | }); 829 | config.port = PORT_COUNTER.fetch_add(1, Ordering::SeqCst); 830 | let port = config.port; 831 | let (main_state, handle) = run_server(config).await.unwrap(); 832 | (main_state, handle, port) 833 | } 834 | 835 | #[cfg(feature = "tls_rustls")] 836 | use std::convert::TryFrom; 837 | #[cfg(feature = "tls_rustls")] 838 | use tokio_rustls::TlsConnector; 839 | 840 | #[cfg(feature = "tls_rustls")] 841 | pub(crate) async fn connect_to_test_tls( 842 | port: u16, 843 | ) -> Framed, IRCLinesCodec> { 844 | let mut certs: Vec = rustls_pemfile::certs(&mut BufReader::new( 845 | File::open(get_cert_file_path()).unwrap(), 846 | )) 847 | .map(|mut certs| certs.drain(..).map(Certificate).collect()) 848 | .unwrap(); 849 | let dnsname = rustls::client::ServerName::try_from("localhost").unwrap(); 850 | 851 | let mut cert_store = rustls::RootCertStore { roots: vec![] }; 852 | cert_store.add(&certs.remove(0)).unwrap(); 853 | let config = Arc::new( 854 | rustls::ClientConfig::builder() 855 | .with_safe_defaults() 856 | .with_root_certificates(cert_store) 857 | .with_no_client_auth(), 858 | ); 859 | let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); 860 | Framed::new( 861 | TlsConnector::from(config) 862 | .connect(dnsname, stream) 863 | .await 864 | .unwrap(), 865 | IRCLinesCodec::new_with_max_length(2000), 866 | ) 867 | } 868 | 869 | #[cfg(feature = "tls_rustls")] 870 | pub(crate) async fn login_to_test_tls<'a>( 871 | port: u16, 872 | nick: &'a str, 873 | name: &'a str, 874 | realname: &'a str, 875 | ) -> Framed, IRCLinesCodec> { 876 | let mut line_stream = connect_to_test_tls(port).await; 877 | line_stream.send(format!("NICK {}", nick)).await.unwrap(); 878 | line_stream 879 | .send(format!("USER {} 8 * :{}", name, realname)) 880 | .await 881 | .unwrap(); 882 | line_stream 883 | } 884 | 885 | #[cfg(feature = "tls_rustls")] 886 | pub(crate) async fn login_to_test_tls_and_skip<'a>( 887 | port: u16, 888 | nick: &'a str, 889 | name: &'a str, 890 | realname: &'a str, 891 | ) -> Framed, IRCLinesCodec> { 892 | let mut line_stream = login_to_test_tls(port, nick, name, realname).await; 893 | for _ in 0..18 { 894 | line_stream.next().await.unwrap().unwrap(); 895 | } 896 | line_stream 897 | } 898 | 899 | #[cfg(feature = "tls_openssl")] 900 | use openssl::ssl::SslConnector; 901 | 902 | #[cfg(feature = "tls_openssl")] 903 | pub(crate) async fn connect_to_test_tls( 904 | port: u16, 905 | ) -> Framed, IRCLinesCodec> { 906 | let mut connector = SslConnector::builder(SslMethod::tls()).unwrap(); 907 | connector.set_ca_file(get_cert_file_path()).unwrap(); 908 | 909 | let ssl = connector 910 | .build() 911 | .configure() 912 | .unwrap() 913 | .into_ssl("localhost") 914 | .unwrap(); 915 | 916 | let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); 917 | let mut tls_stream = SslStream::new(ssl, stream).unwrap(); 918 | use std::pin::Pin; 919 | Pin::new(&mut tls_stream).connect().await.unwrap(); 920 | Framed::new(tls_stream, IRCLinesCodec::new_with_max_length(2000)) 921 | } 922 | 923 | #[cfg(feature = "tls_openssl")] 924 | pub(crate) async fn login_to_test_tls<'a>( 925 | port: u16, 926 | nick: &'a str, 927 | name: &'a str, 928 | realname: &'a str, 929 | ) -> Framed, IRCLinesCodec> { 930 | let mut line_stream = connect_to_test_tls(port).await; 931 | line_stream.send(format!("NICK {}", nick)).await.unwrap(); 932 | line_stream 933 | .send(format!("USER {} 8 * :{}", name, realname)) 934 | .await 935 | .unwrap(); 936 | line_stream 937 | } 938 | 939 | #[cfg(feature = "tls_openssl")] 940 | pub(crate) async fn login_to_test_tls_and_skip<'a>( 941 | port: u16, 942 | nick: &'a str, 943 | name: &'a str, 944 | realname: &'a str, 945 | ) -> Framed, IRCLinesCodec> { 946 | let mut line_stream = login_to_test_tls(port, nick, name, realname).await; 947 | for _ in 0..18 { 948 | line_stream.next().await.unwrap().unwrap(); 949 | } 950 | line_stream 951 | } 952 | 953 | #[tokio::test] 954 | async fn test_server_command0() { 955 | let (main_state, handle, port) = run_test_server(MainConfig::default()).await; 956 | 957 | { 958 | let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); 959 | let mut line_stream = Framed::new(stream, IRCLinesCodec::new_with_max_length(10000)); 960 | line_stream.send("POG :welcome".to_string()).await.unwrap(); 961 | assert_eq!( 962 | ":irc.irc 421 127.0.0.1 POG :Unknown command".to_string(), 963 | line_stream.next().await.unwrap().unwrap() 964 | ); 965 | line_stream.send("".to_string()).await.unwrap(); 966 | line_stream.send(" ".to_string()).await.unwrap(); 967 | line_stream.send(":welcome".to_string()).await.unwrap(); 968 | assert_eq!( 969 | ":irc.irc ERROR :No command supplied".to_string(), 970 | line_stream.next().await.unwrap().unwrap() 971 | ); 972 | line_stream 973 | .send(":@! PING :welcome".to_string()) 974 | .await 975 | .unwrap(); 976 | assert_eq!( 977 | ":irc.irc ERROR :Wrong source".to_string(), 978 | line_stream.next().await.unwrap().unwrap() 979 | ); 980 | line_stream.send("PART aaa".to_string()).await.unwrap(); 981 | assert_eq!( 982 | ":irc.irc ERROR :Wrong parameter 0 in command 'PART'".to_string(), 983 | line_stream.next().await.unwrap().unwrap() 984 | ); 985 | line_stream.send("PING :welcome".to_string()).await.unwrap(); 986 | assert_eq!( 987 | ":irc.irc 451 127.0.0.1 :You have not registered".to_string(), 988 | line_stream.next().await.unwrap().unwrap() 989 | ); 990 | line_stream.send("CAP XXX".to_string()).await.unwrap(); 991 | assert_eq!( 992 | ":irc.irc ERROR :Unknown subcommand 'XXX' in command 'CAP'".to_string(), 993 | line_stream.next().await.unwrap().unwrap() 994 | ); 995 | line_stream.send("PRIVMSG".to_string()).await.unwrap(); 996 | assert_eq!( 997 | ":irc.irc 461 127.0.0.1 PRIVMSG :Not enough parameters".to_string(), 998 | line_stream.next().await.unwrap().unwrap() 999 | ); 1000 | line_stream.send("MODE lol +T".to_string()).await.unwrap(); 1001 | assert_eq!( 1002 | ":irc.irc 501 127.0.0.1 :Unknown MODE flag".to_string(), 1003 | line_stream.next().await.unwrap().unwrap() 1004 | ); 1005 | line_stream.send("MODE #bum +T".to_string()).await.unwrap(); 1006 | assert_eq!( 1007 | ":irc.irc 472 127.0.0.1 T :is unknown mode char for #bum".to_string(), 1008 | line_stream.next().await.unwrap().unwrap() 1009 | ); 1010 | line_stream 1011 | .send("MODE #bum +l xxx".to_string()) 1012 | .await 1013 | .unwrap(); 1014 | assert_eq!( 1015 | ":irc.irc 696 127.0.0.1 #bum l xxx :invalid digit found in string".to_string(), 1016 | line_stream.next().await.unwrap().unwrap() 1017 | ); 1018 | let mut toolong = String::new(); 1019 | for _ in 0..4000 { 1020 | toolong.push('c'); 1021 | } 1022 | line_stream.send(toolong).await.unwrap(); 1023 | assert_eq!( 1024 | ":irc.irc 417 127.0.0.1 :Input line was too long".to_string(), 1025 | line_stream.next().await.unwrap().unwrap() 1026 | ); 1027 | } 1028 | 1029 | quit_test_server(main_state, handle).await; 1030 | } 1031 | 1032 | #[tokio::test] 1033 | async fn test_server_authentication() { 1034 | let (main_state, handle, port) = run_test_server(MainConfig::default()).await; 1035 | 1036 | { 1037 | let mut line_stream = login_to_test(port, "mati", "mat", "MatiSzpaki").await; 1038 | assert_eq!( 1039 | ":irc.irc 001 mati :Welcome to the IRCnetwork \ 1040 | Network, mati!~mat@127.0.0.1" 1041 | .to_string(), 1042 | line_stream.next().await.unwrap().unwrap() 1043 | ); 1044 | assert_eq!( 1045 | concat!( 1046 | ":irc.irc 002 mati :Your host is irc.irc, running \ 1047 | version ", 1048 | env!("CARGO_PKG_NAME"), 1049 | "-", 1050 | env!("CARGO_PKG_VERSION") 1051 | ) 1052 | .to_string(), 1053 | line_stream.next().await.unwrap().unwrap() 1054 | ); 1055 | assert_eq!( 1056 | format!( 1057 | ":irc.irc 003 mati :This server was created {}", 1058 | main_state.created 1059 | ), 1060 | line_stream.next().await.unwrap().unwrap() 1061 | ); 1062 | assert_eq!( 1063 | concat!( 1064 | ":irc.irc 004 mati irc.irc ", 1065 | env!("CARGO_PKG_NAME"), 1066 | "-", 1067 | env!("CARGO_PKG_VERSION"), 1068 | " Oiorw Iabehiklmnopqstv" 1069 | ), 1070 | line_stream.next().await.unwrap().unwrap() 1071 | ); 1072 | assert_eq!( 1073 | ":irc.irc 005 mati AWAYLEN=1000 CASEMAPPING=ascii \ 1074 | CHANMODES=Iabehiklmnopqstv CHANNELLEN=1000 CHANTYPES=&# EXCEPTS=e FNC \ 1075 | HOSTLEN=1000 INVEX=I KEYLEN=1000 :are supported by this server" 1076 | .to_string(), 1077 | line_stream.next().await.unwrap().unwrap() 1078 | ); 1079 | assert_eq!( 1080 | ":irc.irc 005 mati KICKLEN=1000 LINELEN=2000 MAXLIST=beI:1000 \ 1081 | MAXNICKLEN=200 MAXPARA=500 MAXTARGETS=500 MODES=500 NETWORK=IRCnetwork \ 1082 | NICKLEN=200 PREFIX=(qaohv)~&@%+ :are supported by this server" 1083 | .to_string(), 1084 | line_stream.next().await.unwrap().unwrap() 1085 | ); 1086 | assert_eq!( 1087 | ":irc.irc 005 mati SAFELIST STATUSMSG=~&@%+ TOPICLEN=1000 USERLEN=200 \ 1088 | USERMODES=Oiorw :are supported by this server" 1089 | .to_string(), 1090 | line_stream.next().await.unwrap().unwrap() 1091 | ); 1092 | assert_eq!( 1093 | ":irc.irc 251 mati :There are 1 users and 0 invisible \ 1094 | on 1 servers" 1095 | .to_string(), 1096 | line_stream.next().await.unwrap().unwrap() 1097 | ); 1098 | assert_eq!( 1099 | ":irc.irc 252 mati 0 :operator(s) online".to_string(), 1100 | line_stream.next().await.unwrap().unwrap() 1101 | ); 1102 | assert_eq!( 1103 | ":irc.irc 253 mati 0 :unknown connection(s)".to_string(), 1104 | line_stream.next().await.unwrap().unwrap() 1105 | ); 1106 | assert_eq!( 1107 | ":irc.irc 254 mati 0 :channels formed".to_string(), 1108 | line_stream.next().await.unwrap().unwrap() 1109 | ); 1110 | assert_eq!( 1111 | ":irc.irc 255 mati :I have 1 clients and 1 servers".to_string(), 1112 | line_stream.next().await.unwrap().unwrap() 1113 | ); 1114 | assert_eq!( 1115 | ":irc.irc 265 mati 1 1 :Current local users 1, max 1".to_string(), 1116 | line_stream.next().await.unwrap().unwrap() 1117 | ); 1118 | assert_eq!( 1119 | ":irc.irc 266 mati 1 1 :Current global users 1, max 1".to_string(), 1120 | line_stream.next().await.unwrap().unwrap() 1121 | ); 1122 | assert_eq!( 1123 | ":irc.irc 375 mati :- irc.irc Message of the day - ".to_string(), 1124 | line_stream.next().await.unwrap().unwrap() 1125 | ); 1126 | assert_eq!( 1127 | ":irc.irc 372 mati :Hello, world!".to_string(), 1128 | line_stream.next().await.unwrap().unwrap() 1129 | ); 1130 | assert_eq!( 1131 | ":irc.irc 376 mati :End of /MOTD command.".to_string(), 1132 | line_stream.next().await.unwrap().unwrap() 1133 | ); 1134 | assert_eq!( 1135 | ":irc.irc 221 mati +".to_string(), 1136 | line_stream.next().await.unwrap().unwrap() 1137 | ); 1138 | 1139 | let state = main_state.state.read().await; 1140 | assert_eq!( 1141 | HashSet::from(["mati".to_string()]), 1142 | HashSet::from_iter(state.users.keys().cloned()) 1143 | ); 1144 | assert_eq!( 1145 | HashSet::from(["mat".to_string()]), 1146 | HashSet::from_iter(state.users.values().map(|u| u.name.clone())) 1147 | ); 1148 | assert_eq!( 1149 | HashSet::from(["MatiSzpaki".to_string()]), 1150 | HashSet::from_iter(state.users.values().map(|u| u.realname.clone())) 1151 | ); 1152 | 1153 | line_stream.send("CAP LIST".to_string()).await.unwrap(); 1154 | assert_eq!( 1155 | ":irc.irc CAP * LIST :".to_string(), 1156 | line_stream.next().await.unwrap().unwrap() 1157 | ); 1158 | 1159 | line_stream.send("QUIT :Bye".to_string()).await.unwrap(); 1160 | assert_eq!( 1161 | ":irc.irc ERROR: Closing connection".to_string(), 1162 | line_stream.next().await.unwrap().unwrap() 1163 | ); 1164 | } 1165 | time::sleep(Duration::from_millis(50)).await; 1166 | { 1167 | // after close 1168 | let state = main_state.state.read().await; 1169 | assert_eq!( 1170 | HashSet::new(), 1171 | HashSet::from_iter(state.users.keys().cloned()) 1172 | ); 1173 | } 1174 | 1175 | quit_test_server(main_state, handle).await; 1176 | } 1177 | 1178 | #[cfg(any(feature = "tls_rustls", feature = "tls_openssl"))] 1179 | #[tokio::test] 1180 | async fn test_server_tls_first() { 1181 | let (main_state, handle, port) = run_test_tls_server(MainConfig::default()).await; 1182 | { 1183 | let mut line_stream = login_to_test_tls(port, "mati", "mat", "MatiSzpaki").await; 1184 | assert_eq!( 1185 | ":irc.irc 001 mati :Welcome to the IRCnetwork \ 1186 | Network, mati!~mat@127.0.0.1" 1187 | .to_string(), 1188 | line_stream.next().await.unwrap().unwrap() 1189 | ); 1190 | assert_eq!( 1191 | concat!( 1192 | ":irc.irc 002 mati :Your host is irc.irc, running \ 1193 | version ", 1194 | env!("CARGO_PKG_NAME"), 1195 | "-", 1196 | env!("CARGO_PKG_VERSION") 1197 | ) 1198 | .to_string(), 1199 | line_stream.next().await.unwrap().unwrap() 1200 | ); 1201 | assert_eq!( 1202 | format!( 1203 | ":irc.irc 003 mati :This server was created {}", 1204 | main_state.created 1205 | ), 1206 | line_stream.next().await.unwrap().unwrap() 1207 | ); 1208 | assert_eq!( 1209 | concat!( 1210 | ":irc.irc 004 mati irc.irc ", 1211 | env!("CARGO_PKG_NAME"), 1212 | "-", 1213 | env!("CARGO_PKG_VERSION"), 1214 | " Oiorw Iabehiklmnopqstv" 1215 | ), 1216 | line_stream.next().await.unwrap().unwrap() 1217 | ); 1218 | assert_eq!( 1219 | ":irc.irc 005 mati AWAYLEN=1000 CASEMAPPING=ascii \ 1220 | CHANMODES=Iabehiklmnopqstv CHANNELLEN=1000 CHANTYPES=&# EXCEPTS=e FNC \ 1221 | HOSTLEN=1000 INVEX=I KEYLEN=1000 :are supported by this server" 1222 | .to_string(), 1223 | line_stream.next().await.unwrap().unwrap() 1224 | ); 1225 | assert_eq!( 1226 | ":irc.irc 005 mati KICKLEN=1000 LINELEN=2000 MAXLIST=beI:1000 \ 1227 | MAXNICKLEN=200 MAXPARA=500 MAXTARGETS=500 MODES=500 NETWORK=IRCnetwork \ 1228 | NICKLEN=200 PREFIX=(qaohv)~&@%+ :are supported by this server" 1229 | .to_string(), 1230 | line_stream.next().await.unwrap().unwrap() 1231 | ); 1232 | assert_eq!( 1233 | ":irc.irc 005 mati SAFELIST STATUSMSG=~&@%+ TOPICLEN=1000 USERLEN=200 \ 1234 | USERMODES=Oiorw :are supported by this server" 1235 | .to_string(), 1236 | line_stream.next().await.unwrap().unwrap() 1237 | ); 1238 | assert_eq!( 1239 | ":irc.irc 251 mati :There are 1 users and 0 invisible \ 1240 | on 1 servers" 1241 | .to_string(), 1242 | line_stream.next().await.unwrap().unwrap() 1243 | ); 1244 | assert_eq!( 1245 | ":irc.irc 252 mati 0 :operator(s) online".to_string(), 1246 | line_stream.next().await.unwrap().unwrap() 1247 | ); 1248 | assert_eq!( 1249 | ":irc.irc 253 mati 0 :unknown connection(s)".to_string(), 1250 | line_stream.next().await.unwrap().unwrap() 1251 | ); 1252 | assert_eq!( 1253 | ":irc.irc 254 mati 0 :channels formed".to_string(), 1254 | line_stream.next().await.unwrap().unwrap() 1255 | ); 1256 | assert_eq!( 1257 | ":irc.irc 255 mati :I have 1 clients and 1 servers".to_string(), 1258 | line_stream.next().await.unwrap().unwrap() 1259 | ); 1260 | assert_eq!( 1261 | ":irc.irc 265 mati 1 1 :Current local users 1, max 1".to_string(), 1262 | line_stream.next().await.unwrap().unwrap() 1263 | ); 1264 | assert_eq!( 1265 | ":irc.irc 266 mati 1 1 :Current global users 1, max 1".to_string(), 1266 | line_stream.next().await.unwrap().unwrap() 1267 | ); 1268 | assert_eq!( 1269 | ":irc.irc 375 mati :- irc.irc Message of the day - ".to_string(), 1270 | line_stream.next().await.unwrap().unwrap() 1271 | ); 1272 | assert_eq!( 1273 | ":irc.irc 372 mati :Hello, world!".to_string(), 1274 | line_stream.next().await.unwrap().unwrap() 1275 | ); 1276 | assert_eq!( 1277 | ":irc.irc 376 mati :End of /MOTD command.".to_string(), 1278 | line_stream.next().await.unwrap().unwrap() 1279 | ); 1280 | assert_eq!( 1281 | ":irc.irc 221 mati +".to_string(), 1282 | line_stream.next().await.unwrap().unwrap() 1283 | ); 1284 | } 1285 | 1286 | quit_test_server(main_state, handle).await; 1287 | } 1288 | 1289 | #[cfg(any(feature = "tls_rustls", feature = "tls_openssl"))] 1290 | #[tokio::test] 1291 | async fn test_server_timeouts() { 1292 | let (main_state, handle, port) = run_test_server(MainConfig::default()).await; 1293 | 1294 | { 1295 | let mut line_stream = login_to_test_and_skip(port, "mati", "mat", "MatiSzpaki").await; 1296 | 1297 | line_stream.send("PING :bumbum".to_string()).await.unwrap(); 1298 | assert_eq!( 1299 | ":irc.irc PONG irc.irc :bumbum".to_string(), 1300 | line_stream.next().await.unwrap().unwrap() 1301 | ); 1302 | time::pause(); 1303 | time::advance(Duration::from_millis(119900)).await; 1304 | time::resume(); 1305 | assert_eq!( 1306 | ":irc.irc PING :LALAL".to_string(), 1307 | line_stream.next().await.unwrap().unwrap() 1308 | ); 1309 | line_stream.send("PONG :LALAL".to_string()).await.unwrap(); 1310 | 1311 | // test timeout 1312 | time::pause(); 1313 | time::advance(Duration::from_millis(119900)).await; 1314 | time::resume(); 1315 | assert_eq!( 1316 | ":irc.irc PING :LALAL".to_string(), 1317 | line_stream.next().await.unwrap().unwrap() 1318 | ); 1319 | time::pause(); 1320 | time::advance(Duration::from_millis(19900)).await; 1321 | time::resume(); 1322 | assert_eq!( 1323 | ":irc.irc ERROR :Pong timeout, connection will \ 1324 | be closed." 1325 | .to_string(), 1326 | line_stream.next().await.unwrap().unwrap() 1327 | ); 1328 | } 1329 | 1330 | quit_test_server(main_state, handle).await; 1331 | } 1332 | } 1333 | 1334 | mod channel_cmds; 1335 | mod conn_cmds; 1336 | mod rest_cmds; 1337 | mod srv_query_cmds; 1338 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // config.rs - configuration 2 | // 3 | // simple-irc-server - simple IRC server 4 | // Copyright (C) 2022 Mateusz Szpakowski 5 | // 6 | // This library is free software; you can redistribute it and/or 7 | // modify it under the terms of the GNU Lesser General Public 8 | // License as published by the Free Software Foundation; either 9 | // version 2.1 of the License, or (at your option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // Lesser General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public 17 | // License along with this library; if not, write to the Free Software 18 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | 20 | use serde::Deserializer; 21 | use serde_derive::Deserialize; 22 | use std::collections::HashSet; 23 | use std::error::Error; 24 | use std::fmt; 25 | use std::fs::File; 26 | use std::io::Read; 27 | use std::net::IpAddr; 28 | use std::str::FromStr; 29 | use validator::Validate; 30 | 31 | use crate::utils::match_wildcard; 32 | use crate::utils::validate_channel; 33 | use crate::utils::validate_password_hash; 34 | use crate::utils::validate_username; 35 | 36 | #[derive(clap::Parser, Clone)] 37 | #[clap(author, version, about, long_about = None)] 38 | pub(crate) struct Cli { 39 | #[clap(short, long, help = "Generate password hash")] 40 | pub(crate) gen_password_hash: bool, 41 | #[clap(short = 'P', long, help = "Password for generated password hash")] 42 | pub(crate) password: Option, 43 | #[clap(short, long, help = "Configuration file path")] 44 | config: Option, 45 | #[clap(short, long, help = "Listen bind address")] 46 | listen: Option, 47 | #[clap(short, long, help = "Listen port")] 48 | port: Option, 49 | #[clap(short = 'n', long, help = "Server name")] 50 | name: Option, 51 | #[clap(short = 'N', long, help = "Network")] 52 | network: Option, 53 | #[clap(short, long, help = "DNS lookup if client connects")] 54 | dns_lookup: bool, 55 | #[clap(short = 'C', long, help = "TLS certificate file")] 56 | tls_cert_file: Option, 57 | #[clap(short = 'K', long, help = "TLS certificate key file")] 58 | tls_cert_key_file: Option, 59 | #[clap(short = 'L', long, help = "Log file path")] 60 | log_file: Option, 61 | } 62 | 63 | #[derive(PartialEq, Eq, Deserialize, Debug, Clone)] 64 | pub(crate) struct TLSConfig { 65 | pub(crate) cert_file: String, 66 | pub(crate) cert_key_file: String, 67 | } 68 | 69 | #[derive(PartialEq, Eq, Deserialize, Debug, Validate)] 70 | pub(crate) struct OperatorConfig { 71 | #[validate(custom = "validate_username")] 72 | pub(crate) name: String, 73 | #[validate(custom = "validate_password_hash")] 74 | pub(crate) password: String, 75 | pub(crate) mask: Option, 76 | } 77 | 78 | #[derive(Copy, Clone, PartialEq, Eq, Deserialize, Debug, Default)] 79 | pub(crate) struct UserModes { 80 | pub(crate) invisible: bool, 81 | pub(crate) oper: bool, 82 | pub(crate) local_oper: bool, 83 | pub(crate) registered: bool, 84 | pub(crate) wallops: bool, 85 | } 86 | 87 | impl fmt::Display for UserModes { 88 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 89 | let mut s = '+'.to_string(); 90 | if self.invisible { 91 | s.push('i'); 92 | } 93 | if self.oper { 94 | s.push('o'); 95 | } 96 | if self.local_oper { 97 | s.push('O'); 98 | } 99 | if self.registered { 100 | s.push('r'); 101 | } 102 | if self.wallops { 103 | s.push('w'); 104 | } 105 | f.write_str(&s) 106 | } 107 | } 108 | 109 | impl UserModes { 110 | pub(crate) fn is_local_oper(&self) -> bool { 111 | self.local_oper || self.oper 112 | } 113 | } 114 | 115 | #[derive(Clone, PartialEq, Eq, Deserialize, Debug, Validate, Default)] 116 | pub(crate) struct ChannelModes { 117 | // If channel modes we use Option to avoid unnecessary field definition if list 118 | // in this field should be. The administrator can omit fields for empty lists. 119 | pub(crate) ban: Option>, 120 | pub(crate) exception: Option>, 121 | pub(crate) client_limit: Option, 122 | pub(crate) invite_exception: Option>, 123 | pub(crate) key: Option, 124 | pub(crate) operators: Option>, 125 | pub(crate) half_operators: Option>, 126 | pub(crate) voices: Option>, 127 | pub(crate) founders: Option>, 128 | pub(crate) protecteds: Option>, 129 | pub(crate) invite_only: bool, 130 | pub(crate) moderated: bool, 131 | pub(crate) secret: bool, 132 | pub(crate) protected_topic: bool, 133 | pub(crate) no_external_messages: bool, 134 | } 135 | 136 | impl ChannelModes { 137 | // create new channel modes for new channel created by user. By default, 138 | // user that created channel is founder and operator in this channel. 139 | pub(crate) fn new_for_channel(user_nick: String) -> Self { 140 | ChannelModes { 141 | operators: Some([user_nick.clone()].into()), 142 | founders: Some([user_nick].into()), 143 | ..ChannelModes::default() 144 | } 145 | } 146 | 147 | pub(crate) fn banned(&self, source: &str) -> bool { 148 | self.ban 149 | .as_ref() 150 | .map_or(false, |b| b.iter().any(|b| match_wildcard(b, source))) 151 | && (!self 152 | .exception 153 | .as_ref() 154 | .map_or(false, |e| e.iter().any(|e| match_wildcard(e, source)))) 155 | } 156 | 157 | // rename user - just rename nick in lists. 158 | pub(crate) fn rename_user(&mut self, old_nick: &String, nick: String) { 159 | if let Some(ref mut operators) = self.operators { 160 | if operators.remove(old_nick) { 161 | operators.insert(nick.clone()); 162 | } 163 | } 164 | if let Some(ref mut half_operators) = self.half_operators { 165 | if half_operators.remove(old_nick) { 166 | half_operators.insert(nick.clone()); 167 | } 168 | } 169 | if let Some(ref mut voices) = self.voices { 170 | if voices.remove(old_nick) { 171 | voices.insert(nick.clone()); 172 | } 173 | } 174 | if let Some(ref mut founders) = self.founders { 175 | if founders.remove(old_nick) { 176 | founders.insert(nick.clone()); 177 | } 178 | } 179 | if let Some(ref mut protecteds) = self.protecteds { 180 | if protecteds.remove(old_nick) { 181 | protecteds.insert(nick); 182 | } 183 | } 184 | } 185 | } 186 | 187 | impl fmt::Display for ChannelModes { 188 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 189 | let mut s = '+'.to_string(); 190 | if self.invite_only { 191 | s.push('i'); 192 | } 193 | if self.moderated { 194 | s.push('m'); 195 | } 196 | if self.secret { 197 | s.push('s'); 198 | } 199 | if self.protected_topic { 200 | s.push('t'); 201 | } 202 | if self.no_external_messages { 203 | s.push('n'); 204 | } 205 | if self.key.is_some() { 206 | s.push('k'); 207 | } 208 | if self.client_limit.is_some() { 209 | s.push('l'); 210 | } 211 | if let Some(ref k) = self.key { 212 | s.push(' '); 213 | s += k; 214 | } 215 | if let Some(l) = self.client_limit { 216 | s.push(' '); 217 | s += &l.to_string(); 218 | } 219 | if let Some(ref ban) = self.ban { 220 | ban.iter().for_each(|b| { 221 | s += " +b "; 222 | s += b; 223 | }); 224 | } 225 | if let Some(ref exception) = self.exception { 226 | exception.iter().for_each(|e| { 227 | s += " +e "; 228 | s += e; 229 | }); 230 | } 231 | if let Some(ref invite_exception) = self.invite_exception { 232 | invite_exception.iter().for_each(|i| { 233 | s += " +I "; 234 | s += i; 235 | }); 236 | } 237 | 238 | if let Some(ref founders) = self.founders { 239 | founders.iter().for_each(|q| { 240 | s += " +q "; 241 | s += q; 242 | }); 243 | } 244 | if let Some(ref protecteds) = self.protecteds { 245 | protecteds.iter().for_each(|a| { 246 | s += " +a "; 247 | s += a; 248 | }); 249 | } 250 | if let Some(ref operators) = self.operators { 251 | operators.iter().for_each(|o| { 252 | s += " +o "; 253 | s += o; 254 | }); 255 | } 256 | if let Some(ref half_operators) = self.half_operators { 257 | half_operators.iter().for_each(|h| { 258 | s += " +h "; 259 | s += h; 260 | }); 261 | } 262 | if let Some(ref voices) = self.voices { 263 | voices.iter().for_each(|v| { 264 | s += " +v "; 265 | s += v; 266 | }); 267 | } 268 | f.write_str(&s) 269 | } 270 | } 271 | 272 | #[derive(PartialEq, Eq, Deserialize, Debug, Validate)] 273 | pub(crate) struct ChannelConfig { 274 | #[validate(custom = "validate_channel")] 275 | pub(crate) name: String, 276 | pub(crate) topic: Option, 277 | #[validate] 278 | pub(crate) modes: ChannelModes, 279 | } 280 | 281 | #[derive(PartialEq, Eq, Deserialize, Debug, Validate)] 282 | pub(crate) struct UserConfig { 283 | #[validate(custom = "validate_username")] 284 | pub(crate) name: String, 285 | #[validate(custom = "validate_username")] 286 | pub(crate) nick: String, 287 | #[validate(length(min = 6))] 288 | #[validate(custom = "validate_password_hash")] 289 | pub(crate) password: Option, 290 | pub(crate) mask: Option, 291 | } 292 | 293 | /// Main configuration structure. 294 | #[derive(PartialEq, Eq, Deserialize, Debug, Validate)] 295 | pub(crate) struct MainConfig { 296 | #[validate(contains = ".")] 297 | pub(crate) name: String, 298 | pub(crate) admin_info: String, 299 | pub(crate) admin_info2: Option, 300 | pub(crate) admin_email: Option, 301 | pub(crate) info: String, 302 | pub(crate) motd: String, 303 | pub(crate) listen: IpAddr, 304 | pub(crate) port: u16, 305 | pub(crate) network: String, 306 | #[validate(custom = "validate_password_hash")] 307 | pub(crate) password: Option, 308 | pub(crate) max_connections: Option, 309 | pub(crate) max_joins: Option, 310 | pub(crate) ping_timeout: u64, 311 | pub(crate) pong_timeout: u64, 312 | pub(crate) dns_lookup: bool, 313 | pub(crate) default_user_modes: UserModes, 314 | pub(crate) log_file: Option, 315 | #[serde(deserialize_with = "tracing_log_level_deserialize")] 316 | pub(crate) log_level: tracing::Level, 317 | pub(crate) tls: Option, 318 | // If MainConfig modes we use Option to avoid unnecessary field definition if list 319 | // in this field should be. The administrator can omit fields for empty lists. 320 | #[validate] 321 | pub(crate) operators: Option>, 322 | #[validate] 323 | pub(crate) users: Option>, 324 | #[validate] 325 | pub(crate) channels: Option>, 326 | } 327 | 328 | struct TracingLevelVisitor; 329 | 330 | impl<'de> serde::de::Visitor<'de> for TracingLevelVisitor { 331 | type Value = tracing::Level; 332 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 333 | formatter.write_str("TracingLevel") 334 | } 335 | 336 | fn visit_str(self, v: &str) -> Result { 337 | tracing::Level::from_str(v).map_err(|e| serde::de::Error::custom(e)) 338 | } 339 | } 340 | 341 | fn tracing_log_level_deserialize<'de, D: Deserializer<'de>>( 342 | ds: D, 343 | ) -> Result { 344 | ds.deserialize_str(TracingLevelVisitor) 345 | } 346 | 347 | impl MainConfig { 348 | // create new main config from command line. 349 | pub(crate) fn new(cli: Cli) -> Result> { 350 | // get config path. 351 | let config_path = cli.config.as_deref().unwrap_or("simple-irc-server.toml"); 352 | let mut config_file = File::open(config_path)?; 353 | let mut config_str = String::new(); 354 | config_file.read_to_string(&mut config_str)?; 355 | // modify configuration by CLI options 356 | { 357 | let mut config: MainConfig = toml::from_str(&config_str)?; 358 | if let Some(addr) = cli.listen { 359 | config.listen = addr; 360 | } 361 | if let Some(port) = cli.port { 362 | config.port = port; 363 | } 364 | if let Some(name) = cli.name { 365 | config.name = name; 366 | } 367 | if let Some(network) = cli.network { 368 | config.network = network; 369 | } 370 | if let Some(log_file) = cli.log_file { 371 | config.log_file = Some(log_file) 372 | } 373 | config.dns_lookup = config.dns_lookup || cli.dns_lookup; 374 | 375 | // get indicator to check later 376 | let (have_cert, have_cert_key) = 377 | (cli.tls_cert_file.is_some(), cli.tls_cert_key_file.is_some()); 378 | 379 | if let Some(tls_cert_file) = cli.tls_cert_file { 380 | if let Some(tls_cert_key_file) = cli.tls_cert_key_file { 381 | config.tls = Some(TLSConfig { 382 | cert_file: tls_cert_file, 383 | cert_key_file: tls_cert_key_file, 384 | }); 385 | } 386 | } 387 | // both config are required 388 | if (have_cert && !have_cert_key) || (!have_cert && have_cert_key) { 389 | return Err(Box::new(clap::error::Error::raw( 390 | clap::ErrorKind::ValueValidation, 391 | "TLS certifcate file and certificate \ 392 | key file together are required", 393 | ))); 394 | } 395 | if let Err(e) = config.validate() { 396 | Err(Box::new(e)) 397 | } else if !config.validate_nicknames() { 398 | Err(Box::new(clap::error::Error::raw( 399 | clap::ErrorKind::ValueValidation, 400 | "Wrong nikname lengths", 401 | ))) 402 | } else { 403 | Ok(config) 404 | } 405 | } 406 | } 407 | 408 | fn validate_nicknames(&self) -> bool { 409 | if let Some(ref users) = self.users { 410 | !users.iter().any(|u| u.nick.len() > 200) 411 | } else { 412 | true 413 | } 414 | } 415 | } 416 | 417 | impl Default for MainConfig { 418 | fn default() -> Self { 419 | MainConfig { 420 | name: "irc.irc".to_string(), 421 | admin_info: "ircadmin is IRC admin".to_string(), 422 | admin_info2: None, 423 | admin_email: None, 424 | info: "This is IRC server".to_string(), 425 | listen: "127.0.0.1".parse().unwrap(), 426 | port: 6667, 427 | network: "IRCnetwork".to_string(), 428 | password: None, 429 | motd: "Hello, world!".to_string(), 430 | max_connections: None, 431 | max_joins: None, 432 | ping_timeout: 120, 433 | pong_timeout: 20, 434 | dns_lookup: false, 435 | channels: None, 436 | operators: None, 437 | users: None, 438 | default_user_modes: UserModes::default(), 439 | tls: None, 440 | log_file: None, 441 | log_level: tracing::Level::INFO, 442 | } 443 | } 444 | } 445 | 446 | #[cfg(test)] 447 | mod test { 448 | use super::*; 449 | 450 | use std::env::temp_dir; 451 | use std::fs; 452 | 453 | struct TempFileHandle { 454 | path: String, 455 | } 456 | 457 | impl TempFileHandle { 458 | fn new(path: &str) -> TempFileHandle { 459 | TempFileHandle { 460 | path: temp_dir().join(path).to_string_lossy().to_string(), 461 | } 462 | } 463 | } 464 | 465 | impl Drop for TempFileHandle { 466 | fn drop(&mut self) { 467 | fs::remove_file(self.path.as_str()).unwrap(); 468 | } 469 | } 470 | 471 | #[test] 472 | fn test_mainconfig_new() { 473 | let file_handle = TempFileHandle::new("temp_config.toml"); 474 | let cli = Cli { 475 | config: Some(file_handle.path.clone()), 476 | gen_password_hash: false, 477 | password: None, 478 | listen: None, 479 | port: None, 480 | name: None, 481 | network: None, 482 | dns_lookup: false, 483 | tls_cert_file: None, 484 | tls_cert_key_file: None, 485 | log_file: None, 486 | }; 487 | 488 | fs::write( 489 | file_handle.path.as_str(), 490 | r##" 491 | name = "irci.localhost" 492 | admin_info = "IRCI is local IRC server" 493 | admin_info2 = "IRCI is good server" 494 | info = "This is IRCI server" 495 | listen = "127.0.0.1" 496 | port = 6667 497 | password = "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJntk8HEZDgh4ctFei3bqt1r0O1VIyOV7dL+w" 498 | network = "IRCInetwork" 499 | max_connections = 4000 500 | max_joins = 10 501 | ping_timeout = 100 502 | pong_timeout = 30 503 | motd = "Hello, guys!" 504 | dns_lookup = false 505 | log_level = "INFO" 506 | 507 | [default_user_modes] 508 | invisible = false 509 | oper = false 510 | local_oper = false 511 | registered = true 512 | wallops = false 513 | 514 | [tls] 515 | cert_file = "cert.crt" 516 | cert_key_file = "cert_key.crt" 517 | 518 | [[operators]] 519 | name = "matiszpaki" 520 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 521 | 522 | [[channels]] 523 | name = "#channel1" 524 | topic = "Some topic" 525 | [channels.modes] 526 | ban = [ 'baddi@*', 'baddi2@*' ] 527 | exception = [ 'bobby@*', 'mati@*' ] 528 | moderated = false 529 | invite_only = false 530 | secret = false 531 | protected_topic = false 532 | no_external_messages = false 533 | 534 | [[users]] 535 | name = "lucas" 536 | nick = "luckboy" 537 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 538 | 539 | [[channels]] 540 | name = "#channel2" 541 | topic = "Some topic 2" 542 | [channels.modes] 543 | key = "hokus pokus" 544 | ban = [] 545 | exception = [] 546 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 547 | operators = [ "banny", "rorry" ] 548 | moderated = true 549 | invite_only = true 550 | client_limit = 200 551 | secret = false 552 | protected_topic = true 553 | no_external_messages = false 554 | "##, 555 | ) 556 | .unwrap(); 557 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 558 | assert_eq!( 559 | Ok(MainConfig { 560 | name: "irci.localhost".to_string(), 561 | admin_info: "IRCI is local IRC server".to_string(), 562 | admin_info2: Some("IRCI is good server".to_string()), 563 | admin_email: None, 564 | info: "This is IRCI server".to_string(), 565 | listen: "127.0.0.1".parse().unwrap(), 566 | port: 6667, 567 | password: Some( 568 | "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJntk8HEZDgh\ 569 | 4ctFei3bqt1r0O1VIyOV7dL+w" 570 | .to_string() 571 | ), 572 | motd: "Hello, guys!".to_string(), 573 | network: "IRCInetwork".to_string(), 574 | max_connections: Some(4000), 575 | max_joins: Some(10), 576 | ping_timeout: 100, 577 | pong_timeout: 30, 578 | dns_lookup: false, 579 | log_file: None, 580 | log_level: tracing::Level::INFO, 581 | tls: Some(TLSConfig { 582 | cert_file: "cert.crt".to_string(), 583 | cert_key_file: "cert_key.crt".to_string() 584 | }), 585 | default_user_modes: UserModes { 586 | invisible: false, 587 | oper: false, 588 | local_oper: false, 589 | registered: true, 590 | wallops: false, 591 | }, 592 | operators: Some(vec![OperatorConfig { 593 | name: "matiszpaki".to_string(), 594 | password: "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr\ 595 | 4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 596 | .to_string(), 597 | mask: None 598 | }]), 599 | users: Some(vec![UserConfig { 600 | name: "lucas".to_string(), 601 | nick: "luckboy".to_string(), 602 | password: Some( 603 | "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639l\ 604 | UsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 605 | .to_string() 606 | ), 607 | mask: None 608 | }]), 609 | channels: Some(vec![ 610 | ChannelConfig { 611 | name: "#channel1".to_string(), 612 | topic: Some("Some topic".to_string()), 613 | modes: ChannelModes { 614 | key: None, 615 | ban: Some(["baddi@*".to_string(), "baddi2@*".to_string()].into()), 616 | exception: Some(["bobby@*".to_string(), "mati@*".to_string()].into()), 617 | invite_exception: None, 618 | operators: None, 619 | half_operators: None, 620 | voices: None, 621 | founders: None, 622 | protecteds: None, 623 | client_limit: None, 624 | invite_only: false, 625 | moderated: false, 626 | secret: false, 627 | protected_topic: false, 628 | no_external_messages: false 629 | }, 630 | }, 631 | ChannelConfig { 632 | name: "#channel2".to_string(), 633 | topic: Some("Some topic 2".to_string()), 634 | modes: ChannelModes { 635 | key: Some("hokus pokus".to_string()), 636 | ban: Some([].into()), 637 | exception: Some([].into()), 638 | invite_exception: Some( 639 | ["nomi@buru.com".to_string(), "pampam@zerox.net".to_string()] 640 | .into() 641 | ), 642 | operators: Some(["banny".to_string(), "rorry".to_string()].into()), 643 | half_operators: None, 644 | voices: None, 645 | founders: None, 646 | protecteds: None, 647 | client_limit: Some(200), 648 | invite_only: true, 649 | moderated: true, 650 | secret: false, 651 | protected_topic: true, 652 | no_external_messages: false 653 | }, 654 | }, 655 | ]), 656 | }), 657 | result 658 | ); 659 | 660 | let cli2 = Cli { 661 | config: Some(file_handle.path.clone()), 662 | gen_password_hash: false, 663 | password: None, 664 | listen: Some("192.168.1.4".parse().unwrap()), 665 | port: Some(6668), 666 | name: Some("ircer.localhost".to_string()), 667 | network: Some("SomeNetwork".to_string()), 668 | dns_lookup: true, 669 | tls_cert_file: Some("some_cert.crt".to_string()), 670 | tls_cert_key_file: Some("some_key.crt".to_string()), 671 | log_file: Some("irc.log".to_string()), 672 | }; 673 | 674 | let result = MainConfig::new(cli2).map_err(|e| e.to_string()); 675 | assert_eq!( 676 | Ok(MainConfig { 677 | name: "ircer.localhost".to_string(), 678 | admin_info: "IRCI is local IRC server".to_string(), 679 | admin_info2: Some("IRCI is good server".to_string()), 680 | admin_email: None, 681 | info: "This is IRCI server".to_string(), 682 | listen: "192.168.1.4".parse().unwrap(), 683 | port: 6668, 684 | password: Some( 685 | "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJntk8HEZDgh\ 686 | 4ctFei3bqt1r0O1VIyOV7dL+w" 687 | .to_string() 688 | ), 689 | motd: "Hello, guys!".to_string(), 690 | network: "SomeNetwork".to_string(), 691 | max_connections: Some(4000), 692 | max_joins: Some(10), 693 | ping_timeout: 100, 694 | pong_timeout: 30, 695 | dns_lookup: true, 696 | log_file: Some("irc.log".to_string()), 697 | log_level: tracing::Level::INFO, 698 | tls: Some(TLSConfig { 699 | cert_file: "some_cert.crt".to_string(), 700 | cert_key_file: "some_key.crt".to_string() 701 | }), 702 | default_user_modes: UserModes { 703 | invisible: false, 704 | oper: false, 705 | local_oper: false, 706 | registered: true, 707 | wallops: false, 708 | }, 709 | operators: Some(vec![OperatorConfig { 710 | name: "matiszpaki".to_string(), 711 | password: "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr\ 712 | 4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 713 | .to_string(), 714 | mask: None 715 | }]), 716 | users: Some(vec![UserConfig { 717 | name: "lucas".to_string(), 718 | nick: "luckboy".to_string(), 719 | password: Some( 720 | "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639l\ 721 | UsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 722 | .to_string() 723 | ), 724 | mask: None 725 | }]), 726 | channels: Some(vec![ 727 | ChannelConfig { 728 | name: "#channel1".to_string(), 729 | topic: Some("Some topic".to_string()), 730 | modes: ChannelModes { 731 | key: None, 732 | ban: Some(["baddi@*".to_string(), "baddi2@*".to_string()].into()), 733 | exception: Some(["bobby@*".to_string(), "mati@*".to_string()].into()), 734 | invite_exception: None, 735 | operators: None, 736 | half_operators: None, 737 | voices: None, 738 | founders: None, 739 | protecteds: None, 740 | client_limit: None, 741 | invite_only: false, 742 | moderated: false, 743 | secret: false, 744 | protected_topic: false, 745 | no_external_messages: false 746 | }, 747 | }, 748 | ChannelConfig { 749 | name: "#channel2".to_string(), 750 | topic: Some("Some topic 2".to_string()), 751 | modes: ChannelModes { 752 | key: Some("hokus pokus".to_string()), 753 | ban: Some([].into()), 754 | exception: Some([].into()), 755 | invite_exception: Some( 756 | ["nomi@buru.com".to_string(), "pampam@zerox.net".to_string()] 757 | .into() 758 | ), 759 | operators: Some(["banny".to_string(), "rorry".to_string()].into()), 760 | half_operators: None, 761 | voices: None, 762 | founders: None, 763 | protecteds: None, 764 | client_limit: Some(200), 765 | invite_only: true, 766 | moderated: true, 767 | secret: false, 768 | protected_topic: true, 769 | no_external_messages: false 770 | }, 771 | }, 772 | ]), 773 | }), 774 | result 775 | ); 776 | 777 | let cli2 = Cli { 778 | config: Some(file_handle.path.clone()), 779 | gen_password_hash: false, 780 | password: None, 781 | listen: Some("192.168.1.4".parse().unwrap()), 782 | port: Some(6668), 783 | name: Some("ircer.localhost".to_string()), 784 | network: Some("SomeNetwork".to_string()), 785 | dns_lookup: true, 786 | tls_cert_file: Some("some_cert.crt".to_string()), 787 | tls_cert_key_file: None, 788 | log_file: None, 789 | }; 790 | let result = MainConfig::new(cli2).map_err(|e| e.to_string()); 791 | assert_eq!( 792 | Err( 793 | "error: TLS certifcate file and certificate key file together \ 794 | are required" 795 | .to_string() 796 | ), 797 | result 798 | ); 799 | 800 | // next testcase 801 | fs::write( 802 | file_handle.path.as_str(), 803 | r##" 804 | name = "irci.localhost" 805 | admin_info = "IRCI is local IRC server" 806 | admin_info2 = "IRCI is good server" 807 | info = "This is IRCI server" 808 | listen = "127.0.0.1" 809 | port = 6667 810 | motd = "Hello, guys!" 811 | network = "IRCInetwork" 812 | ping_timeout = 100 813 | pong_timeout = 30 814 | dns_lookup = false 815 | log_file = "log.log" 816 | log_level = "INFO" 817 | 818 | [default_user_modes] 819 | invisible = false 820 | oper = false 821 | local_oper = false 822 | registered = true 823 | wallops = false 824 | 825 | [[channels]] 826 | name = "#channel1" 827 | topic = "Some topic" 828 | [channels.modes] 829 | ban = [ 'baddi@*', 'baddi2@*' ] 830 | exception = [ 'bobby@*', 'mati@*' ] 831 | moderated = false 832 | invite_only = false 833 | secret = false 834 | protected_topic = false 835 | no_external_messages = false 836 | 837 | [[channels]] 838 | name = "#channel2" 839 | topic = "Some topic 2" 840 | [channels.modes] 841 | moderated = true 842 | secret = false 843 | invite_only = false 844 | protected_topic = true 845 | no_external_messages = false 846 | "##, 847 | ) 848 | .unwrap(); 849 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 850 | assert_eq!( 851 | Ok(MainConfig { 852 | name: "irci.localhost".to_string(), 853 | admin_info: "IRCI is local IRC server".to_string(), 854 | admin_info2: Some("IRCI is good server".to_string()), 855 | admin_email: None, 856 | info: "This is IRCI server".to_string(), 857 | listen: "127.0.0.1".parse().unwrap(), 858 | port: 6667, 859 | password: None, 860 | motd: "Hello, guys!".to_string(), 861 | network: "IRCInetwork".to_string(), 862 | max_connections: None, 863 | max_joins: None, 864 | ping_timeout: 100, 865 | pong_timeout: 30, 866 | dns_lookup: false, 867 | log_file: Some("log.log".to_string()), 868 | log_level: tracing::Level::INFO, 869 | tls: None, 870 | default_user_modes: UserModes { 871 | invisible: false, 872 | oper: false, 873 | local_oper: false, 874 | registered: true, 875 | wallops: false, 876 | }, 877 | operators: None, 878 | users: None, 879 | channels: Some(vec![ 880 | ChannelConfig { 881 | name: "#channel1".to_string(), 882 | topic: Some("Some topic".to_string()), 883 | modes: ChannelModes { 884 | key: None, 885 | ban: Some(["baddi@*".to_string(), "baddi2@*".to_string()].into()), 886 | exception: Some(["bobby@*".to_string(), "mati@*".to_string()].into()), 887 | invite_exception: None, 888 | operators: None, 889 | half_operators: None, 890 | voices: None, 891 | founders: None, 892 | protecteds: None, 893 | client_limit: None, 894 | invite_only: false, 895 | moderated: false, 896 | secret: false, 897 | protected_topic: false, 898 | no_external_messages: false 899 | }, 900 | }, 901 | ChannelConfig { 902 | name: "#channel2".to_string(), 903 | topic: Some("Some topic 2".to_string()), 904 | modes: ChannelModes { 905 | key: None, 906 | ban: None, 907 | exception: None, 908 | invite_exception: None, 909 | operators: None, 910 | half_operators: None, 911 | voices: None, 912 | founders: None, 913 | protecteds: None, 914 | client_limit: None, 915 | invite_only: false, 916 | moderated: true, 917 | secret: false, 918 | protected_topic: true, 919 | no_external_messages: false 920 | }, 921 | }, 922 | ]), 923 | }), 924 | result 925 | ); 926 | 927 | // error 928 | fs::write( 929 | file_handle.path.as_str(), 930 | r##" 931 | name = "ircilocalhost" 932 | admin_info = "IRCI is local IRC server" 933 | admin_info2 = "IRCI is good server" 934 | info = "This is IRCI server" 935 | listen = "127.0.0.1" 936 | port = 6667 937 | motd = "Hello, guys!" 938 | network = "IRCInetwork" 939 | max_connections = 4000 940 | max_joins = 10 941 | ping_timeout = 100 942 | pong_timeout = 30 943 | dns_lookup = false 944 | log_level = "INFO" 945 | 946 | [default_user_modes] 947 | invisible = false 948 | oper = false 949 | local_oper = false 950 | registered = true 951 | wallops = false 952 | 953 | [tls] 954 | cert_file = "cert.crt" 955 | cert_key_file = "cert_key.crt" 956 | 957 | [[operators]] 958 | name = "matiszpaki" 959 | password = "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJntk8HEZDgh4ctFei3bqt1r0O1VIyOV7dL+w" 960 | 961 | [[channels]] 962 | name = "#channel1" 963 | topic = "Some topic" 964 | [channels.modes] 965 | ban = [ 'baddi@*', 'baddi2@*' ] 966 | exception = [ 'bobby@*', 'mati@*' ] 967 | moderated = false 968 | invite_only = false 969 | secret = false 970 | protected_topic = false 971 | no_external_messages = false 972 | 973 | [[users]] 974 | name = "lucas" 975 | nick = "luckboy" 976 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 977 | 978 | [[channels]] 979 | name = "#channel2" 980 | topic = "Some topic 2" 981 | [channels.modes] 982 | key = "hokus pokus" 983 | ban = [] 984 | exception = [] 985 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 986 | moderated = true 987 | invite_only = true 988 | client_limit = 200 989 | secret = false 990 | protected_topic = true 991 | no_external_messages = false 992 | "##, 993 | ) 994 | .unwrap(); 995 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 996 | // because sorting of ValidationErrors changes order we check to cases 997 | assert!( 998 | Err( 999 | "name: Validation error: contains [{\"value\": String(\"ircilocalhost\"), \ 1000 | \"needle\": String(\".\")}]" 1001 | .to_string() 1002 | ) == result 1003 | || Err( 1004 | "name: Validation error: contains [{\"needle\": String(\".\"), \ 1005 | \"value\": String(\"ircilocalhost\")}]" 1006 | .to_string() 1007 | ) == result 1008 | ); 1009 | 1010 | fs::write( 1011 | file_handle.path.as_str(), 1012 | r##" 1013 | name = "irci.localhost" 1014 | admin_info = "IRCI is local IRC server" 1015 | admin_info2 = "IRCI is good server" 1016 | info = "This is IRCI server" 1017 | listen = "127.0.0.1" 1018 | port = 6667 1019 | motd = "Hello, guys!" 1020 | network = "IRCInetwork" 1021 | max_connections = 4000 1022 | max_joins = 10 1023 | ping_timeout = 100 1024 | pong_timeout = 30 1025 | dns_lookup = false 1026 | log_level = "INFO" 1027 | 1028 | [default_user_modes] 1029 | invisible = false 1030 | oper = false 1031 | local_oper = false 1032 | registered = true 1033 | wallops = false 1034 | 1035 | [tls] 1036 | cert_file = "cert.crt" 1037 | cert_key_file = "cert_key.crt" 1038 | 1039 | [[operators]] 1040 | name = "matis.zpaki" 1041 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1042 | 1043 | [[channels]] 1044 | name = "#channel1" 1045 | topic = "Some topic" 1046 | [channels.modes] 1047 | ban = [ 'baddi@*', 'baddi2@*' ] 1048 | exception = [ 'bobby@*', 'mati@*' ] 1049 | moderated = false 1050 | secret = false 1051 | invite_only = false 1052 | protected_topic = false 1053 | no_external_messages = false 1054 | 1055 | [[users]] 1056 | name = "lucas" 1057 | nick = "luckboy" 1058 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1059 | 1060 | [[channels]] 1061 | name = "#channel2" 1062 | topic = "Some topic 2" 1063 | [channels.modes] 1064 | key = "hokus pokus" 1065 | ban = [] 1066 | exception = [] 1067 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1068 | moderated = true 1069 | invite_only = true 1070 | client_limit = 200 1071 | secret = false 1072 | protected_topic = true 1073 | no_external_messages = false 1074 | "##, 1075 | ) 1076 | .unwrap(); 1077 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1078 | assert_eq!( 1079 | Err("operators[0].name: Validation error: Username must not \ 1080 | contains '.', ',' or ':'. [{\"value\": String(\"matis.zpaki\")}]" 1081 | .to_string()), 1082 | result 1083 | ); 1084 | 1085 | fs::write( 1086 | file_handle.path.as_str(), 1087 | r##" 1088 | name = "irci.localhost" 1089 | admin_info = "IRCI is local IRC server" 1090 | admin_info2 = "IRCI is good server" 1091 | info = "This is IRCI server" 1092 | listen = "127.0.0.1" 1093 | port = 6667 1094 | motd = "Hello, guys!" 1095 | network = "IRCInetwork" 1096 | max_connections = 4000 1097 | max_joins = 10 1098 | ping_timeout = 100 1099 | pong_timeout = 30 1100 | dns_lookup = false 1101 | log_level = "INFO" 1102 | 1103 | [default_user_modes] 1104 | invisible = false 1105 | oper = false 1106 | local_oper = false 1107 | registered = true 1108 | wallops = false 1109 | 1110 | [tls] 1111 | cert_file = "cert.crt" 1112 | cert_key_file = "cert_key.crt" 1113 | 1114 | [[operators]] 1115 | name = "matis:zpaki" 1116 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1117 | 1118 | [[channels]] 1119 | name = "#channel1" 1120 | topic = "Some topic" 1121 | [channels.modes] 1122 | ban = [ 'baddi@*', 'baddi2@*' ] 1123 | exception = [ 'bobby@*', 'mati@*' ] 1124 | moderated = false 1125 | invite_only = false 1126 | secret = false 1127 | protected_topic = false 1128 | no_external_messages = false 1129 | 1130 | [[users]] 1131 | name = "lucas" 1132 | nick = "luckboy" 1133 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1134 | 1135 | [[channels]] 1136 | name = "#channel2" 1137 | topic = "Some topic 2" 1138 | [channels.modes] 1139 | key = "hokus pokus" 1140 | ban = [] 1141 | exception = [] 1142 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1143 | moderated = true 1144 | invite_only = true 1145 | client_limit = 200 1146 | secret = false 1147 | protected_topic = true 1148 | no_external_messages = false 1149 | "##, 1150 | ) 1151 | .unwrap(); 1152 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1153 | assert_eq!( 1154 | Err("operators[0].name: Validation error: Username must not \ 1155 | contains '.', ',' or ':'. [{\"value\": String(\"matis:zpaki\")}]" 1156 | .to_string()), 1157 | result 1158 | ); 1159 | 1160 | fs::write( 1161 | file_handle.path.as_str(), 1162 | r##" 1163 | name = "irci.localhost" 1164 | admin_info = "IRCI is local IRC server" 1165 | admin_info2 = "IRCI is good server" 1166 | info = "This is IRCI server" 1167 | listen = "127.0.0.1" 1168 | port = 6667 1169 | motd = "Hello, guys!" 1170 | network = "IRCInetwork" 1171 | max_connections = 4000 1172 | max_joins = 10 1173 | ping_timeout = 100 1174 | pong_timeout = 30 1175 | dns_lookup = false 1176 | log_level = "INFO" 1177 | 1178 | [default_user_modes] 1179 | invisible = false 1180 | oper = false 1181 | local_oper = false 1182 | registered = true 1183 | wallops = false 1184 | 1185 | [tls] 1186 | cert_file = "cert.crt" 1187 | cert_key_file = "cert_key.crt" 1188 | 1189 | [[operators]] 1190 | name = "matiszpaki" 1191 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1192 | 1193 | [[channels]] 1194 | name = "#channel1" 1195 | topic = "Some topic" 1196 | [channels.modes] 1197 | ban = [ 'baddi@*', 'baddi2@*' ] 1198 | exception = [ 'bobby@*', 'mati@*' ] 1199 | moderated = false 1200 | invite_only = false 1201 | secret = false 1202 | protected_topic = false 1203 | no_external_messages = false 1204 | 1205 | [[users]] 1206 | name = "lucas" 1207 | nick = "luckboy" 1208 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1209 | 1210 | [[channels]] 1211 | name = "^channel2" 1212 | topic = "Some topic 2" 1213 | [channels.modes] 1214 | key = "hokus pokus" 1215 | ban = [] 1216 | exception = [] 1217 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1218 | moderated = true 1219 | invite_only = true 1220 | client_limit = 200 1221 | secret = false 1222 | protected_topic = true 1223 | no_external_messages = false 1224 | "##, 1225 | ) 1226 | .unwrap(); 1227 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1228 | assert_eq!( 1229 | Err( 1230 | "channels[1].name: Validation error: Channel name must have '#' or \ 1231 | '&' at start and must not contains ',' or ':'. [{\"value\": String(\"^channel2\")}]" 1232 | .to_string() 1233 | ), 1234 | result 1235 | ); 1236 | 1237 | fs::write( 1238 | file_handle.path.as_str(), 1239 | r##" 1240 | name = "irci.localhost" 1241 | admin_info = "IRCI is local IRC server" 1242 | admin_info2 = "IRCI is good server" 1243 | info = "This is IRCI server" 1244 | listen = "127.0.0.1" 1245 | port = 6667 1246 | motd = "Hello, guys!" 1247 | network = "IRCInetwork" 1248 | max_connections = 4000 1249 | max_joins = 10 1250 | ping_timeout = 100 1251 | pong_timeout = 30 1252 | dns_lookup = false 1253 | log_level = "INFO" 1254 | 1255 | [default_user_modes] 1256 | invisible = false 1257 | oper = false 1258 | local_oper = false 1259 | registered = true 1260 | wallops = false 1261 | 1262 | [tls] 1263 | cert_file = "cert.crt" 1264 | cert_key_file = "cert_key.crt" 1265 | 1266 | [[operators]] 1267 | name = "matiszpaki" 1268 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1269 | 1270 | [[channels]] 1271 | name = "#channel1" 1272 | topic = "Some topic" 1273 | [channels.modes] 1274 | ban = [ 'baddi@*', 'baddi2@*' ] 1275 | exception = [ 'bobby@*', 'mati@*' ] 1276 | moderated = false 1277 | invite_only = false 1278 | secret = false 1279 | protected_topic = false 1280 | no_external_messages = false 1281 | 1282 | [[users]] 1283 | name = "lucas" 1284 | nick = "luckboy" 1285 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1286 | 1287 | [[channels]] 1288 | name = "#cha:nnel2" 1289 | topic = "Some topic 2" 1290 | [channels.modes] 1291 | key = "hokus pokus" 1292 | ban = [] 1293 | exception = [] 1294 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1295 | moderated = true 1296 | invite_only = true 1297 | client_limit = 200 1298 | secret = false 1299 | protected_topic = true 1300 | no_external_messages = false 1301 | "##, 1302 | ) 1303 | .unwrap(); 1304 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1305 | assert_eq!( 1306 | Err( 1307 | "channels[1].name: Validation error: Channel name must have '#' or \ 1308 | '&' at start and must not contains ',' or ':'. \ 1309 | [{\"value\": String(\"#cha:nnel2\")}]" 1310 | .to_string() 1311 | ), 1312 | result 1313 | ); 1314 | 1315 | fs::write( 1316 | file_handle.path.as_str(), 1317 | r##" 1318 | name = "irci.localhost" 1319 | admin_info = "IRCI is local IRC server" 1320 | admin_info2 = "IRCI is good server" 1321 | info = "This is IRCI server" 1322 | listen = "127.0.0.1" 1323 | port = 6667 1324 | password = "814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1325 | motd = "Hello, guys!" 1326 | network = "IRCInetwork" 1327 | max_connections = 4000 1328 | max_joins = 10 1329 | ping_timeout = 100 1330 | pong_timeout = 30 1331 | dns_lookup = false 1332 | log_level = "INFO" 1333 | 1334 | [default_user_modes] 1335 | invisible = false 1336 | oper = false 1337 | local_oper = false 1338 | registered = true 1339 | wallops = false 1340 | 1341 | [tls] 1342 | cert_file = "cert.crt" 1343 | cert_key_file = "cert_key.crt" 1344 | 1345 | [[operators]] 1346 | name = "matiszpaki" 1347 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1348 | 1349 | [[channels]] 1350 | name = "#channel1" 1351 | topic = "Some topic" 1352 | [channels.modes] 1353 | ban = [ 'baddi@*', 'baddi2@*' ] 1354 | exception = [ 'bobby@*', 'mati@*' ] 1355 | moderated = false 1356 | invite_only = false 1357 | secret = false 1358 | protected_topic = false 1359 | no_external_messages = false 1360 | 1361 | [[users]] 1362 | name = "lucas" 1363 | nick = "luckboy" 1364 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1365 | 1366 | [[channels]] 1367 | name = "#channel2" 1368 | topic = "Some topic 2" 1369 | [channels.modes] 1370 | key = "hokus pokus" 1371 | ban = [] 1372 | exception = [] 1373 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1374 | moderated = true 1375 | invite_only = true 1376 | client_limit = 200 1377 | secret = false 1378 | protected_topic = true 1379 | no_external_messages = false 1380 | "##, 1381 | ) 1382 | .unwrap(); 1383 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1384 | assert_eq!( 1385 | Err("password: Validation error: Wrong password hash length \ 1386 | [{\"value\": String(\"814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr\ 1387 | 4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag\")}]" 1388 | .to_string()), 1389 | result 1390 | ); 1391 | 1392 | fs::write( 1393 | file_handle.path.as_str(), 1394 | r##" 1395 | name = "irci.localhost" 1396 | admin_info = "IRCI is local IRC server" 1397 | admin_info2 = "IRCI is good server" 1398 | info = "This is IRCI server" 1399 | listen = "127.0.0.1" 1400 | port = 6667 1401 | password = "xxxxxxxxxx" 1402 | motd = "Hello, guys!" 1403 | network = "IRCInetwork" 1404 | max_connections = 4000 1405 | max_joins = 10 1406 | ping_timeout = 100 1407 | pong_timeout = 30 1408 | dns_lookup = false 1409 | log_level = "INFO" 1410 | 1411 | [default_user_modes] 1412 | invisible = false 1413 | oper = false 1414 | local_oper = false 1415 | registered = true 1416 | wallops = false 1417 | 1418 | [tls] 1419 | cert_file = "cert.crt" 1420 | cert_key_file = "cert_key.crt" 1421 | 1422 | [[operators]] 1423 | name = "matiszpaki" 1424 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1425 | 1426 | [[channels]] 1427 | name = "#channel1" 1428 | topic = "Some topic" 1429 | [channels.modes] 1430 | ban = [ 'baddi@*', 'baddi2@*' ] 1431 | exception = [ 'bobby@*', 'mati@*' ] 1432 | moderated = false 1433 | invite_only = false 1434 | secret = false 1435 | protected_topic = false 1436 | no_external_messages = false 1437 | 1438 | [[users]] 1439 | name = "lucas" 1440 | nick = "luckboy" 1441 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1442 | 1443 | [[channels]] 1444 | name = "#channel2" 1445 | topic = "Some topic 2" 1446 | [channels.modes] 1447 | key = "hokus pokus" 1448 | ban = [] 1449 | exception = [] 1450 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1451 | moderated = true 1452 | invite_only = true 1453 | client_limit = 200 1454 | secret = false 1455 | protected_topic = true 1456 | no_external_messages = false 1457 | "##, 1458 | ) 1459 | .unwrap(); 1460 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1461 | assert_eq!( 1462 | Err("password: Validation error: Wrong base64 password hash \ 1463 | [{\"value\": String(\"xxxxxxxxxx\")}]" 1464 | .to_string()), 1465 | result 1466 | ); 1467 | 1468 | fs::write( 1469 | file_handle.path.as_str(), 1470 | r##" 1471 | name = "irci.localhost" 1472 | admin_info = "IRCI is local IRC server" 1473 | admin_info2 = "IRCI is good server" 1474 | info = "This is IRCI server" 1475 | listen = "127.0.0.1" 1476 | port = 6667 1477 | password = "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJntk8HEZDgh4ctFei3bqt1r0O1VIyOV7dL+w" 1478 | motd = "Hello, guys!" 1479 | network = "IRCInetwork" 1480 | max_connections = 4000 1481 | max_joins = 10 1482 | ping_timeout = 100 1483 | pong_timeout = 30 1484 | dns_lookup = false 1485 | log_level = "INFO" 1486 | 1487 | [default_user_modes] 1488 | invisible = false 1489 | oper = false 1490 | local_oper = false 1491 | registered = true 1492 | wallops = false 1493 | 1494 | [tls] 1495 | cert_file = "cert.crt" 1496 | cert_key_file = "cert_key.crt" 1497 | 1498 | [[operators]] 1499 | name = "matiszpaki" 1500 | password = "xxxxxxx" 1501 | 1502 | [[channels]] 1503 | name = "#channel1" 1504 | topic = "Some topic" 1505 | [channels.modes] 1506 | ban = [ 'baddi@*', 'baddi2@*' ] 1507 | exception = [ 'bobby@*', 'mati@*' ] 1508 | moderated = false 1509 | invite_only = false 1510 | secret = false 1511 | protected_topic = false 1512 | no_external_messages = false 1513 | 1514 | [[users]] 1515 | name = "lucas" 1516 | nick = "luckboy" 1517 | password = "DGEKj3C60CRBF+eQQF9HCmt26ofniR373G54P9D2FsxzSXzq639lUsgEeQRlMtutYUf/nWnYSOKWIVyeMtK+ug" 1518 | 1519 | [[channels]] 1520 | name = "#channel2" 1521 | topic = "Some topic 2" 1522 | [channels.modes] 1523 | key = "hokus pokus" 1524 | ban = [] 1525 | exception = [] 1526 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1527 | moderated = true 1528 | invite_only = true 1529 | client_limit = 200 1530 | secret = false 1531 | protected_topic = true 1532 | no_external_messages = false 1533 | "##, 1534 | ) 1535 | .unwrap(); 1536 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1537 | assert_eq!( 1538 | Err( 1539 | "operators[0].password: Validation error: Wrong base64 password \ 1540 | hash [{\"value\": String(\"xxxxxxx\")}]" 1541 | .to_string() 1542 | ), 1543 | result 1544 | ); 1545 | 1546 | fs::write( 1547 | file_handle.path.as_str(), 1548 | r##" 1549 | name = "irci.localhost" 1550 | admin_info = "IRCI is local IRC server" 1551 | admin_info2 = "IRCI is good server" 1552 | info = "This is IRCI server" 1553 | listen = "127.0.0.1" 1554 | port = 6667 1555 | password = "VgWezXctjWvsY6V7gzSQPnluUuAwq06m5IxwcIg3OfBIMM+zWCJntk8HEZDgh4ctFei3bqt1r0O1VIyOV7dL+w" 1556 | motd = "Hello, guys!" 1557 | network = "IRCInetwork" 1558 | max_connections = 4000 1559 | max_joins = 10 1560 | ping_timeout = 100 1561 | pong_timeout = 30 1562 | dns_lookup = false 1563 | log_level = "INFO" 1564 | 1565 | [default_user_modes] 1566 | invisible = false 1567 | oper = false 1568 | local_oper = false 1569 | registered = true 1570 | wallops = false 1571 | 1572 | [tls] 1573 | cert_file = "cert.crt" 1574 | cert_key_file = "cert_key.crt" 1575 | 1576 | [[operators]] 1577 | name = "matiszpaki" 1578 | password = "u1hG814j88zYGsEZoKba2op9ems63On/QsqWWTFvEkUWaZFkzcr4Bri/sUIG5+u01qbfQ+GWF+PMXNFIPCJdag" 1579 | 1580 | [[channels]] 1581 | name = "#channel1" 1582 | topic = "Some topic" 1583 | [channels.modes] 1584 | ban = [ 'baddi@*', 'baddi2@*' ] 1585 | exception = [ 'bobby@*', 'mati@*' ] 1586 | moderated = false 1587 | invite_only = false 1588 | secret = false 1589 | protected_topic = false 1590 | no_external_messages = false 1591 | 1592 | [[users]] 1593 | name = "lucas" 1594 | nick = "luckboy" 1595 | password = "xxxxxxxx" 1596 | 1597 | [[channels]] 1598 | name = "#channel2" 1599 | topic = "Some topic 2" 1600 | [channels.modes] 1601 | key = "hokus pokus" 1602 | ban = [] 1603 | exception = [] 1604 | invite_exception = [ "nomi@buru.com", "pampam@zerox.net" ] 1605 | moderated = true 1606 | invite_only = true 1607 | client_limit = 200 1608 | secret = false 1609 | protected_topic = true 1610 | no_external_messages = false 1611 | "##, 1612 | ) 1613 | .unwrap(); 1614 | let result = MainConfig::new(cli.clone()).map_err(|e| e.to_string()); 1615 | assert_eq!( 1616 | Err( 1617 | "users[0].password: Validation error: Wrong base64 password hash \ 1618 | [{\"value\": String(\"xxxxxxxx\")}]" 1619 | .to_string() 1620 | ), 1621 | result 1622 | ); 1623 | } 1624 | 1625 | #[test] 1626 | fn test_usermodes_to_string() { 1627 | assert_eq!( 1628 | "+oOr".to_string(), 1629 | UserModes { 1630 | invisible: false, 1631 | oper: true, 1632 | local_oper: true, 1633 | registered: true, 1634 | wallops: false 1635 | } 1636 | .to_string() 1637 | ); 1638 | assert_eq!( 1639 | "+irw".to_string(), 1640 | UserModes { 1641 | invisible: true, 1642 | oper: false, 1643 | local_oper: false, 1644 | registered: true, 1645 | wallops: true 1646 | } 1647 | .to_string() 1648 | ); 1649 | } 1650 | 1651 | #[test] 1652 | fn test_channelmodes_to_string() { 1653 | assert_eq!( 1654 | "+itnl 10 +I somebody +o expert".to_string(), 1655 | ChannelModes { 1656 | ban: None, 1657 | exception: None, 1658 | invite_exception: Some(["somebody".to_string()].into()), 1659 | client_limit: Some(10), 1660 | key: None, 1661 | operators: Some(["expert".to_string()].into()), 1662 | half_operators: None, 1663 | voices: None, 1664 | founders: None, 1665 | protecteds: None, 1666 | invite_only: true, 1667 | moderated: false, 1668 | secret: false, 1669 | protected_topic: true, 1670 | no_external_messages: true 1671 | } 1672 | .to_string() 1673 | ); 1674 | let chm_str = ChannelModes { 1675 | ban: Some(["somebody".to_string(), "somebody2".to_string()].into()), 1676 | exception: None, 1677 | invite_exception: None, 1678 | client_limit: None, 1679 | key: Some("password".to_string()), 1680 | operators: Some(["expert".to_string()].into()), 1681 | half_operators: Some(["spec".to_string()].into()), 1682 | voices: None, 1683 | founders: None, 1684 | protecteds: None, 1685 | invite_only: false, 1686 | moderated: false, 1687 | secret: true, 1688 | protected_topic: true, 1689 | no_external_messages: false, 1690 | } 1691 | .to_string(); 1692 | assert!( 1693 | "+stk password +b somebody +b somebody2 +o expert +h spec" == chm_str 1694 | || "+stk password +b somebody2 +b somebody +o expert +h spec" == chm_str 1695 | ); 1696 | let chm_str = ChannelModes { 1697 | ban: None, 1698 | exception: None, 1699 | invite_exception: Some(["somebody".to_string()].into()), 1700 | client_limit: None, 1701 | key: None, 1702 | operators: None, 1703 | half_operators: None, 1704 | founders: None, 1705 | protecteds: None, 1706 | voices: Some(["guy1".to_string(), "guy2".to_string()].into()), 1707 | invite_only: true, 1708 | moderated: true, 1709 | secret: false, 1710 | protected_topic: false, 1711 | no_external_messages: true, 1712 | } 1713 | .to_string(); 1714 | assert!( 1715 | "+imn +I somebody +v guy1 +v guy2".to_string() == chm_str 1716 | || "+imn +I somebody +v guy2 +v guy1".to_string() == chm_str 1717 | ); 1718 | let chm_str = ChannelModes { 1719 | ban: None, 1720 | exception: None, 1721 | invite_exception: Some(["somebody".to_string()].into()), 1722 | client_limit: None, 1723 | key: None, 1724 | operators: None, 1725 | half_operators: None, 1726 | founders: Some(["guy1".to_string(), "guy2".to_string()].into()), 1727 | protecteds: None, 1728 | voices: None, 1729 | invite_only: true, 1730 | moderated: true, 1731 | secret: false, 1732 | protected_topic: false, 1733 | no_external_messages: true, 1734 | } 1735 | .to_string(); 1736 | assert!( 1737 | "+imn +I somebody +q guy1 +q guy2".to_string() == chm_str 1738 | || "+imn +I somebody +q guy2 +q guy1".to_string() == chm_str 1739 | ); 1740 | let chm_str = ChannelModes { 1741 | ban: None, 1742 | exception: None, 1743 | invite_exception: Some(["somebody".to_string()].into()), 1744 | client_limit: None, 1745 | key: None, 1746 | operators: None, 1747 | half_operators: None, 1748 | founders: None, 1749 | protecteds: Some(["guy1".to_string(), "guy2".to_string()].into()), 1750 | voices: None, 1751 | invite_only: true, 1752 | moderated: true, 1753 | secret: false, 1754 | protected_topic: false, 1755 | no_external_messages: true, 1756 | } 1757 | .to_string(); 1758 | assert!( 1759 | "+imn +I somebody +a guy1 +a guy2".to_string() == chm_str 1760 | || "+imn +I somebody +a guy2 +a guy1".to_string() == chm_str 1761 | ); 1762 | } 1763 | 1764 | #[test] 1765 | fn test_channelmodes_new_for_channel() { 1766 | let mut exp_chm = ChannelModes::default(); 1767 | exp_chm.founders = Some(["biggy".to_string()].into()); 1768 | exp_chm.operators = Some(["biggy".to_string()].into()); 1769 | assert_eq!(exp_chm, ChannelModes::new_for_channel("biggy".to_string())); 1770 | } 1771 | 1772 | #[test] 1773 | fn test_channelmodes_banned() { 1774 | let mut chm = ChannelModes::default(); 1775 | chm.ban = Some(["bom!*@*".to_string()].into()); 1776 | assert!(chm.banned("bom!bom@gugu.com")); 1777 | assert!(chm.banned("bom!bam@ggregi.com")); 1778 | assert!(!chm.banned("bam!bom@gugu.com")); 1779 | chm.exception = Some(["bom!*@ggregi*".to_string()].into()); 1780 | assert!(chm.banned("bom!bom@gugu.com")); 1781 | assert!(!chm.banned("bom!bam@ggregi.com")); 1782 | chm.exception = Some(["*!*@ggregi*".to_string()].into()); 1783 | assert!(chm.banned("bom!bom@gugu.com")); 1784 | assert!(!chm.banned("bom!bam@ggregi.com")); 1785 | chm.ban = Some(["bom!*@*".to_string(), "zigi!*@*".to_string()].into()); 1786 | assert!(chm.banned("bom!bom@gugu.com")); 1787 | assert!(chm.banned("zigi!zigol@gugu.com")); 1788 | assert!(!chm.banned("bom!bam@ggregi.com")); 1789 | assert!(!chm.banned("zigi!zigol@ggregi.net")); 1790 | } 1791 | 1792 | #[test] 1793 | fn test_channelmodes_rename_user() { 1794 | let mut chm = ChannelModes::default(); 1795 | chm.operators = Some(["bobby".to_string(), "gugu".to_string()].into()); 1796 | chm.half_operators = Some(["bobby".to_string(), "alice".to_string()].into()); 1797 | chm.voices = Some(["bobby".to_string(), "nolan".to_string()].into()); 1798 | chm.founders = Some(["bobby".to_string(), "ben".to_string()].into()); 1799 | chm.protecteds = Some(["bobby".to_string(), "irek".to_string()].into()); 1800 | chm.rename_user(&"bobby".to_string(), "robert".to_string()); 1801 | let mut exp_chm = ChannelModes::default(); 1802 | exp_chm.operators = Some(["robert".to_string(), "gugu".to_string()].into()); 1803 | exp_chm.half_operators = Some(["robert".to_string(), "alice".to_string()].into()); 1804 | exp_chm.voices = Some(["robert".to_string(), "nolan".to_string()].into()); 1805 | exp_chm.founders = Some(["robert".to_string(), "ben".to_string()].into()); 1806 | exp_chm.protecteds = Some(["robert".to_string(), "irek".to_string()].into()); 1807 | assert_eq!(exp_chm, chm); 1808 | } 1809 | } 1810 | --------------------------------------------------------------------------------