feat: log in with soulseek-rs's own client major version (176) (#13)

The Login message hardcoded SoulseekQt's major version 157, so the
server could not tell soulseek-rs apart from the official client. The
protocol document now reserves a unique major version per project, so
claim 176 and send it via a new ClientVersion on ClientSettings.

Clients and bots built on the library keep major 176 and pick their own
minor version (default 1); the reference CLI/TUI logs in with minor 100.

Refs #12
This commit is contained in:
Michel de Graaf
2026-07-25 21:51:58 +02:00
committed by GitHub
parent e2c2b5063a
commit d2f3dd13d7
9 changed files with 93 additions and 39 deletions
+6 -3
View File
@@ -24,7 +24,7 @@ use crate::message::{Handlers, MessageType};
use crate::message::{Message, MessageReader};
use crate::peer::ConnectionType;
use crate::peer::Peer;
use crate::types::{RoomEvent, RoomInfo};
use crate::types::{ClientVersion, RoomEvent, RoomInfo};
use crate::utils::lock::RwLockExt;
use std::io::{self, Error, Write};
@@ -159,6 +159,7 @@ pub enum ServerMessage {
Login {
username: String,
password: String,
version: ClientVersion,
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
},
FileSearch {
@@ -533,9 +534,10 @@ impl ServerActor {
ServerMessage::Login {
username,
password,
version,
response,
} => {
self.handle_login(username, password, response);
self.handle_login(username, password, version, response);
}
ServerMessage::FileSearch { token, query } => {
self.file_search(token, &query);
@@ -640,10 +642,11 @@ impl ServerActor {
&mut self,
username: String,
password: String,
version: ClientVersion,
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
) {
self.queue_message(MessageFactory::build_login_message(
&username, &password,
&username, &password, version,
));
let start = std::time::Instant::now();
+1
View File
@@ -108,6 +108,7 @@ impl Client {
let _ = handle.send(ServerMessage::Login {
username: self.username.clone(),
password: self.password.clone(),
version: self.version,
response: tx,
});
+9 -2
View File
@@ -4,8 +4,8 @@ use crate::actor::server_actor::{
};
use crate::download_store::{DownloadStore, collect_failed_tokens};
use crate::types::{
DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo, UserInfo,
UserPresence, UserStats, UserStatus,
ClientVersion, DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo,
UserInfo, UserPresence, UserStats, UserStatus,
};
use crate::utils::logger;
use crate::{
@@ -139,6 +139,10 @@ pub struct ClientSettings {
/// Directories whose files are shared with (uploaded to) other peers.
/// Empty means nothing is shared.
pub shared_directories: Vec<String>,
/// The version reported to the server on login. Defaults to the
/// soulseek-rs major version with minor version 1; clients and bots built
/// on this library should pick their own minor version.
pub version: ClientVersion,
}
impl ClientSettings {
@@ -166,6 +170,7 @@ impl Default for ClientSettings {
enable_listen: true,
listen_port: DEFAULT_LISTEN_PORT,
shared_directories: Vec::new(),
version: ClientVersion::default(),
}
}
}
@@ -762,6 +767,7 @@ pub struct Client {
address: PeerAddress,
username: String,
password: String,
version: ClientVersion,
shared_directories: Vec<String>,
server_handle: Option<ActorHandle<ServerMessage>>,
context: Arc<RwLock<ClientContext>>,
@@ -784,6 +790,7 @@ impl Client {
address: settings.server_address,
username: settings.username,
password: settings.password,
version: settings.version,
shared_directories: settings.shared_directories,
context: Arc::new(RwLock::new(ClientContext::new())),
server_handle: None,
+6 -5
View File
@@ -15,9 +15,9 @@ pub mod utils;
pub mod prelude {
pub use crate::actor::server_actor::PeerAddress;
pub use crate::types::{
DownloadStatus, File, RoomEvent, RoomInfo, Search, SearchResult,
Transfer, UploadInfo, UploadStatus, UserInfo, UserPresence, UserStats,
UserStatus,
ClientVersion, DownloadStatus, File, RoomEvent, RoomInfo, Search,
SearchResult, Transfer, UploadInfo, UploadStatus, UserInfo,
UserPresence, UserStats, UserStatus,
};
pub use crate::{debug, error, info, trace, warn};
}
@@ -28,6 +28,7 @@ pub use client::{Client, ClientSettings};
pub use error::{Result, SoulseekRs};
pub use message::peer::SharedDirectory;
pub use types::{
DownloadStatus, File, RoomEvent, RoomInfo, Search, SearchResult, Transfer,
UploadInfo, UploadStatus, UserInfo, UserPresence, UserStats, UserStatus,
ClientVersion, DownloadStatus, File, RoomEvent, RoomInfo, Search,
SearchResult, Transfer, UploadInfo, UploadStatus, UserInfo, UserPresence,
UserStats, UserStatus,
};
@@ -1,5 +1,8 @@
use crate::{
message::Message, peer::ConnectionType, types::Transfer, utils::md5::md5,
message::Message,
peer::ConnectionType,
types::{ClientVersion, Transfer},
utils::md5::md5,
};
pub struct MessageFactory;
@@ -13,23 +16,11 @@ impl MessageFactory {
message
}
#[must_use]
pub fn build_login_message(username: &str, password: &str) -> Message {
// Message::new_with_data(
// [
// 1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95, 116, 104, 101,
// 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51, 55, 53, 49, 51, 55, 160, 0, 0,
// 0, 32, 0, 0, 0, 50, 101, 100, 102, 53, 49, 100, 48, 51, 55, 57, 52, 51, 55, 56, 102,
// 56, 98, 98, 54, 51, 49, 48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 17, 0, 0,
// 0,
// //0, // 84, 0, 0, 0, 1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95,
// // 116, 104, 101, 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51, 55, 53, 49, 51,
// // 55, 160, 0, 0, 0, 32, 0, 0, 0, 50, 101, 100, 102, 53, 49, 100, 48, 51, 55, 57, 52, 51,
// // 55, 56, 102, 56, 98, 98, 54, 51, 49, 48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 17, 0, 0,
// // 0,
// ]
// .to_vec(),
// )
// .clone()fac
pub fn build_login_message(
username: &str,
password: &str,
version: ClientVersion,
) -> Message {
let hash = md5([username, password].join("").as_str());
let mut message = Message::new();
@@ -38,9 +29,9 @@ impl MessageFactory {
.write_int32(1)
.write_string(username)
.write_string(password)
.write_int32(157) // version
.write_int32(version.major)
.write_string(&hash)
.write_int32(100)
.write_int32(version.minor)
.clone()
}
@@ -268,15 +259,18 @@ fn test_build_watch_user() {
#[test]
fn test_build_login_message() {
let message =
MessageFactory::build_login_message("insane_in_the_brain2", "13375137");
let message = MessageFactory::build_login_message(
"insane_in_the_brain2",
"13375137",
ClientVersion::default(),
);
let expect: Vec<u8> = [
1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95,
116, 104, 101, 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51,
55, 53, 49, 51, 55, 157, 0, 0, 0, 32, 0, 0, 0, 50, 101, 100, 102, 53,
55, 53, 49, 51, 55, 176, 0, 0, 0, 32, 0, 0, 0, 50, 101, 100, 102, 53,
49, 100, 48, 51, 55, 57, 52, 51, 55, 56, 102, 56, 98, 98, 54, 51, 49,
48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 100, 0, 0, 0,
48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 1, 0, 0, 0,
]
.to_vec();
+33
View File
@@ -10,6 +10,39 @@ pub struct File {
pub size: u64,
pub attribs: HashMap<u32, u32>,
}
/// The client version sent to the server on login.
///
/// Major versions identify client projects on the Soulseek network and are
/// reserved per project for the project's lifetime:
/// <https://nicotine-plus.org/doc/SLSKPROTOCOL.html#major-versions>.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientVersion {
pub major: u32,
pub minor: u32,
}
impl ClientVersion {
/// The major version reserved for the soulseek-rs project. Clients and
/// bots built on this library keep this major and pick their own non-zero
/// minor version.
pub const MAJOR: u32 = 176;
/// The version the soulseek-rs reference client (CLI/TUI) logs in with.
pub const REFERENCE_CLIENT: Self = Self {
major: Self::MAJOR,
minor: 100,
};
}
impl Default for ClientVersion {
fn default() -> Self {
Self {
major: Self::MAJOR,
minor: 1,
}
}
}
pub struct UploadFailed {
pub filename: String,
}
+16 -4
View File
@@ -28,7 +28,9 @@ use std::time::{Duration, Instant};
use soulseek_rs::message::Message;
use soulseek_rs::message::server::MessageFactory;
use soulseek_rs::peer::ConnectionType;
use soulseek_rs::{Client, ClientSettings, DownloadStatus, PeerAddress};
use soulseek_rs::{
Client, ClientSettings, ClientVersion, DownloadStatus, PeerAddress,
};
/// A Soulseek server to test against: either a child soulfind process we
/// spawned, or an external server referenced by `SOULSEEK_TEST_SERVER`.
@@ -97,6 +99,7 @@ impl TestServer {
enable_listen: false,
listen_port: 0,
shared_directories: Vec::new(),
version: ClientVersion::default(),
}
}
@@ -601,7 +604,12 @@ fn login_raw(
let mut srv = connect_retry(server_addr, Duration::from_secs(5))?;
srv.set_read_timeout(Some(Duration::from_secs(10)))?;
srv.write_all(
&MessageFactory::build_login_message(username, password).get_buffer(),
&MessageFactory::build_login_message(
username,
password,
ClientVersion::default(),
)
.get_buffer(),
)?;
srv.flush()?;
loop {
@@ -816,8 +824,12 @@ fn run_mock_direct_peer(cfg: &MockDirectUpload) -> std::io::Result<()> {
let mut srv = connect_retry(&cfg.server_addr, Duration::from_secs(5))?;
srv.set_read_timeout(Some(Duration::from_secs(10)))?;
srv.write_all(
&MessageFactory::build_login_message(&cfg.username, &cfg.password)
.get_buffer(),
&MessageFactory::build_login_message(
&cfg.username,
&cfg.password,
ClientVersion::default(),
)
.get_buffer(),
)?;
srv.flush()?;
loop {
+3 -1
View File
@@ -11,7 +11,7 @@ use clap::Parser;
use cli::{Cli, Commands, ConfigCommand, SharesCommand, parse_server_address};
use commands::Ctx;
use output::{CliError, CliResult, Exit, Out};
use soulseek_rs::{ClientSettings, PeerAddress};
use soulseek_rs::{ClientSettings, ClientVersion, PeerAddress};
use std::io::{BufRead, IsTerminal};
use std::path::PathBuf;
use std::process::ExitCode;
@@ -146,6 +146,7 @@ fn context(
enable_listen: !resolved.disable_listener,
listen_port: resolved.listener_port,
shared_directories: shared_directories(resolved, out),
version: ClientVersion::REFERENCE_CLIENT,
},
download_dir: resolved.download_dir.clone(),
max_concurrent_downloads: resolved.max_concurrent_downloads,
@@ -281,6 +282,7 @@ fn run_default_tui(
enable_listen,
listen_port,
shared_directories: shared_directories.clone(),
version: ClientVersion::REFERENCE_CLIENT,
};
// Clear screen and enable mouse capture before initializing TUI
+1
View File
@@ -442,6 +442,7 @@ impl TestServer {
enable_listen: true,
listen_port: port,
shared_directories: shares,
version: soulseek_rs::ClientVersion::default(),
});
client.connect().expect("peer connect");
assert!(client.login().expect("peer login"), "peer should log in");