Implement native login and session initialization (#56)
All checks were successful
Native code generation / deterministic (push) Successful in 11m53s
Imaging and meshing gate / native (push) Successful in 4m3s
JPEG 2000 feature / linux (push) Successful in 2m33s
Native Rust workspace compile / compile (push) Successful in 4m3s
Skia feature / linux (push) Successful in 31m24s
All checks were successful
Native code generation / deterministic (push) Successful in 11m53s
Imaging and meshing gate / native (push) Successful in 4m3s
JPEG 2000 feature / linux (push) Successful in 2m33s
Native Rust workspace compile / compile (push) Successful in 4m3s
Skia feature / linux (push) Successful in 31m24s
This commit is contained in:
@@ -19,6 +19,8 @@ futures-util = "0.3.31"
|
||||
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
|
||||
libremetaverse-structured-data = { path = "../libremetaverse-structured-data" }
|
||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||
mac_address2 = "2.0.2"
|
||||
os_info = { version = "3.15.0", default-features = false }
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "stream"] }
|
||||
roxmltree = "0.21.1"
|
||||
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
|
||||
|
||||
@@ -116,6 +116,7 @@ struct ClientRuntime {
|
||||
agent_throttle_sender: Mutex<Option<Arc<dyn crate::udp_transport::AgentThrottleSender>>>,
|
||||
http_caps_client: Mutex<crate::caps_http::HttpCapsClient>,
|
||||
caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
|
||||
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
|
||||
shutdown_complete: Condvar,
|
||||
shutdown_wait: Mutex<()>,
|
||||
}
|
||||
@@ -142,6 +143,7 @@ impl ClientRuntime {
|
||||
agent_throttle_sender: Mutex::new(None),
|
||||
http_caps_client: Mutex::new(http_caps_client),
|
||||
caps_rate_limiter: Mutex::new(caps_rate_limiter),
|
||||
network_manager: Mutex::new(std::sync::Weak::new()),
|
||||
shutdown_complete: Condvar::new(),
|
||||
shutdown_wait: Mutex::new(()),
|
||||
}
|
||||
@@ -350,6 +352,31 @@ impl GridClient {
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_network(&self) -> Result<crate::NetworkManager, crate::Error> {
|
||||
let mut cached = self
|
||||
.runtime
|
||||
.network_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(inner) = cached.upgrade() {
|
||||
return Ok(crate::network_manager::NetworkManager::native_from_inner(
|
||||
inner,
|
||||
));
|
||||
}
|
||||
let manager = crate::network_manager::NetworkManager::native_new(self.clone())?;
|
||||
*cached = manager.native_inner_weak();
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
|
||||
pub(crate) fn native_set_network(&self, value: crate::NetworkManager) {
|
||||
*self
|
||||
.runtime
|
||||
.network_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value.native_inner_weak();
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_caps_rate_limiter(
|
||||
&mut self,
|
||||
value: crate::caps_http::CapsRateLimiter,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ mod generated;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod genepool_catalog;
|
||||
mod j2k;
|
||||
mod login;
|
||||
mod message_codec;
|
||||
mod message_decoder;
|
||||
mod network_manager;
|
||||
|
||||
1687
crates/libremetaverse/src/login.rs
Normal file
1687
crates/libremetaverse/src/login.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map.
|
||||
#![allow(clippy::needless_pass_by_value)] // Owned mapped parameters preserve the public API.
|
||||
#![allow(clippy::too_many_arguments)] // Login overloads preserve the mapped public API.
|
||||
#![allow(clippy::too_many_lines)] // Packet dispatch keeps the complete protocol switch together.
|
||||
#![allow(clippy::type_complexity)] // Delegate signatures are fixed by the compatibility map.
|
||||
|
||||
@@ -18,11 +19,15 @@ use crate::packets::{
|
||||
};
|
||||
use crate::udp_transport::{UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransportConfig};
|
||||
use crate::{
|
||||
Avatar, Caps, Error, GenericStreamingMethod, GridClient, Helpers, LoginResponseData, Primitive,
|
||||
RegionFlags, RegionProtocols, SimAccess, SimulatorDataPool, SimulatorFeatures,
|
||||
SimulatorSimStats, TerrainPatch,
|
||||
AccountLevelBenefits, Avatar, Caps, Error, GenericStreamingMethod, GridClient, Helpers,
|
||||
LoginCredential, LoginParams, LoginProgressEventArgs, LoginResponseData, LoginStatus,
|
||||
NetworkManagerLoginResponseCallback, Primitive, RegionFlags, RegionProtocols, SimAccess,
|
||||
SimulatorDataPool, SimulatorFeatures, SimulatorSimStats, TerrainPatch,
|
||||
};
|
||||
use libremetaverse_structured_data::{OSD, OSDFormat, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, CancellationTokenSource, EventHandler, Object, Subscription, Uri,
|
||||
};
|
||||
use libremetaverse_types::compat::{EventHandler, Object, Subscription, Uri};
|
||||
use libremetaverse_types::{UUID, Vector2};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
@@ -90,6 +95,76 @@ fn positive_millisecond_duration(value: i32) -> Duration {
|
||||
Duration::from_millis(u64::try_from(value.max(1)).unwrap_or(1))
|
||||
}
|
||||
|
||||
fn validate_login_uri(value: &str) -> Result<Uri, Error> {
|
||||
let parsed = reqwest::Url::parse(value).map_err(|_| Error::Argument)?;
|
||||
if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Uri(parsed.to_string()))
|
||||
}
|
||||
|
||||
struct LoginWireSecrets {
|
||||
password: String,
|
||||
token: String,
|
||||
mfa_hash: String,
|
||||
}
|
||||
|
||||
impl Drop for LoginWireSecrets {
|
||||
fn drop(&mut self) {
|
||||
// Overwrite the live buffers before releasing them. This cannot erase
|
||||
// allocator copies, but it prevents the owned request state surviving
|
||||
// the login/redirect sequence.
|
||||
self.password
|
||||
.replace_range(.., &"\0".repeat(self.password.len()));
|
||||
self.token.replace_range(.., &"\0".repeat(self.token.len()));
|
||||
self.mfa_hash
|
||||
.replace_range(.., &"\0".repeat(self.mfa_hash.len()));
|
||||
self.password.clear();
|
||||
self.token.clear();
|
||||
self.mfa_hash.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn login_request(params: &LoginParams, secrets: &LoginWireSecrets) -> OSD {
|
||||
let mut map = HashMap::from([
|
||||
("first".to_owned(), OSD::String(params.first_name.clone())),
|
||||
("last".to_owned(), OSD::String(params.last_name.clone())),
|
||||
("passwd".to_owned(), OSD::String(secrets.password.clone())),
|
||||
("start".to_owned(), OSD::String(params.start.clone())),
|
||||
("channel".to_owned(), OSD::String(params.channel.clone())),
|
||||
("version".to_owned(), OSD::String(params.version.clone())),
|
||||
("platform".to_owned(), OSD::String(params.platform.clone())),
|
||||
(
|
||||
"platform_version".to_owned(),
|
||||
OSD::String(params.platform_version.clone()),
|
||||
),
|
||||
("mac".to_owned(), OSD::String(params.mac.clone())),
|
||||
("agree_to_tos".to_owned(), OSD::Boolean(params.agree_to_tos)),
|
||||
(
|
||||
"read_critical".to_owned(),
|
||||
OSD::Boolean(params.read_critical),
|
||||
),
|
||||
(
|
||||
"viewer_digest".to_owned(),
|
||||
OSD::String(params.viewer_digest.clone()),
|
||||
),
|
||||
("id0".to_owned(), OSD::String(params.id0.clone())),
|
||||
(
|
||||
"last_exec_event".to_owned(),
|
||||
OSD::Integer(params.last_exec_event as i32),
|
||||
),
|
||||
(
|
||||
"options".to_owned(),
|
||||
OSD::Array(params.options.iter().cloned().map(OSD::String).collect()),
|
||||
),
|
||||
]);
|
||||
if params.mfa_enabled {
|
||||
map.insert("token".to_owned(), OSD::String(secrets.token.clone()));
|
||||
map.insert("mfa_hash".to_owned(), OSD::String(secrets.mfa_hash.clone()));
|
||||
}
|
||||
OSD::Map(map)
|
||||
}
|
||||
|
||||
async fn run_blocking_compat<T: Send + 'static>(
|
||||
name: &str,
|
||||
operation: impl FnOnce() -> T + Send + 'static,
|
||||
@@ -857,6 +932,8 @@ pub struct SimulatorData {
|
||||
handshake_wait_lock: Mutex<()>,
|
||||
disconnect_candidate: AtomicBool,
|
||||
circuit_code: AtomicU32,
|
||||
agent_id: RwLock<UUID>,
|
||||
session_id: RwLock<UUID>,
|
||||
seed_caps: RwLock<Option<Uri>>,
|
||||
caps_state: RwLock<Option<Arc<crate::caps::CapsInner>>>,
|
||||
event_queue_generation: Mutex<u64>,
|
||||
@@ -1018,6 +1095,8 @@ impl Simulator {
|
||||
handshake_wait_lock: Mutex::new(()),
|
||||
disconnect_candidate: AtomicBool::new(false),
|
||||
circuit_code: AtomicU32::new(0),
|
||||
agent_id: RwLock::new(UUID::zero()),
|
||||
session_id: RwLock::new(UUID::zero()),
|
||||
seed_caps: RwLock::new(None),
|
||||
caps_state: RwLock::new(None),
|
||||
event_queue_generation: Mutex::new(0),
|
||||
@@ -1058,6 +1137,8 @@ impl Simulator {
|
||||
fn attach_manager(&self, manager: &Arc<NetworkManagerInner>, circuit_code: u32) {
|
||||
*mutex(&self.manager) = Arc::downgrade(manager);
|
||||
self.circuit_code.store(circuit_code, Ordering::Release);
|
||||
*write(&self.agent_id) = *read(&manager.agent_id);
|
||||
*write(&self.session_id) = *read(&manager.session_id);
|
||||
}
|
||||
|
||||
pub fn native_connect(&self, _move_to_sim: bool) -> Result<bool, Error> {
|
||||
@@ -1414,8 +1495,8 @@ impl Simulator {
|
||||
fn use_circuit_code_bytes(&self) -> Result<Vec<u8>, Error> {
|
||||
let mut packet = UseCircuitCodePacket::new_with_constructor()?;
|
||||
packet.circuit_code.code = self.circuit_code.load(Ordering::Acquire);
|
||||
packet.circuit_code.id = UUID::zero();
|
||||
packet.circuit_code.session_id = UUID::zero();
|
||||
packet.circuit_code.id = *read(&self.agent_id);
|
||||
packet.circuit_code.session_id = *read(&self.session_id);
|
||||
packet.to_bytes_with_method()
|
||||
}
|
||||
|
||||
@@ -1566,6 +1647,7 @@ struct NetworkEvents {
|
||||
disconnected: EventRegistry<DisconnectedEventArgs>,
|
||||
event_queue_running: EventRegistry<EventQueueRunningEventArgs>,
|
||||
generic_streaming_message: EventRegistry<GenericStreamingMessageEventArgs>,
|
||||
login_progress: EventRegistry<LoginProgressEventArgs>,
|
||||
packet_sent: EventRegistry<PacketSentEventArgs>,
|
||||
sim_changed: EventRegistry<SimChangedEventArgs>,
|
||||
sim_connected: EventRegistry<SimConnectedEventArgs>,
|
||||
@@ -1573,7 +1655,23 @@ struct NetworkEvents {
|
||||
sim_disconnected: EventRegistry<SimDisconnectedEventArgs>,
|
||||
}
|
||||
|
||||
struct NetworkManagerInner {
|
||||
struct LoginRuntimeState {
|
||||
next_generation: u64,
|
||||
active: Option<(u64, CancellationTokenSource)>,
|
||||
response_callbacks: Vec<(NetworkManagerLoginResponseCallback, Option<Vec<String>>)>,
|
||||
}
|
||||
|
||||
impl Default for LoginRuntimeState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
next_generation: 1,
|
||||
active: None,
|
||||
response_callbacks: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct NetworkManagerInner {
|
||||
client: GridClient,
|
||||
simulators: SimulatorCollection,
|
||||
packet_events: PacketEventDictionary,
|
||||
@@ -1582,6 +1680,14 @@ struct NetworkManagerInner {
|
||||
current_sim: RwLock<Option<Simulator>>,
|
||||
connected: AtomicBool,
|
||||
circuit_code: AtomicU32,
|
||||
agent_id: RwLock<UUID>,
|
||||
session_id: RwLock<UUID>,
|
||||
login_status: RwLock<LoginStatus>,
|
||||
login_error_key: RwLock<String>,
|
||||
login_message: RwLock<String>,
|
||||
login_seed_capability: RwLock<Option<Uri>>,
|
||||
login_response: LoginResponseData,
|
||||
login_runtime: Mutex<LoginRuntimeState>,
|
||||
inbox_count: AtomicI32,
|
||||
outbox_count: AtomicI32,
|
||||
workers: Mutex<Option<NetworkWorkers>>,
|
||||
@@ -2039,10 +2145,23 @@ impl fmt::Debug for NetworkManager {
|
||||
}
|
||||
|
||||
impl NetworkManager {
|
||||
pub(crate) fn native_from_inner(inner: Arc<NetworkManagerInner>) -> Self {
|
||||
Self {
|
||||
login_response_data: Some(inner.login_response.clone()),
|
||||
simulators: inner.simulators.clone(),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_inner_weak(&self) -> Weak<NetworkManagerInner> {
|
||||
Arc::downgrade(&self.inner)
|
||||
}
|
||||
|
||||
pub fn native_new(client: GridClient) -> Result<Self, Error> {
|
||||
let simulators = SimulatorCollection::default();
|
||||
let packet_events = PacketEventDictionary::new(client.clone())?;
|
||||
let caps_events = CapsEventDictionary::new(client.clone())?;
|
||||
let login_response = LoginResponseData::new()?;
|
||||
let inner = Arc::new(NetworkManagerInner {
|
||||
client,
|
||||
simulators: simulators.clone(),
|
||||
@@ -2052,6 +2171,14 @@ impl NetworkManager {
|
||||
current_sim: RwLock::new(None),
|
||||
connected: AtomicBool::new(false),
|
||||
circuit_code: AtomicU32::new(0),
|
||||
agent_id: RwLock::new(UUID::zero()),
|
||||
session_id: RwLock::new(UUID::zero()),
|
||||
login_status: RwLock::new(LoginStatus::None),
|
||||
login_error_key: RwLock::new(String::new()),
|
||||
login_message: RwLock::new(String::new()),
|
||||
login_seed_capability: RwLock::new(None),
|
||||
login_response: login_response.clone(),
|
||||
login_runtime: Mutex::new(LoginRuntimeState::default()),
|
||||
inbox_count: AtomicI32::new(0),
|
||||
outbox_count: AtomicI32::new(0),
|
||||
workers: Mutex::new(None),
|
||||
@@ -2075,7 +2202,7 @@ impl NetworkManager {
|
||||
return;
|
||||
};
|
||||
let manager = NetworkManager {
|
||||
login_response_data: None,
|
||||
login_response_data: Some(inner.login_response.clone()),
|
||||
simulators: inner.simulators.clone(),
|
||||
inner: Arc::clone(&inner),
|
||||
};
|
||||
@@ -2099,12 +2226,623 @@ impl NetworkManager {
|
||||
}),
|
||||
)?;
|
||||
Ok(Self {
|
||||
login_response_data: None,
|
||||
login_response_data: Some(login_response),
|
||||
simulators,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn native_subscribe_login_progress(
|
||||
&self,
|
||||
handler: EventHandler<LoginProgressEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.events.login_progress.subscribe(handler)
|
||||
}
|
||||
|
||||
fn update_login_status(&self, status: LoginStatus, message: String) {
|
||||
if status == LoginStatus::Failed {
|
||||
*write(&self.inner.agent_id) = UUID::zero();
|
||||
*write(&self.inner.session_id) = UUID::zero();
|
||||
*write(&self.inner.login_seed_capability) = None;
|
||||
self.inner.login_response.clear_secrets();
|
||||
}
|
||||
*write(&self.inner.login_status) = status;
|
||||
write(&self.inner.login_message).clone_from(&message);
|
||||
let fail_reason = read(&self.inner.login_error_key).clone();
|
||||
if let Ok(args) = LoginProgressEventArgs::new(status, message, fail_reason) {
|
||||
self.inner.events.login_progress.emit_with(|| args.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn set_login_error(&self, value: &str) {
|
||||
let mut error = write(&self.inner.login_error_key);
|
||||
error.clear();
|
||||
error.push_str(value);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_login_status_code(&self) -> LoginStatus {
|
||||
*read(&self.inner.login_status)
|
||||
}
|
||||
|
||||
pub fn native_set_login_status_code(&self, value: LoginStatus) {
|
||||
*write(&self.inner.login_status) = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_login_error_key(&self) -> String {
|
||||
read(&self.inner.login_error_key).clone()
|
||||
}
|
||||
|
||||
pub fn native_set_login_error_key(&self, value: String) {
|
||||
*write(&self.inner.login_error_key) = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_login_message(&self) -> String {
|
||||
read(&self.inner.login_message).clone()
|
||||
}
|
||||
|
||||
pub fn native_set_login_message(&self, value: String) {
|
||||
*write(&self.inner.login_message) = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_login_seed_capability(&self) -> Option<Uri> {
|
||||
read(&self.inner.login_seed_capability).clone()
|
||||
}
|
||||
|
||||
pub fn native_set_login_seed_capability(&self, value: Option<Uri>) {
|
||||
*write(&self.inner.login_seed_capability) = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_account_level_benefits(&self) -> Option<AccountLevelBenefits> {
|
||||
self.inner.login_response.account_level_benefits()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_agent_appearance_service_url(&self) -> Option<String> {
|
||||
let value = self.inner.login_response.agent_appearance_service_url();
|
||||
(!value.is_empty()).then_some(value)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn native_max_agent_groups(&self) -> i32 {
|
||||
let fallback = self.inner.login_response.max_agent_groups();
|
||||
self.inner
|
||||
.login_response
|
||||
.account_level_benefits()
|
||||
.map_or(fallback, |benefits| {
|
||||
let limit = benefits.group_membership_limit();
|
||||
if limit > 0 { limit } else { fallback }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn native_default_login_params(
|
||||
&self,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
password: String,
|
||||
channel: String,
|
||||
version: String,
|
||||
) -> Result<LoginParams, Error> {
|
||||
LoginParams::new_with_grid_client_string_string_string_string_string(
|
||||
self.inner.client.clone(),
|
||||
first_name,
|
||||
last_name,
|
||||
password,
|
||||
channel,
|
||||
version,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn native_register_login_response_callback(
|
||||
&self,
|
||||
callback: NetworkManagerLoginResponseCallback,
|
||||
options: Option<Vec<String>>,
|
||||
) -> Result<(), Error> {
|
||||
let mut state = mutex(&self.inner.login_runtime);
|
||||
if state
|
||||
.response_callbacks
|
||||
.iter()
|
||||
.any(|(candidate, _)| candidate.ptr_eq(&callback))
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
state.response_callbacks.push((callback, options));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn native_unregister_login_response_callback(
|
||||
&self,
|
||||
callback: &NetworkManagerLoginResponseCallback,
|
||||
) -> Result<(), Error> {
|
||||
let mut state = mutex(&self.inner.login_runtime);
|
||||
state
|
||||
.response_callbacks
|
||||
.retain(|(candidate, _)| !candidate.ptr_eq(callback));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_login(
|
||||
&self,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<(u64, CancellationTokenSource), Error> {
|
||||
let mut runtime = mutex(&self.inner.login_runtime);
|
||||
if runtime.active.is_some() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
let caller = cancellation_token.unwrap_or_default();
|
||||
let source =
|
||||
CancellationTokenSource::new_linked(&[caller, self.inner.client.cancellation_token()]);
|
||||
let generation = runtime.next_generation;
|
||||
runtime.next_generation = runtime.next_generation.wrapping_add(1).max(1);
|
||||
runtime.active = Some((generation, source.clone()));
|
||||
Ok((generation, source))
|
||||
}
|
||||
|
||||
fn release_login(&self, generation: u64) {
|
||||
let mut runtime = mutex(&self.inner.login_runtime);
|
||||
if runtime
|
||||
.active
|
||||
.as_ref()
|
||||
.is_some_and(|(active, _)| *active == generation)
|
||||
{
|
||||
runtime.active = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn native_abort_login(&self) -> Result<(), Error> {
|
||||
let active = mutex(&self.inner.login_runtime).active.take();
|
||||
if let Some((_, source)) = active {
|
||||
source.cancel();
|
||||
}
|
||||
self.update_login_status(LoginStatus::Failed, "Abort Requested".to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn native_begin_login(&self, login_params: LoginParams) -> Result<(), Error> {
|
||||
let (generation, source) = self.reserve_login(None)?;
|
||||
let manager = Self {
|
||||
login_response_data: Some(self.inner.login_response.clone()),
|
||||
simulators: self.simulators.clone(),
|
||||
inner: Arc::clone(&self.inner),
|
||||
};
|
||||
thread::Builder::new()
|
||||
.name("libremetaverse-login".to_owned())
|
||||
.spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build();
|
||||
let result = match runtime {
|
||||
Ok(runtime) => {
|
||||
runtime.block_on(manager.perform_login(login_params, source.token()))
|
||||
}
|
||||
Err(_) => Err(Error::InvalidOperation),
|
||||
};
|
||||
if let Err(error) = result {
|
||||
manager.login_transport_failure(&error);
|
||||
}
|
||||
manager.release_login(generation);
|
||||
})
|
||||
.map_err(|_| {
|
||||
self.release_login(generation);
|
||||
Error::InvalidOperation
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn native_login(
|
||||
&self,
|
||||
login_params: LoginParams,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<bool, Error> {
|
||||
Ok(self
|
||||
.native_login_response_async(login_params, cancellation_token)
|
||||
.await?
|
||||
.is_some_and(|response| response.success()))
|
||||
}
|
||||
|
||||
pub async fn native_login_response_async(
|
||||
&self,
|
||||
login_params: LoginParams,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<Option<LoginResponseData>, Error> {
|
||||
let timeout = positive_millisecond_duration(login_params.timeout);
|
||||
let (generation, source) = self.reserve_login(cancellation_token)?;
|
||||
let token = source.token();
|
||||
let result = tokio::select! {
|
||||
result = tokio::time::timeout(timeout, self.perform_login(login_params, token.clone())) => {
|
||||
if let Ok(result) = result {
|
||||
result
|
||||
} else {
|
||||
source.cancel();
|
||||
self.set_login_error("timeout");
|
||||
self.update_login_status(LoginStatus::Failed, "Login timed out".to_owned());
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
() = token.cancelled() => {
|
||||
self.set_login_error("canceled");
|
||||
self.update_login_status(LoginStatus::Failed, "Canceled".to_owned());
|
||||
Ok(None)
|
||||
}
|
||||
};
|
||||
self.release_login(generation);
|
||||
match result {
|
||||
Ok(value) => Ok(value),
|
||||
Err(error) => {
|
||||
self.login_transport_failure(&error);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn native_login_credential(
|
||||
&self,
|
||||
credential: LoginCredential,
|
||||
channel: String,
|
||||
start: Option<String>,
|
||||
version: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<bool, Error> {
|
||||
let mut login_params = LoginParams::new_with_grid_client_login_credential_string_string(
|
||||
self.inner.client.clone(),
|
||||
credential,
|
||||
channel,
|
||||
version,
|
||||
)?;
|
||||
if let Some(start) = start {
|
||||
login_params.start = start;
|
||||
}
|
||||
self.native_login(login_params, cancellation_token).await
|
||||
}
|
||||
|
||||
pub async fn native_login_credential_response(
|
||||
&self,
|
||||
credential: LoginCredential,
|
||||
channel: String,
|
||||
start: Option<String>,
|
||||
version: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<Option<LoginResponseData>, Error> {
|
||||
let mut login_params = LoginParams::new_with_grid_client_login_credential_string_string(
|
||||
self.inner.client.clone(),
|
||||
credential,
|
||||
channel,
|
||||
version,
|
||||
)?;
|
||||
if let Some(start) = start {
|
||||
login_params.start = start;
|
||||
}
|
||||
self.native_login_response_async(login_params, cancellation_token)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn native_login_strings(
|
||||
&self,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
password: String,
|
||||
channel: String,
|
||||
start: String,
|
||||
version: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<bool, Error> {
|
||||
let mut params =
|
||||
self.native_default_login_params(first_name, last_name, password, channel, version)?;
|
||||
params.start = start;
|
||||
self.native_login(params, cancellation_token).await
|
||||
}
|
||||
|
||||
pub async fn native_login_strings_response(
|
||||
&self,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
password: String,
|
||||
channel: String,
|
||||
start: String,
|
||||
version: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<Option<LoginResponseData>, Error> {
|
||||
let mut params =
|
||||
self.native_default_login_params(first_name, last_name, password, channel, version)?;
|
||||
params.start = start;
|
||||
self.native_login_response_async(params, cancellation_token)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn perform_login(
|
||||
&self,
|
||||
mut params: LoginParams,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Option<LoginResponseData>, Error> {
|
||||
if params.password.len() != 35 && !params.password.starts_with("$1$") {
|
||||
params.password = libremetaverse_types::Utils::md5_with_string(params.password)?;
|
||||
}
|
||||
if params.channel.trim().is_empty() {
|
||||
params.channel = crate::Settings::user_agent();
|
||||
}
|
||||
if params.version.trim().is_empty() {
|
||||
params.version = String::from("?.?.?");
|
||||
}
|
||||
let wire_secrets = LoginWireSecrets {
|
||||
password: std::mem::take(&mut params.password),
|
||||
token: std::mem::take(&mut params.token),
|
||||
mfa_hash: std::mem::take(&mut params.mfa_hash),
|
||||
};
|
||||
params.clear_secrets();
|
||||
let requested_start = if params.login_location.trim().is_empty() {
|
||||
params.start.clone()
|
||||
} else {
|
||||
params.login_location.clone()
|
||||
};
|
||||
params.start = crate::login::normalize_start(&requested_start)?;
|
||||
|
||||
let callback_options: Vec<String> = mutex(&self.inner.login_runtime)
|
||||
.response_callbacks
|
||||
.iter()
|
||||
.filter_map(|(_, options)| options.as_ref())
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect();
|
||||
for option in callback_options {
|
||||
if !params.options.contains(&option) {
|
||||
params.options.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
let mut redirects = 0_usize;
|
||||
loop {
|
||||
cancellation_token.throw_if_cancellation_requested()?;
|
||||
let login_uri = validate_login_uri(¶ms.uri)?;
|
||||
self.update_login_status(
|
||||
LoginStatus::ConnectingToLogin,
|
||||
format!(
|
||||
"Logging in as {} {}...",
|
||||
params.first_name, params.last_name
|
||||
),
|
||||
);
|
||||
let request = login_request(¶ms, &wire_secrets);
|
||||
let response = self
|
||||
.inner
|
||||
.client
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_osd_format_osd_cancellation_token_i_progress(
|
||||
login_uri,
|
||||
OSDFormat::Xml,
|
||||
request,
|
||||
cancellation_token.clone(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let (http_response, response_data) = match response {
|
||||
Ok(response) => response,
|
||||
Err(Error::Cancelled) => {
|
||||
self.set_login_error("canceled");
|
||||
self.update_login_status(LoginStatus::Failed, "Login canceled".to_owned());
|
||||
return Ok(None);
|
||||
}
|
||||
Err(error) => {
|
||||
self.login_transport_failure(&error);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
if !http_response.is_success_status_code() {
|
||||
self.set_login_error("bad response");
|
||||
self.update_login_status(
|
||||
LoginStatus::Failed,
|
||||
format!("Login service returned HTTP {}", http_response.status_code),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
self.update_login_status(
|
||||
LoginStatus::ReadingResponse,
|
||||
"Reading login response...".to_owned(),
|
||||
);
|
||||
let parsed = match OSDParser::deserialize_with_bytes(response_data) {
|
||||
Ok(OSD::Map(map)) => OSDMap::new_with_dictionary(map),
|
||||
_ => Err(Error::Argument),
|
||||
};
|
||||
let Ok(parsed) = parsed else {
|
||||
self.set_login_error("bad response");
|
||||
self.update_login_status(
|
||||
LoginStatus::Failed,
|
||||
"Empty or corrupt login response".to_owned(),
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
let has_login = parsed.get("login").is_some();
|
||||
self.inner.login_response.parse_with_osd_map(parsed)?;
|
||||
let response = self.inner.login_response.clone();
|
||||
if !has_login {
|
||||
self.update_login_status(
|
||||
LoginStatus::Failed,
|
||||
"Login parameter missing in the response".to_owned(),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
match response.login() {
|
||||
crate::LoginState::Indeterminate => {
|
||||
redirects += 1;
|
||||
if redirects > 10 {
|
||||
self.set_login_error("redirect loop");
|
||||
self.update_login_status(
|
||||
LoginStatus::Failed,
|
||||
"Too many login redirects".to_owned(),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
self.update_login_status(LoginStatus::Redirecting, response.message());
|
||||
params.uri = response.next_url();
|
||||
validate_login_uri(¶ms.uri)?;
|
||||
let delay = Duration::from_secs(
|
||||
u64::try_from(response.next_duration().clamp(0, 300)).unwrap_or(0),
|
||||
);
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(delay) => {}
|
||||
() = cancellation_token.cancelled() => {
|
||||
self.set_login_error("canceled");
|
||||
self.update_login_status(LoginStatus::Failed, "Login canceled".to_owned());
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::LoginState::True => {
|
||||
self.invoke_login_response(true, false, &response);
|
||||
if response.seed_capability().is_empty() {
|
||||
self.set_login_error("bad response");
|
||||
self.update_login_status(
|
||||
LoginStatus::Failed,
|
||||
"Login response did not include a seed capability".to_owned(),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
if self.initialize_login_response(
|
||||
response.clone(),
|
||||
response.message(),
|
||||
"Unable to establish a UDP connection to the simulator",
|
||||
"Login server did not return a simulator address",
|
||||
)? {
|
||||
return Ok(Some(response));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
crate::LoginState::False => {
|
||||
let reason = response.reason();
|
||||
self.set_login_error(if reason.is_empty() {
|
||||
"unknown"
|
||||
} else {
|
||||
&reason
|
||||
});
|
||||
self.update_login_status(LoginStatus::Failed, response.message());
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn invoke_login_response(&self, success: bool, redirect: bool, response: &LoginResponseData) {
|
||||
let callbacks: Vec<_> = mutex(&self.inner.login_runtime)
|
||||
.response_callbacks
|
||||
.iter()
|
||||
.map(|(callback, _)| callback.clone())
|
||||
.collect();
|
||||
for callback in callbacks {
|
||||
let _ = callback.invoke(
|
||||
success,
|
||||
redirect,
|
||||
response.message(),
|
||||
response.reason(),
|
||||
Some(response.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn login_transport_failure(&self, error: &Error) {
|
||||
let (key, message) = match error {
|
||||
Error::Cancelled => ("canceled", "Login canceled"),
|
||||
Error::HttpRequest => ("no connection", "Unable to contact login service"),
|
||||
Error::Argument => ("bad response", "Invalid login request or response"),
|
||||
_ => ("no connection", "Login failed"),
|
||||
};
|
||||
self.set_login_error(key);
|
||||
self.update_login_status(LoginStatus::Failed, message.to_owned());
|
||||
}
|
||||
|
||||
pub fn native_login_response(&self, response: LoginResponseData) -> Result<bool, Error> {
|
||||
self.initialize_login_response(
|
||||
response,
|
||||
"Login success".to_owned(),
|
||||
"Unable to connect to simulator",
|
||||
"Unable to connect to simulator",
|
||||
)
|
||||
}
|
||||
|
||||
fn initialize_login_response(
|
||||
&self,
|
||||
response: LoginResponseData,
|
||||
success_message: String,
|
||||
connect_failure_message: &str,
|
||||
address_failure_message: &str,
|
||||
) -> Result<bool, Error> {
|
||||
self.inner.login_response.copy_from(&response);
|
||||
self.inner
|
||||
.circuit_code
|
||||
.store(response.circuit_code().cast_unsigned(), Ordering::Release);
|
||||
*write(&self.inner.agent_id) = response.agent_id();
|
||||
*write(&self.inner.session_id) = response.session_id();
|
||||
let seed = if response.seed_capability().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(validate_login_uri(&response.seed_capability())?)
|
||||
};
|
||||
write(&self.inner.login_seed_capability).clone_from(&seed);
|
||||
self.update_login_status(
|
||||
LoginStatus::ConnectingToSim,
|
||||
"Connecting to simulator...".to_owned(),
|
||||
);
|
||||
let Some(ip) = response.sim_ip() else {
|
||||
self.set_login_error("bad response");
|
||||
self.update_login_status(LoginStatus::Failed, address_failure_message.to_owned());
|
||||
return Ok(false);
|
||||
};
|
||||
if response.sim_port() == 0 {
|
||||
self.set_login_error("bad response");
|
||||
self.update_login_status(LoginStatus::Failed, address_failure_message.to_owned());
|
||||
return Ok(false);
|
||||
}
|
||||
let handle = (u64::from(response.region_x()) << 32) | u64::from(response.region_y());
|
||||
let endpoint = std::net::SocketAddr::new(ip, response.sim_port());
|
||||
let Ok(connected) = self.native_connect(
|
||||
endpoint,
|
||||
handle,
|
||||
true,
|
||||
seed,
|
||||
response.region_size_x(),
|
||||
response.region_size_y(),
|
||||
) else {
|
||||
self.cleanup_failed_login_connection(endpoint);
|
||||
self.set_login_error("no connection");
|
||||
self.update_login_status(LoginStatus::Failed, connect_failure_message.to_owned());
|
||||
return Ok(false);
|
||||
};
|
||||
if let Some(simulator) = connected {
|
||||
let economy_request =
|
||||
Packet::build_packet_with_packet_type(PacketType::EconomyDataRequest);
|
||||
if economy_request
|
||||
.and_then(|packet| simulator.native_send_packet(packet))
|
||||
.is_err()
|
||||
{
|
||||
self.cleanup_failed_login_connection(endpoint);
|
||||
self.set_login_error("no connection");
|
||||
self.update_login_status(LoginStatus::Failed, connect_failure_message.to_owned());
|
||||
return Ok(false);
|
||||
}
|
||||
self.update_login_status(LoginStatus::Success, success_message);
|
||||
Ok(true)
|
||||
} else {
|
||||
self.cleanup_failed_login_connection(endpoint);
|
||||
self.update_login_status(LoginStatus::Failed, connect_failure_message.to_owned());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_failed_login_connection(&self, endpoint: std::net::SocketAddr) {
|
||||
if let Some(simulator) = self.native_find_endpoint(endpoint) {
|
||||
let _ = simulator.native_disconnect(false);
|
||||
self.simulators.remove(&simulator);
|
||||
let mut current = write(&self.inner.current_sim);
|
||||
if current.as_ref() == Some(&simulator) {
|
||||
*current = None;
|
||||
}
|
||||
}
|
||||
if self.simulators.is_empty() {
|
||||
self.inner.connected.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn native_subscribe_disconnected(
|
||||
&self,
|
||||
handler: EventHandler<DisconnectedEventArgs>,
|
||||
@@ -2299,7 +3037,7 @@ impl NetworkManager {
|
||||
size_y: u32,
|
||||
) -> Result<Option<Simulator>, Error> {
|
||||
let manager = Self {
|
||||
login_response_data: None,
|
||||
login_response_data: Some(self.inner.login_response.clone()),
|
||||
simulators: self.simulators.clone(),
|
||||
inner: Arc::clone(&self.inner),
|
||||
};
|
||||
|
||||
@@ -7,10 +7,16 @@ use libremetaverse::packets::{
|
||||
RegionHandshakePacket, RegionHandshakeReplyPacket, StartPingCheckPacket, UseCircuitCodePacket,
|
||||
};
|
||||
use libremetaverse::{
|
||||
CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, NetworkManager,
|
||||
NetworkManagerDisconnectType, PacketEventDictionary, Simulator,
|
||||
CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient, LoginState,
|
||||
LoginStatus, NetworkManager, NetworkManagerDisconnectType, NetworkManagerLoginResponseCallback,
|
||||
PacketEventDictionary, Simulator,
|
||||
};
|
||||
use libremetaverse_types::compat::{EventHandler, Uri};
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationTokenSource, EventHandler, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
|
||||
};
|
||||
use libremetaverse_types::{UUID, Utils};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
@@ -204,6 +210,35 @@ fn synchronous_specific_dispatch_suppresses_default_async_chain_like_reference()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abort_and_callback_unregistration_preserve_reference_noop_behavior() {
|
||||
let client = GridClient::new().unwrap();
|
||||
let manager = client.network();
|
||||
let progress = Arc::new(Mutex::new(Vec::new()));
|
||||
let observed = Arc::clone(&progress);
|
||||
let _guard = manager.subscribe_login_progress(Arc::new(move |args| {
|
||||
observed.lock().unwrap().push(args);
|
||||
}));
|
||||
let callback = NetworkManagerLoginResponseCallback::from_handler(|_, _, _, _, _| {});
|
||||
manager
|
||||
.register_login_response_callback_with_login_response_callback(callback.clone())
|
||||
.unwrap();
|
||||
manager
|
||||
.unregister_login_response_callback(callback.clone())
|
||||
.unwrap();
|
||||
manager
|
||||
.unregister_login_response_callback(callback)
|
||||
.unwrap();
|
||||
|
||||
manager.abort_login().unwrap();
|
||||
assert_eq!(manager.login_status_code(), LoginStatus::Failed);
|
||||
assert_eq!(manager.login_message(), "Abort Requested");
|
||||
let progress = progress.lock().unwrap();
|
||||
assert_eq!(progress.len(), 1);
|
||||
assert_eq!(progress[0].status(), LoginStatus::Failed);
|
||||
assert_eq!(progress[0].message(), "Abort Requested");
|
||||
}
|
||||
|
||||
struct TestCapsMessage;
|
||||
impl IMessage for TestCapsMessage {}
|
||||
|
||||
@@ -319,6 +354,7 @@ fn sim_connecting_can_cancel_without_leaking_collection_state() {
|
||||
enum ServerCommand {
|
||||
Ping,
|
||||
Disable,
|
||||
ExpectEconomy,
|
||||
Stop,
|
||||
}
|
||||
|
||||
@@ -327,6 +363,7 @@ enum ServerReport {
|
||||
Ack(u32),
|
||||
Circuit(u32),
|
||||
HandshakeReply(u32),
|
||||
Economy,
|
||||
Ping(u8),
|
||||
}
|
||||
|
||||
@@ -472,6 +509,13 @@ fn spawn_fake_server() -> FakeServer {
|
||||
bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED);
|
||||
socket.send_to(&bytes, client_endpoint).unwrap();
|
||||
}
|
||||
ServerCommand::ExpectEconomy => loop {
|
||||
let (length, _) = socket.recv_from(&mut buffer).expect("EconomyDataRequest");
|
||||
if packet_type(&buffer[..length]) == Some(PacketType::EconomyDataRequest) {
|
||||
report_sender.send(ServerReport::Economy).unwrap();
|
||||
break;
|
||||
}
|
||||
},
|
||||
ServerCommand::Stop => break,
|
||||
}
|
||||
}
|
||||
@@ -620,7 +664,6 @@ fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() {
|
||||
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||
ServerReport::HandshakeReply(0x1 | 0x2 | 0x4)
|
||||
);
|
||||
|
||||
assert_reuse_preserves_connected_override(&mut manager, &server, &sim);
|
||||
|
||||
assert_ping_exchange(&server, &packet_receiver);
|
||||
@@ -897,3 +940,396 @@ fn handshake_timeout_preserves_reference_connect_result_and_shutdown_tears_down_
|
||||
assert_eq!(sim_disconnects.load(Ordering::Relaxed), 1);
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
fn login_http_response(body: OSD) -> HttpResponse {
|
||||
let body = OSDParser::serialize_llsd_xml_bytes(body).expect("serialize LLSD");
|
||||
HttpResponse {
|
||||
status_code: 200,
|
||||
headers: BTreeMap::from([("Content-Length".into(), body.len().to_string())]),
|
||||
content_type: Some("application/llsd+xml".into()),
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
fn successful_login_response(endpoint: SocketAddr, seed: &str) -> OSD {
|
||||
OSD::Map(HashMap::from([
|
||||
("login".into(), OSD::Boolean(true)),
|
||||
("message".into(), OSD::String("Welcome".into())),
|
||||
(
|
||||
"agent_id".into(),
|
||||
OSD::UUID(
|
||||
UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"session_id".into(),
|
||||
OSD::UUID(
|
||||
UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"secure_session_id".into(),
|
||||
OSD::UUID(
|
||||
UUID::new_with_string("cccccccc-cccc-cccc-cccc-cccccccccccc".into()).unwrap(),
|
||||
),
|
||||
),
|
||||
("circuit_code".into(), OSD::Integer(0x1234)),
|
||||
("region_x".into(), OSD::Integer(1000)),
|
||||
("region_y".into(), OSD::Integer(2000)),
|
||||
("region_size_x".into(), OSD::Integer(256)),
|
||||
("region_size_y".into(), OSD::Integer(256)),
|
||||
("sim_ip".into(), OSD::String(endpoint.ip().to_string())),
|
||||
("sim_port".into(), OSD::Integer(i32::from(endpoint.port()))),
|
||||
("seed_capability".into(), OSD::String(seed.into())),
|
||||
(
|
||||
"account_level_benefits".into(),
|
||||
OSD::Map(HashMap::from([(
|
||||
"group_membership_limit".into(),
|
||||
OSD::Integer(70),
|
||||
)])),
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn login_hashes_credentials_parses_response_and_hands_off_session_to_udp_and_caps() {
|
||||
let server = spawn_fake_server();
|
||||
let requests = Arc::new(Mutex::new(Vec::<HttpRequest>::new()));
|
||||
let recorded = Arc::clone(&requests);
|
||||
let endpoint = server.endpoint;
|
||||
let handler = HttpMessageHandler::new(move |request, _| {
|
||||
let recorded = Arc::clone(&recorded);
|
||||
async move {
|
||||
let response = if request.uri.0.contains("/login") {
|
||||
login_http_response(successful_login_response(
|
||||
endpoint,
|
||||
"http://caps.test/seed?token=secret",
|
||||
))
|
||||
} else {
|
||||
login_http_response(OSD::Map(HashMap::new()))
|
||||
};
|
||||
recorded.lock().unwrap().push(request);
|
||||
response
|
||||
}
|
||||
});
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.settings().timing().login_timeout = 2_000;
|
||||
client.settings().connection_mut().mfa_enabled = true;
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let manager = client.network();
|
||||
let callback_calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed_calls = Arc::clone(&callback_calls);
|
||||
let callback = NetworkManagerLoginResponseCallback::from_handler(
|
||||
move |success, redirect, message, reason, response| {
|
||||
assert!(success);
|
||||
assert!(!redirect);
|
||||
assert_eq!(message, "Welcome");
|
||||
assert!(reason.is_empty());
|
||||
assert!(response.is_some_and(|value| value.success()));
|
||||
observed_calls.fetch_add(1, Ordering::Relaxed);
|
||||
},
|
||||
);
|
||||
manager
|
||||
.register_login_response_callback_with_login_response_callback_string_array(
|
||||
callback,
|
||||
Some(vec!["custom-login-option".into()]),
|
||||
)
|
||||
.unwrap();
|
||||
let progress = Arc::new(Mutex::new(Vec::new()));
|
||||
let observed = Arc::clone(&progress);
|
||||
let _progress_guard = manager.subscribe_login_progress(Arc::new(move |args| {
|
||||
observed.lock().unwrap().push(args.status());
|
||||
}));
|
||||
let mut params = manager
|
||||
.default_login_params(
|
||||
"Ada".into(),
|
||||
"Lovelace".into(),
|
||||
"plain-secret".into(),
|
||||
"MetaCrate".into(),
|
||||
"1.0".into(),
|
||||
)
|
||||
.unwrap();
|
||||
params.uri = "http://login.test/login".into();
|
||||
params.token = "123456".into();
|
||||
params.mfa_hash = "saved-mfa-hash".into();
|
||||
|
||||
let response = manager
|
||||
.login_with_response_with_login_params_cancellation_token(params, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("successful response");
|
||||
assert_eq!(response.login(), LoginState::True);
|
||||
assert_eq!(manager.login_status_code(), LoginStatus::Success);
|
||||
assert_eq!(manager.login_message(), "Welcome");
|
||||
assert_eq!(manager.max_agent_groups(), 70);
|
||||
assert_eq!(callback_calls.load(Ordering::Relaxed), 1);
|
||||
assert!(manager.connected());
|
||||
assert!(manager.current_sim().is_some());
|
||||
assert_eq!(
|
||||
manager.login_seed_capability(),
|
||||
Some(Uri("http://caps.test/seed?token=secret".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||
ServerReport::Circuit(0x1234)
|
||||
);
|
||||
assert_eq!(
|
||||
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||
ServerReport::HandshakeReply(0x1 | 0x2 | 0x4)
|
||||
);
|
||||
server.commands.send(ServerCommand::ExpectEconomy).unwrap();
|
||||
assert_eq!(
|
||||
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||
ServerReport::Economy
|
||||
);
|
||||
|
||||
let login_request = requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|request| request.uri.0.contains("/login"))
|
||||
.cloned()
|
||||
.expect("login request");
|
||||
let request = OSDParser::deserialize_with_bytes(login_request.body).unwrap();
|
||||
let OSD::Map(request) = request else {
|
||||
panic!("login request must be an LLSD map");
|
||||
};
|
||||
assert_eq!(
|
||||
request["passwd"].as_string().unwrap(),
|
||||
Utils::md5_with_string("plain-secret".into()).unwrap()
|
||||
);
|
||||
assert_eq!(request["token"].as_string().unwrap(), "123456");
|
||||
assert_eq!(request["mfa_hash"].as_string().unwrap(), "saved-mfa-hash");
|
||||
assert_eq!(request["start"].as_string().unwrap(), "last");
|
||||
assert_eq!(request["channel"].as_string().unwrap(), "MetaCrate");
|
||||
assert_eq!(request["version"].as_string().unwrap(), "1.0");
|
||||
assert_eq!(
|
||||
request["platform"].as_string().unwrap(),
|
||||
NetworkManager::get_platform().unwrap()
|
||||
);
|
||||
assert!(!request["platform_version"].as_string().unwrap().is_empty());
|
||||
assert_eq!(
|
||||
request["mac"].as_string().unwrap(),
|
||||
request["id0"].as_string().unwrap()
|
||||
);
|
||||
let OSD::Array(options) = &request["options"] else {
|
||||
panic!("login options must be an LLSD array");
|
||||
};
|
||||
assert!(
|
||||
options
|
||||
.iter()
|
||||
.any(|option| option.as_string().as_deref() == Ok("custom-login-option"))
|
||||
);
|
||||
assert_eq!(
|
||||
*progress.lock().unwrap(),
|
||||
[
|
||||
LoginStatus::ConnectingToLogin,
|
||||
LoginStatus::ReadingResponse,
|
||||
LoginStatus::ConnectingToSim,
|
||||
LoginStatus::Success,
|
||||
]
|
||||
);
|
||||
manager
|
||||
.shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn redirect_reuses_hashed_credentials_and_failure_is_typed_without_partial_session() {
|
||||
let requests = Arc::new(Mutex::new(Vec::<HttpRequest>::new()));
|
||||
let recorded = Arc::clone(&requests);
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let handler_calls = Arc::clone(&calls);
|
||||
let handler = HttpMessageHandler::new(move |request, _| {
|
||||
let recorded = Arc::clone(&recorded);
|
||||
let index = handler_calls.fetch_add(1, Ordering::Relaxed);
|
||||
async move {
|
||||
recorded.lock().unwrap().push(request);
|
||||
if index == 0 {
|
||||
login_http_response(OSD::Map(HashMap::from([
|
||||
("login".into(), OSD::String("indeterminate".into())),
|
||||
(
|
||||
"next_url".into(),
|
||||
OSD::String("http://login.test/redirected".into()),
|
||||
),
|
||||
("next_duration".into(), OSD::Integer(0)),
|
||||
("message".into(), OSD::String("Redirecting".into())),
|
||||
])))
|
||||
} else {
|
||||
login_http_response(OSD::Map(HashMap::from([
|
||||
("login".into(), OSD::Boolean(false)),
|
||||
("reason".into(), OSD::String("key".into())),
|
||||
("message".into(), OSD::String("Denied".into())),
|
||||
])))
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.settings().timing().login_timeout = 1_000;
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let manager = client.network();
|
||||
let callback_calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed_calls = Arc::clone(&callback_calls);
|
||||
manager
|
||||
.register_login_response_callback_with_login_response_callback(
|
||||
NetworkManagerLoginResponseCallback::from_handler(move |_, _, _, _, _| {
|
||||
observed_calls.fetch_add(1, Ordering::Relaxed);
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut params = manager
|
||||
.default_login_params(
|
||||
"Ada".into(),
|
||||
"Lovelace".into(),
|
||||
"plain-secret".into(),
|
||||
"MetaCrate".into(),
|
||||
"1.0".into(),
|
||||
)
|
||||
.unwrap();
|
||||
params.uri = "http://login.test/login".into();
|
||||
|
||||
assert!(
|
||||
!manager
|
||||
.login_with_login_params_cancellation_token(params, None)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(manager.login_status_code(), LoginStatus::Failed);
|
||||
assert_eq!(manager.login_error_key(), "key");
|
||||
assert_eq!(manager.login_message(), "Denied");
|
||||
assert!(!manager.connected());
|
||||
assert!(manager.current_sim().is_none());
|
||||
assert_eq!(
|
||||
callback_calls.load(Ordering::Relaxed),
|
||||
0,
|
||||
"the C# callback only runs for a successful login"
|
||||
);
|
||||
let passwords: Vec<_> = requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|request| {
|
||||
let OSD::Map(map) = OSDParser::deserialize_with_bytes(request.body.clone()).unwrap()
|
||||
else {
|
||||
panic!("request must be map");
|
||||
};
|
||||
map["passwd"].as_string().unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(passwords.len(), 2);
|
||||
assert_eq!(passwords[0], passwords[1]);
|
||||
assert_ne!(passwords[0], "plain-secret");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn successful_authentication_with_invalid_simulator_clears_partial_session() {
|
||||
let handler = HttpMessageHandler::new(|_, _| async move {
|
||||
let mut response = successful_login_response(
|
||||
"127.0.0.1:1".parse().unwrap(),
|
||||
"http://caps.test/seed?token=must-not-survive",
|
||||
);
|
||||
let OSD::Map(ref mut fields) = response else {
|
||||
unreachable!();
|
||||
};
|
||||
fields.insert("sim_port".into(), OSD::Integer(0));
|
||||
login_http_response(response)
|
||||
});
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.settings().timing().login_timeout = 1_000;
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let manager = client.network();
|
||||
let mut params = manager
|
||||
.default_login_params(
|
||||
"Ada".into(),
|
||||
"Lovelace".into(),
|
||||
"plain-secret".into(),
|
||||
"MetaCrate".into(),
|
||||
"1.0".into(),
|
||||
)
|
||||
.unwrap();
|
||||
params.uri = "http://login.test/login".into();
|
||||
|
||||
assert!(
|
||||
!manager
|
||||
.login_with_login_params_cancellation_token(params, None)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(manager.login_status_code(), LoginStatus::Failed);
|
||||
assert!(manager.current_sim().is_none());
|
||||
assert!(manager.simulators.is_empty());
|
||||
assert!(!manager.connected());
|
||||
assert!(manager.login_seed_capability().is_none());
|
||||
let response = manager.login_response_data.as_ref().unwrap();
|
||||
assert_eq!(response.session_id(), UUID::zero());
|
||||
assert_eq!(response.secure_session_id(), UUID::zero());
|
||||
assert!(response.seed_capability().is_empty());
|
||||
assert!(response.account_level_benefits().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||
async fn login_timeout_is_typed_and_leaves_no_partial_session() {
|
||||
let handler =
|
||||
HttpMessageHandler::new(|_, _| async move { std::future::pending::<HttpResponse>().await });
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.settings().timing().login_timeout = 25;
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let manager = client.network();
|
||||
let mut params = manager
|
||||
.default_login_params(
|
||||
"Ada".into(),
|
||||
"Lovelace".into(),
|
||||
"plain-secret".into(),
|
||||
"MetaCrate".into(),
|
||||
"1.0".into(),
|
||||
)
|
||||
.unwrap();
|
||||
params.uri = "http://login.test/login".into();
|
||||
|
||||
assert!(
|
||||
!manager
|
||||
.login_with_login_params_cancellation_token(params, None)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(manager.login_status_code(), LoginStatus::Failed);
|
||||
assert_eq!(manager.login_error_key(), "timeout");
|
||||
assert_eq!(manager.login_message(), "Login timed out");
|
||||
assert!(manager.current_sim().is_none());
|
||||
assert!(manager.login_seed_capability().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn canceled_login_finishes_failed_without_retaining_simulator_work() {
|
||||
let handler = HttpMessageHandler::new(|_, cancellation| async move {
|
||||
cancellation.cancelled().await;
|
||||
login_http_response(OSD::Map(HashMap::new()))
|
||||
});
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.settings().timing().login_timeout = 1_000;
|
||||
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
|
||||
let manager = client.network();
|
||||
let mut params = manager
|
||||
.default_login_params(
|
||||
"Ada".into(),
|
||||
"Lovelace".into(),
|
||||
"plain-secret".into(),
|
||||
"MetaCrate".into(),
|
||||
"1.0".into(),
|
||||
)
|
||||
.unwrap();
|
||||
params.uri = "http://login.test/login".into();
|
||||
let cancellation = CancellationTokenSource::new();
|
||||
cancellation.cancel();
|
||||
|
||||
assert!(
|
||||
!manager
|
||||
.login_with_login_params_cancellation_token(params, Some(cancellation.token()))
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(manager.login_status_code(), LoginStatus::Failed);
|
||||
assert_eq!(manager.login_error_key(), "canceled");
|
||||
assert!(!manager.connected());
|
||||
assert!(manager.simulators.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user