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:
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user