//! Server messages (`server.slsknet.org:2242`), u32 codes. use bytes::Bytes; use md5::{Digest, Md5}; use std::net::{Ipv4Addr, SocketAddrV4}; use crate::wire::{Reader, Writer}; use crate::Result; /// Client version fields sent at login. 160 is the protocol version spoken /// by current clients (Nicotine+ uses the same). pub const VERSION: u32 = 160; pub const MINOR_VERSION: u32 = 1; pub mod code { pub const LOGIN: u32 = 1; pub const SET_LISTEN_PORT: u32 = 2; pub const GET_PEER_ADDRESS: u32 = 3; pub const WATCH_USER: u32 = 5; pub const CONNECT_TO_PEER: u32 = 18; pub const FILE_SEARCH: u32 = 26; pub const PING: u32 = 32; pub const SHARED_FOLDERS_AND_FILES: u32 = 35; pub const HAVE_NO_PARENTS: u32 = 71; pub const PARENT_MIN_SPEED: u32 = 83; pub const PARENT_SPEED_RATIO: u32 = 84; pub const EMBEDDED_MESSAGE: u32 = 93; // wraps a distributed search pub const ACCEPT_CHILDREN: u32 = 100; pub const NET_INFO: u32 = 102; pub const WISHLIST_INTERVAL: u32 = 104; pub const BRANCH_LEVEL: u32 = 126; pub const BRANCH_ROOT: u32 = 127; pub const EXCLUDED_SEARCH_PHRASES: u32 = 160; pub const CANNOT_CONNECT: u32 = 1001; } // ---- outgoing ---- pub fn login(username: &str, password: &str) -> Vec { let hash = hex(Md5::digest(format!("{username}{password}").as_bytes())); Writer::new() .string(username) .string(password) .u32(VERSION) .string(&hash) .u32(MINOR_VERSION) .finish_u32(code::LOGIN) } pub fn set_listen_port(port: u32) -> Vec { Writer::new().u32(port).finish_u32(code::SET_LISTEN_PORT) } pub fn file_search(token: u32, query: &str) -> Vec { Writer::new() .u32(token) .string(query) .finish_u32(code::FILE_SEARCH) } pub fn shared_folders_and_files(dirs: u32, files: u32) -> Vec { Writer::new() .u32(dirs) .u32(files) .finish_u32(code::SHARED_FOLDERS_AND_FILES) } /// Tell the server whether we need a distributed parent. pub fn accept_children(accept: bool) -> Vec { Writer::new().bool(accept).finish_u32(code::ACCEPT_CHILDREN) } pub fn branch_level(level: u32) -> Vec { Writer::new().u32(level).finish_u32(code::BRANCH_LEVEL) } pub fn branch_root(root: &str) -> Vec { Writer::new().string(root).finish_u32(code::BRANCH_ROOT) } pub fn ping() -> Vec { Writer::new().finish_u32(code::PING) } pub fn get_peer_address(username: &str) -> Vec { Writer::new() .string(username) .finish_u32(code::GET_PEER_ADDRESS) } /// Ask the server to have `username` connect to us (indirect connection). pub fn connect_to_peer(token: u32, username: &str, conn_type: &str) -> Vec { Writer::new() .u32(token) .string(username) .string(conn_type) .finish_u32(code::CONNECT_TO_PEER) } pub fn have_no_parents(value: bool) -> Vec { Writer::new().bool(value).finish_u32(code::HAVE_NO_PARENTS) } // ---- incoming ---- #[derive(Debug, Clone)] pub enum ServerEvent { LoginOk { greeting: String, own_ip: Ipv4Addr, is_supporter: bool, }, LoginFailed { reason: String, }, /// Another client wants a connection with us (or is answering our /// indirect request): connect to them and send PierceFirewall(token). ConnectToPeer { username: String, conn_type: String, addr: SocketAddrV4, token: u32, privileged: bool, }, /// Our indirect connection request to `token` failed. CannotConnect { token: u32, username: Option, }, /// Answer to GetPeerAddress. A 0.0.0.0/port-0 address means offline. UserAddress { username: String, addr: SocketAddrV4, }, ExcludedSearchPhrases(Vec), /// Candidate distributed parents to connect to. NetInfo(Vec<(String, SocketAddrV4)>), /// A search relayed directly by the server (older path). ServerSearch { username: String, token: u32, query: String, }, /// A distributed message wrapped by the server (code 93). The inner /// bytes are a distributed frame starting with its code byte — decode /// with `dist::decode`. Embedded(Bytes), Unhandled { code: u32, }, } pub fn decode(code: u32, payload: Bytes) -> Result { let mut r = Reader::new(payload); Ok(match code { code::LOGIN => { if r.bool()? { let greeting = r.string()?; let own_ip = r.ipv4()?; // Trailing hash + supporter flag are absent on some servers. let _hash = r.string().unwrap_or_default(); let is_supporter = r.bool().unwrap_or(false); ServerEvent::LoginOk { greeting, own_ip, is_supporter, } } else { ServerEvent::LoginFailed { reason: r.string()?, } } } code::CONNECT_TO_PEER => { let username = r.string()?; let conn_type = r.string()?; let ip = r.ipv4()?; let port = r.u32()?; let token = r.u32()?; let privileged = r.bool().unwrap_or(false); ServerEvent::ConnectToPeer { username, conn_type, addr: SocketAddrV4::new(ip, port as u16), token, privileged, } } code::GET_PEER_ADDRESS => { let username = r.string()?; let ip = r.ipv4()?; let port = r.u32()?; // Trailing obfuscation fields ignored. ServerEvent::UserAddress { username, addr: SocketAddrV4::new(ip, port as u16), } } code::CANNOT_CONNECT => { let token = r.u32()?; let username = r.string().ok(); ServerEvent::CannotConnect { token, username } } code::NET_INFO => { let count = r.u32()?.min(1000); let mut parents = Vec::with_capacity(count as usize); for _ in 0..count { let username = r.string()?; let ip = r.ipv4()?; let port = r.u32()?; parents.push((username, SocketAddrV4::new(ip, port as u16))); } ServerEvent::NetInfo(parents) } code::FILE_SEARCH => { // Server-relayed search (same code we send): username, token, query. let username = r.string()?; let token = r.u32()?; let query = r.string()?; ServerEvent::ServerSearch { username, token, query, } } code::EMBEDDED_MESSAGE => ServerEvent::Embedded(r.rest()), code::EXCLUDED_SEARCH_PHRASES => { let count = r.u32()?.min(10_000); let mut phrases = Vec::with_capacity(count as usize); for _ in 0..count { phrases.push(r.string()?); } ServerEvent::ExcludedSearchPhrases(phrases) } other => ServerEvent::Unhandled { code: other }, }) } fn hex(digest: impl AsRef<[u8]>) -> String { digest.as_ref().iter().map(|b| format!("{b:02x}")).collect() } #[cfg(test)] mod tests { use super::*; #[test] fn login_message_layout() { let msg = login("user", "pass"); let mut r = Reader::new(Bytes::copy_from_slice(&msg[8..])); assert_eq!(&msg[4..8], &1u32.to_le_bytes()); assert_eq!(r.string().unwrap(), "user"); assert_eq!(r.string().unwrap(), "pass"); assert_eq!(r.u32().unwrap(), VERSION); // md5("userpass") assert_eq!(r.string().unwrap(), "63e780c3f321d13109c71bf81805476e"); assert_eq!(r.u32().unwrap(), MINOR_VERSION); } #[test] fn decode_connect_to_peer() { let payload = Writer::new() .string("bob") .string("P") .u8(4) .u8(3) .u8(2) .u8(1) // 1.2.3.4 reversed on wire .u32(2234) .u32(99) .bool(false) .finish_u32(18); let ev = decode(18, Bytes::copy_from_slice(&payload[8..])).unwrap(); match ev { ServerEvent::ConnectToPeer { username, conn_type, addr, token, .. } => { assert_eq!(username, "bob"); assert_eq!(conn_type, "P"); assert_eq!(addr, "1.2.3.4:2234".parse().unwrap()); assert_eq!(token, 99); } other => panic!("wrong event: {other:?}"), } } }