//! Handing a code to a client that is listening on this machine. //! //! The last step of every authorization this daemon completes is a `GET` of //! a redirect the server built. The server cannot make it: `plan/node.md` //! puts the daemon on the agent host and the authorization server somewhere //! else, and the client's callback listens on loopback here. So the daemon //! fetches it. //! //! That is a request whose target came from a process the model started, so //! it is bounded twice: loopback only, and no chains. Both bounds live here //! rather than at the call site, so that a second caller cannot acquire a //! weaker copy of them. Loopback is what [`didbot_http::is_loopback`] says. use url::Url; /// Why a code did not reach its client. #[derive(Debug, thiserror::Error)] pub enum Trouble { /// The redirect pointed somewhere that is not this machine. #[error("{0}")] Elsewhere(String), /// The client did not answer, or the URL was not one. #[error("{0}")] Failed(String), } /// Fetches `redirect`, which is the act of delivering the code in it. /// /// An error names the target without its query, which carries the code. /// /// `http` is expected to be built with redirects turned off; this does not /// build its own client, because the callers already hold one and a second /// would be a second set of timeouts to keep in step. pub async fn deliver(http: &reqwest::Client, redirect: &str) -> Result<(), Trouble> { let target = Url::parse(redirect) .map_err(|err| Trouble::Failed(format!("the redirect is unusable: {err}")))?; if !didbot_http::is_loopback(&target) { let mut shown = target; shown.set_query(None); shown.set_fragment(None); return Err(Trouble::Elsewhere(format!( "the client asked for its code at {shown}, which is not loopback" ))); } http.get(target).send().await.map_err(|err| { Trouble::Failed(format!( "the client did not take its code: {}", err.without_url() )) })?; Ok(()) } #[cfg(test)] mod tests { use super::*; /// Both refusals are logged, so neither may carry the code. #[tokio::test] async fn a_refusal_does_not_repeat_the_code() { let http = didbot_http::client(); let elsewhere = deliver(&http, "https://example.invalid/cb?code=do-not-log") .await .unwrap_err() .to_string(); assert!(!elsewhere.contains("do-not-log"), "{elsewhere}"); // A loopback port nobody is listening on. let port = std::net::TcpListener::bind("127.0.0.1:0") .unwrap() .local_addr() .unwrap() .port(); let unanswered = deliver( &http, &format!("http://127.0.0.1:{port}/cb?code=do-not-log"), ) .await .unwrap_err() .to_string(); assert!(!unanswered.contains("do-not-log"), "{unanswered}"); } }