Files
SoulStream/crates/soulseek-proto/src/server.rs
T
KeelrLeek a667b4b28e Baseline: full tree snapshot after git repository loss
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.
2026-07-22 17:57:00 -05:00

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:?}"),
}
}
}