Some checks failed
Native code generation / deterministic (push) Failing after 2m12s
Imaging and meshing gate / native (push) Failing after 5m40s
JPEG 2000 feature / linux (push) Successful in 2m50s
Native Rust workspace compile / compile (push) Failing after 56s
Skia feature / linux (push) Successful in 31m24s
211 lines
7.4 KiB
Rust
211 lines
7.4 KiB
Rust
//! Credential-safe native `TestClient` voice capability commands.
|
|
|
|
use super::{FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock};
|
|
use libremetaverse::types::compat::{CancellationToken, Uri};
|
|
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
|
|
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
|
|
const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
|
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(super) enum Command {
|
|
Parcel,
|
|
Account,
|
|
}
|
|
|
|
pub(super) fn commands() -> [super::CommandEntry; 2] {
|
|
use super::{CommandCategory as Cat, CommandHandler};
|
|
[
|
|
(
|
|
"voiceparcel",
|
|
"Obtain redacted parcel voice information. Usage: voiceparcel",
|
|
Cat::Other,
|
|
CommandHandler::Voice(Command::Parcel),
|
|
),
|
|
(
|
|
"voiceaccount",
|
|
"Provision voice account information with credentials redacted. Usage: voiceaccount",
|
|
Cat::Other,
|
|
CommandHandler::Voice(Command::Account),
|
|
),
|
|
]
|
|
}
|
|
|
|
pub(super) trait Backend {
|
|
fn voice_query(
|
|
&self,
|
|
command: Command,
|
|
cancellation: CancellationToken,
|
|
) -> BackendFuture<'_, Result<VoiceResult, String>>;
|
|
}
|
|
|
|
pub(super) enum VoiceResult {
|
|
Parcel {
|
|
region_name_present: bool,
|
|
parcel_local_id: i32,
|
|
channel_present: bool,
|
|
},
|
|
Account {
|
|
username_present: bool,
|
|
password_present: bool,
|
|
},
|
|
}
|
|
|
|
pub(super) async fn execute<B: Backend + ?Sized>(
|
|
backend: &B,
|
|
command: Command,
|
|
args: &[String],
|
|
cancellation: CancellationToken,
|
|
) -> String {
|
|
if !args.is_empty() {
|
|
return match command {
|
|
Command::Parcel => "Usage: voiceparcel",
|
|
Command::Account => "Usage: voiceaccount",
|
|
}
|
|
.into();
|
|
}
|
|
match backend.voice_query(command, cancellation).await {
|
|
Ok(VoiceResult::Parcel {
|
|
region_name_present,
|
|
parcel_local_id,
|
|
channel_present,
|
|
}) => format!(
|
|
"Parcel voice info: region-name-present={region_name_present}, parcel-local-id={parcel_local_id}, channel=<{}>",
|
|
if channel_present {
|
|
"redacted"
|
|
} else {
|
|
"missing"
|
|
}
|
|
),
|
|
Ok(VoiceResult::Account {
|
|
username_present,
|
|
password_present,
|
|
}) => format!(
|
|
"Voice account provisioned: credentials=<{}>",
|
|
if username_present && password_present {
|
|
"redacted"
|
|
} else {
|
|
"incomplete"
|
|
}
|
|
),
|
|
Err(error) => error,
|
|
}
|
|
}
|
|
|
|
impl Backend for FakeBackend {
|
|
fn voice_query(
|
|
&self,
|
|
command: Command,
|
|
cancellation: CancellationToken,
|
|
) -> BackendFuture<'_, Result<VoiceResult, String>> {
|
|
Box::pin(async move {
|
|
if cancellation.is_cancellation_requested() {
|
|
return Err("Voice capability request cancelled".into());
|
|
}
|
|
Ok(match command {
|
|
Command::Parcel => {
|
|
self.record("CALL voice-parcel-info".into());
|
|
VoiceResult::Parcel {
|
|
region_name_present: true,
|
|
parcel_local_id: 42,
|
|
channel_present: true,
|
|
}
|
|
}
|
|
Command::Account => {
|
|
self.record("CALL voice-provision-account credentials=<redacted>".into());
|
|
VoiceResult::Account {
|
|
username_present: true,
|
|
password_present: true,
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Backend for LiveBackend {
|
|
fn voice_query(
|
|
&self,
|
|
command: Command,
|
|
cancellation: CancellationToken,
|
|
) -> BackendFuture<'_, Result<VoiceResult, String>> {
|
|
Box::pin(async move {
|
|
let capability = match command {
|
|
Command::Parcel => "ParcelVoiceInfoRequest",
|
|
Command::Account => "ProvisionVoiceAccountRequest",
|
|
};
|
|
let simulator = self
|
|
.network
|
|
.current_sim()
|
|
.ok_or_else(|| "No current simulator is available".to_owned())?;
|
|
let uri = simulator
|
|
.native_capability_uri(capability)
|
|
.map_err(|_| "Voice capability lookup failed".to_owned())?
|
|
.ok_or_else(|| "Required voice capability is unavailable".to_owned())?;
|
|
let http = lock(&self.client).http_caps_client();
|
|
let request = http.post_with_uri_osd_format_osd_cancellation_token_i_progress(
|
|
Uri(uri.0),
|
|
OSDFormat::Xml,
|
|
OSD::Map(HashMap::new()),
|
|
cancellation.clone(),
|
|
None,
|
|
);
|
|
let result = tokio::select! {
|
|
() = cancellation.cancelled() => return Err("Voice capability request cancelled".into()),
|
|
() = tokio::time::sleep(REQUEST_TIMEOUT) => return Err("Voice capability request timed out".into()),
|
|
result = request => result,
|
|
}
|
|
.map_err(|_| "Voice capability request failed".to_owned())?;
|
|
let (response, bytes) = result;
|
|
if !response.is_success_status_code() || bytes.len() > MAX_RESPONSE_BYTES {
|
|
return Err("Voice capability returned an error".into());
|
|
}
|
|
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)
|
|
.map_err(|_| "Voice capability returned malformed LLSD".to_owned())?
|
|
else {
|
|
return Err("Voice capability response was not an LLSD map".into());
|
|
};
|
|
Ok(match command {
|
|
Command::Parcel => {
|
|
let region_name_present = present_string(&map, "region_name");
|
|
let parcel_local_id = map
|
|
.get("parcel_local_id")
|
|
.and_then(|value| value.as_integer().ok())
|
|
.ok_or_else(|| "Parcel voice response omitted local ID".to_owned())?;
|
|
let channel_present = match map.get("voice_credentials") {
|
|
Some(OSD::Map(credentials)) => present_string(credentials, "channel_uri"),
|
|
_ => false,
|
|
};
|
|
if !region_name_present || !channel_present {
|
|
return Err("Parcel voice response omitted required fields".into());
|
|
}
|
|
VoiceResult::Parcel {
|
|
region_name_present,
|
|
parcel_local_id,
|
|
channel_present,
|
|
}
|
|
}
|
|
Command::Account => {
|
|
let username_present = present_string(&map, "username");
|
|
let password_present = present_string(&map, "password");
|
|
if !username_present || !password_present {
|
|
return Err("Voice account response omitted credentials".into());
|
|
}
|
|
VoiceResult::Account {
|
|
username_present,
|
|
password_present,
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
fn present_string(map: &HashMap<String, OSD>, key: &str) -> bool {
|
|
map.get(key)
|
|
.and_then(|value| value.as_string().ok())
|
|
.is_some_and(|value| !value.is_empty())
|
|
}
|