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,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