The previous .git directory was found gutted (only info/ remained), so history is unrecoverable locally. This commit restores version control with the tree exactly as it stands: the working app, the preview-tracks launcher on the artist hero, the scrollable-track-previews design bundle (docs + approved mockups), and the Phase 0 validation artifacts.
291 lines
8.7 KiB
Rust
291 lines
8.7 KiB
Rust
//! 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<u8> {
|
|
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<u8> {
|
|
Writer::new().u32(port).finish_u32(code::SET_LISTEN_PORT)
|
|
}
|
|
|
|
pub fn file_search(token: u32, query: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.u32(token)
|
|
.string(query)
|
|
.finish_u32(code::FILE_SEARCH)
|
|
}
|
|
|
|
pub fn shared_folders_and_files(dirs: u32, files: u32) -> Vec<u8> {
|
|
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<u8> {
|
|
Writer::new().bool(accept).finish_u32(code::ACCEPT_CHILDREN)
|
|
}
|
|
|
|
pub fn branch_level(level: u32) -> Vec<u8> {
|
|
Writer::new().u32(level).finish_u32(code::BRANCH_LEVEL)
|
|
}
|
|
|
|
pub fn branch_root(root: &str) -> Vec<u8> {
|
|
Writer::new().string(root).finish_u32(code::BRANCH_ROOT)
|
|
}
|
|
|
|
pub fn ping() -> Vec<u8> {
|
|
Writer::new().finish_u32(code::PING)
|
|
}
|
|
|
|
pub fn get_peer_address(username: &str) -> Vec<u8> {
|
|
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<u8> {
|
|
Writer::new()
|
|
.u32(token)
|
|
.string(username)
|
|
.string(conn_type)
|
|
.finish_u32(code::CONNECT_TO_PEER)
|
|
}
|
|
|
|
pub fn have_no_parents(value: bool) -> Vec<u8> {
|
|
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<String>,
|
|
},
|
|
/// Answer to GetPeerAddress. A 0.0.0.0/port-0 address means offline.
|
|
UserAddress {
|
|
username: String,
|
|
addr: SocketAddrV4,
|
|
},
|
|
ExcludedSearchPhrases(Vec<String>),
|
|
/// 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<ServerEvent> {
|
|
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:?}"),
|
|
}
|
|
}
|
|
}
|