forked from Keelrleek/SoulStream
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.
This commit is contained in:
commit
a667b4b28e
161 files changed
+53170
No files matched your search
@@ -0,0 +1,290 @@
|
||||
//! 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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user