diff --git a/crates/xodus/src/tokens/backend/keychain.rs b/crates/xodus/src/tokens/backend/keychain.rs index 1903638..19480da 100644 --- a/crates/xodus/src/tokens/backend/keychain.rs +++ b/crates/xodus/src/tokens/backend/keychain.rs @@ -1,20 +1,45 @@ use crate::tokens::store::{TokenBackend, TokenStoreError}; use serde_json::{Value, json}; use std::io::{BufRead, Read, Write}; +use std::net::{Ipv4Addr, SocketAddrV4, TcpStream}; use std::os::linux::net::SocketAddrExt; use std::os::unix::net::{SocketAddr, UnixStream}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; pub struct KeychainBackend; -fn request(value: Value) -> Result { - let name = std::env::var("DROIDDECK_XBOX_CREDENTIAL_SOCKET") - .map_err(|_| std::io::Error::other("Android credential host is unavailable"))?; - let address = SocketAddr::from_abstract_name(name)?; - let mut stream = UnixStream::connect_addr(&address)?; - stream.set_read_timeout(Some(Duration::from_secs(10)))?; - stream.set_write_timeout(Some(Duration::from_secs(10)))?; - serde_json::to_writer(&mut stream, &value)?; +// keyed loopback port the app also listens on is used for the rest of the process. +static LOOPBACK: AtomicBool = AtomicBool::new(false); +static REPORTED: AtomicBool = AtomicBool::new(false); + +/// Letters and dashes only: the app passes `NEXUS_CREDENTIAL_ERROR_V1 [a-z-]{1,48}` lines to its log. +fn reason(error: &std::io::Error) -> &'static str { + match error.raw_os_error() { + Some(111) => "refused", + Some(13) => "denied", + Some(1) => "not-permitted", + Some(2) => "missing", + Some(110) => "timed-out", + Some(38) => "unsupported", + Some(97) => "family-unsupported", + Some(22) => "invalid", + Some(104) => "reset", + Some(32) => "broken-pipe", + _ => match error.kind() { + std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => "timed-out", + std::io::ErrorKind::UnexpectedEof => "closed", + _ => "other", + }, + } +} + +fn exchange(mut stream: S, prefix: Option<&str>, value: &Value) -> Result { + if let Some(key) = prefix { + stream.write_all(key.as_bytes())?; + stream.write_all(b"\n")?; + } + serde_json::to_writer(&mut stream, value)?; stream.write_all(b"\n")?; let mut bytes = Vec::new(); std::io::BufReader::new(stream).take(2_097_153).read_until(b'\n', &mut bytes)?; @@ -22,10 +47,52 @@ fn request(value: Value) -> Result { return Err(std::io::Error::other("Invalid Android credential response").into()); } let response: Value = serde_json::from_slice(&bytes)?; - if response["ok"] != true { return Err(std::io::Error::other("Android credential storage unavailable").into()); } + if response["ok"] != true { + eprintln!("NEXUS_CREDENTIAL_ERROR_V1 host-refused"); + return Err(std::io::Error::other("Android credential storage unavailable").into()); + } Ok(response) } +fn loopback(value: &Value) -> Result { + let port: u16 = std::env::var("DROIDDECK_XBOX_CREDENTIAL_PORT").ok().and_then(|v| v.parse().ok()) + .ok_or_else(|| std::io::Error::other("Android credential port is unavailable"))?; + let key = std::env::var("DROIDDECK_XBOX_CREDENTIAL_KEY") + .map_err(|_| std::io::Error::other("Android credential key is unavailable"))?; + let address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port).into(); + let stream = TcpStream::connect_timeout(&address, Duration::from_secs(5)).inspect_err(|error| { + eprintln!("NEXUS_CREDENTIAL_ERROR_V1 loopback-{}", reason(error)); + })?; + stream.set_read_timeout(Some(Duration::from_secs(10)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + exchange(stream, Some(&key), value) +} + +fn request(value: Value) -> Result { + if LOOPBACK.load(Ordering::Relaxed) { return loopback(&value); } + let name = std::env::var("DROIDDECK_XBOX_CREDENTIAL_SOCKET") + .map_err(|_| std::io::Error::other("Android credential host is unavailable"))?; + let address = SocketAddr::from_abstract_name(name)?; + match UnixStream::connect_addr(&address) { + Ok(stream) => { + stream.set_read_timeout(Some(Duration::from_secs(10)))?; + stream.set_write_timeout(Some(Duration::from_secs(10)))?; + exchange(stream, None, &value) + } + Err(error) => { + if !REPORTED.swap(true, Ordering::Relaxed) { + eprintln!("NEXUS_CREDENTIAL_ERROR_V1 socket-{}", reason(&error)); + } + let result = loopback(&value); + if result.is_ok() { + LOOPBACK.store(true, Ordering::Relaxed); + eprintln!("NEXUS_CREDENTIAL_ERROR_V1 loopback-in-use"); + } + result + } + } +} + impl TokenBackend for KeychainBackend { fn get(&self, key: &str) -> Result>, TokenStoreError> { let response = request(json!({"op":"get", "key":key}))?;