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:
@@ -24,7 +24,7 @@ use crate::message::{Handlers, MessageType};
|
|||||||
use crate::message::{Message, MessageReader};
|
use crate::message::{Message, MessageReader};
|
||||||
use crate::peer::ConnectionType;
|
use crate::peer::ConnectionType;
|
||||||
use crate::peer::Peer;
|
use crate::peer::Peer;
|
||||||
use crate::types::{RoomEvent, RoomInfo};
|
use crate::types::{ClientVersion, RoomEvent, RoomInfo};
|
||||||
use crate::utils::lock::RwLockExt;
|
use crate::utils::lock::RwLockExt;
|
||||||
|
|
||||||
use std::io::{self, Error, Write};
|
use std::io::{self, Error, Write};
|
||||||
@@ -159,6 +159,7 @@ pub enum ServerMessage {
|
|||||||
Login {
|
Login {
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
version: ClientVersion,
|
||||||
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
|
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
|
||||||
},
|
},
|
||||||
FileSearch {
|
FileSearch {
|
||||||
@@ -533,9 +534,10 @@ impl ServerActor {
|
|||||||
ServerMessage::Login {
|
ServerMessage::Login {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
version,
|
||||||
response,
|
response,
|
||||||
} => {
|
} => {
|
||||||
self.handle_login(username, password, response);
|
self.handle_login(username, password, version, response);
|
||||||
}
|
}
|
||||||
ServerMessage::FileSearch { token, query } => {
|
ServerMessage::FileSearch { token, query } => {
|
||||||
self.file_search(token, &query);
|
self.file_search(token, &query);
|
||||||
@@ -640,10 +642,11 @@ impl ServerActor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
version: ClientVersion,
|
||||||
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
|
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
|
||||||
) {
|
) {
|
||||||
self.queue_message(MessageFactory::build_login_message(
|
self.queue_message(MessageFactory::build_login_message(
|
||||||
&username, &password,
|
&username, &password, version,
|
||||||
));
|
));
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ impl Client {
|
|||||||
let _ = handle.send(ServerMessage::Login {
|
let _ = handle.send(ServerMessage::Login {
|
||||||
username: self.username.clone(),
|
username: self.username.clone(),
|
||||||
password: self.password.clone(),
|
password: self.password.clone(),
|
||||||
|
version: self.version,
|
||||||
response: tx,
|
response: tx,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use crate::actor::server_actor::{
|
|||||||
};
|
};
|
||||||
use crate::download_store::{DownloadStore, collect_failed_tokens};
|
use crate::download_store::{DownloadStore, collect_failed_tokens};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo, UserInfo,
|
ClientVersion, DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo,
|
||||||
UserPresence, UserStats, UserStatus,
|
UserInfo, UserPresence, UserStats, UserStatus,
|
||||||
};
|
};
|
||||||
use crate::utils::logger;
|
use crate::utils::logger;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -139,6 +139,10 @@ pub struct ClientSettings {
|
|||||||
/// Directories whose files are shared with (uploaded to) other peers.
|
/// Directories whose files are shared with (uploaded to) other peers.
|
||||||
/// Empty means nothing is shared.
|
/// Empty means nothing is shared.
|
||||||
pub shared_directories: Vec<String>,
|
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 {
|
impl ClientSettings {
|
||||||
@@ -166,6 +170,7 @@ impl Default for ClientSettings {
|
|||||||
enable_listen: true,
|
enable_listen: true,
|
||||||
listen_port: DEFAULT_LISTEN_PORT,
|
listen_port: DEFAULT_LISTEN_PORT,
|
||||||
shared_directories: Vec::new(),
|
shared_directories: Vec::new(),
|
||||||
|
version: ClientVersion::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,6 +767,7 @@ pub struct Client {
|
|||||||
address: PeerAddress,
|
address: PeerAddress,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
version: ClientVersion,
|
||||||
shared_directories: Vec<String>,
|
shared_directories: Vec<String>,
|
||||||
server_handle: Option<ActorHandle<ServerMessage>>,
|
server_handle: Option<ActorHandle<ServerMessage>>,
|
||||||
context: Arc<RwLock<ClientContext>>,
|
context: Arc<RwLock<ClientContext>>,
|
||||||
@@ -784,6 +790,7 @@ impl Client {
|
|||||||
address: settings.server_address,
|
address: settings.server_address,
|
||||||
username: settings.username,
|
username: settings.username,
|
||||||
password: settings.password,
|
password: settings.password,
|
||||||
|
version: settings.version,
|
||||||
shared_directories: settings.shared_directories,
|
shared_directories: settings.shared_directories,
|
||||||
context: Arc::new(RwLock::new(ClientContext::new())),
|
context: Arc::new(RwLock::new(ClientContext::new())),
|
||||||
server_handle: None,
|
server_handle: None,
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ pub mod utils;
|
|||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::actor::server_actor::PeerAddress;
|
pub use crate::actor::server_actor::PeerAddress;
|
||||||
pub use crate::types::{
|
pub use crate::types::{
|
||||||
DownloadStatus, File, RoomEvent, RoomInfo, Search, SearchResult,
|
ClientVersion, DownloadStatus, File, RoomEvent, RoomInfo, Search,
|
||||||
Transfer, UploadInfo, UploadStatus, UserInfo, UserPresence, UserStats,
|
SearchResult, Transfer, UploadInfo, UploadStatus, UserInfo,
|
||||||
UserStatus,
|
UserPresence, UserStats, UserStatus,
|
||||||
};
|
};
|
||||||
pub use crate::{debug, error, info, trace, warn};
|
pub use crate::{debug, error, info, trace, warn};
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ pub use client::{Client, ClientSettings};
|
|||||||
pub use error::{Result, SoulseekRs};
|
pub use error::{Result, SoulseekRs};
|
||||||
pub use message::peer::SharedDirectory;
|
pub use message::peer::SharedDirectory;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
DownloadStatus, File, RoomEvent, RoomInfo, Search, SearchResult, Transfer,
|
ClientVersion, DownloadStatus, File, RoomEvent, RoomInfo, Search,
|
||||||
UploadInfo, UploadStatus, UserInfo, UserPresence, UserStats, UserStatus,
|
SearchResult, Transfer, UploadInfo, UploadStatus, UserInfo, UserPresence,
|
||||||
|
UserStats, UserStatus,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
message::Message, peer::ConnectionType, types::Transfer, utils::md5::md5,
|
message::Message,
|
||||||
|
peer::ConnectionType,
|
||||||
|
types::{ClientVersion, Transfer},
|
||||||
|
utils::md5::md5,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MessageFactory;
|
pub struct MessageFactory;
|
||||||
@@ -13,23 +16,11 @@ impl MessageFactory {
|
|||||||
message
|
message
|
||||||
}
|
}
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build_login_message(username: &str, password: &str) -> Message {
|
pub fn build_login_message(
|
||||||
// Message::new_with_data(
|
username: &str,
|
||||||
// [
|
password: &str,
|
||||||
// 1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95, 116, 104, 101,
|
version: ClientVersion,
|
||||||
// 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51, 55, 53, 49, 51, 55, 160, 0, 0,
|
) -> Message {
|
||||||
// 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
|
|
||||||
let hash = md5([username, password].join("").as_str());
|
let hash = md5([username, password].join("").as_str());
|
||||||
|
|
||||||
let mut message = Message::new();
|
let mut message = Message::new();
|
||||||
@@ -38,9 +29,9 @@ impl MessageFactory {
|
|||||||
.write_int32(1)
|
.write_int32(1)
|
||||||
.write_string(username)
|
.write_string(username)
|
||||||
.write_string(password)
|
.write_string(password)
|
||||||
.write_int32(157) // version
|
.write_int32(version.major)
|
||||||
.write_string(&hash)
|
.write_string(&hash)
|
||||||
.write_int32(100)
|
.write_int32(version.minor)
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,15 +259,18 @@ fn test_build_watch_user() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_login_message() {
|
fn test_build_login_message() {
|
||||||
let message =
|
let message = MessageFactory::build_login_message(
|
||||||
MessageFactory::build_login_message("insane_in_the_brain2", "13375137");
|
"insane_in_the_brain2",
|
||||||
|
"13375137",
|
||||||
|
ClientVersion::default(),
|
||||||
|
);
|
||||||
|
|
||||||
let expect: Vec<u8> = [
|
let expect: Vec<u8> = [
|
||||||
1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95,
|
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,
|
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,
|
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();
|
.to_vec();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,39 @@ pub struct File {
|
|||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub attribs: HashMap<u32, u32>,
|
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 struct UploadFailed {
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ use std::time::{Duration, Instant};
|
|||||||
use soulseek_rs::message::Message;
|
use soulseek_rs::message::Message;
|
||||||
use soulseek_rs::message::server::MessageFactory;
|
use soulseek_rs::message::server::MessageFactory;
|
||||||
use soulseek_rs::peer::ConnectionType;
|
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
|
/// A Soulseek server to test against: either a child soulfind process we
|
||||||
/// spawned, or an external server referenced by `SOULSEEK_TEST_SERVER`.
|
/// spawned, or an external server referenced by `SOULSEEK_TEST_SERVER`.
|
||||||
@@ -97,6 +99,7 @@ impl TestServer {
|
|||||||
enable_listen: false,
|
enable_listen: false,
|
||||||
listen_port: 0,
|
listen_port: 0,
|
||||||
shared_directories: Vec::new(),
|
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))?;
|
let mut srv = connect_retry(server_addr, Duration::from_secs(5))?;
|
||||||
srv.set_read_timeout(Some(Duration::from_secs(10)))?;
|
srv.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||||
srv.write_all(
|
srv.write_all(
|
||||||
&MessageFactory::build_login_message(username, password).get_buffer(),
|
&MessageFactory::build_login_message(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
ClientVersion::default(),
|
||||||
|
)
|
||||||
|
.get_buffer(),
|
||||||
)?;
|
)?;
|
||||||
srv.flush()?;
|
srv.flush()?;
|
||||||
loop {
|
loop {
|
||||||
@@ -816,7 +824,11 @@ fn run_mock_direct_peer(cfg: &MockDirectUpload) -> std::io::Result<()> {
|
|||||||
let mut srv = connect_retry(&cfg.server_addr, Duration::from_secs(5))?;
|
let mut srv = connect_retry(&cfg.server_addr, Duration::from_secs(5))?;
|
||||||
srv.set_read_timeout(Some(Duration::from_secs(10)))?;
|
srv.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||||
srv.write_all(
|
srv.write_all(
|
||||||
&MessageFactory::build_login_message(&cfg.username, &cfg.password)
|
&MessageFactory::build_login_message(
|
||||||
|
&cfg.username,
|
||||||
|
&cfg.password,
|
||||||
|
ClientVersion::default(),
|
||||||
|
)
|
||||||
.get_buffer(),
|
.get_buffer(),
|
||||||
)?;
|
)?;
|
||||||
srv.flush()?;
|
srv.flush()?;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use clap::Parser;
|
|||||||
use cli::{Cli, Commands, ConfigCommand, SharesCommand, parse_server_address};
|
use cli::{Cli, Commands, ConfigCommand, SharesCommand, parse_server_address};
|
||||||
use commands::Ctx;
|
use commands::Ctx;
|
||||||
use output::{CliError, CliResult, Exit, Out};
|
use output::{CliError, CliResult, Exit, Out};
|
||||||
use soulseek_rs::{ClientSettings, PeerAddress};
|
use soulseek_rs::{ClientSettings, ClientVersion, PeerAddress};
|
||||||
use std::io::{BufRead, IsTerminal};
|
use std::io::{BufRead, IsTerminal};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
@@ -146,6 +146,7 @@ fn context(
|
|||||||
enable_listen: !resolved.disable_listener,
|
enable_listen: !resolved.disable_listener,
|
||||||
listen_port: resolved.listener_port,
|
listen_port: resolved.listener_port,
|
||||||
shared_directories: shared_directories(resolved, out),
|
shared_directories: shared_directories(resolved, out),
|
||||||
|
version: ClientVersion::REFERENCE_CLIENT,
|
||||||
},
|
},
|
||||||
download_dir: resolved.download_dir.clone(),
|
download_dir: resolved.download_dir.clone(),
|
||||||
max_concurrent_downloads: resolved.max_concurrent_downloads,
|
max_concurrent_downloads: resolved.max_concurrent_downloads,
|
||||||
@@ -281,6 +282,7 @@ fn run_default_tui(
|
|||||||
enable_listen,
|
enable_listen,
|
||||||
listen_port,
|
listen_port,
|
||||||
shared_directories: shared_directories.clone(),
|
shared_directories: shared_directories.clone(),
|
||||||
|
version: ClientVersion::REFERENCE_CLIENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear screen and enable mouse capture before initializing TUI
|
// Clear screen and enable mouse capture before initializing TUI
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ impl TestServer {
|
|||||||
enable_listen: true,
|
enable_listen: true,
|
||||||
listen_port: port,
|
listen_port: port,
|
||||||
shared_directories: shares,
|
shared_directories: shares,
|
||||||
|
version: soulseek_rs::ClientVersion::default(),
|
||||||
});
|
});
|
||||||
client.connect().expect("peer connect");
|
client.connect().expect("peer connect");
|
||||||
assert!(client.login().expect("peer login"), "peer should log in");
|
assert!(client.login().expect("peer login"), "peer should log in");
|
||||||
|
|||||||
Reference in New Issue
Block a user