// // A wrapper around https://git.estrogen.zone/dumbswitch.git to provide nice space api responses // Copyright (C) 2025 memdmp // // This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License along with this program. If not, see . // use std::env; use std::io::Error; use std::net::SocketAddr; use std::sync::LazyLock; use std::time::Duration; use http_body_util::{BodyExt, Empty, Full}; use hyper::Uri; use hyper::body::Incoming; use hyper::header::HeaderValue; use hyper::{Request, Response, body::Bytes, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use serde_json::{Map, Value}; use tokio::net::TcpStream; use tokio::{net::TcpListener, sync::RwLock, task, time}; static RAW_JSON: &'static str = include_str!("../api.json"); static PARSED_JSON: LazyLock> = LazyLock::new(|| RwLock::new(serde_json::from_str(RAW_JSON).expect("JSON parse error"))); type StdResult = std::result::Result; type Result = StdResult>; async fn respond(_: Request) -> Result>> { let str = PARSED_JSON.read().await.to_string(); let len = str.len(); let mut res = Response::new(Full::new(Bytes::from(str))); res .headers_mut() .append("Content-Type", HeaderValue::from_static("application/json")); let len = HeaderValue::from_str(&len.to_string()); if len.is_ok() { res.headers_mut().append("Content-Length", len.unwrap()); } Ok(res) } async fn fetch_url(url: hyper::Uri) -> Result> { if url.scheme_str() != Some("http") { return Err(Box::new(Error::new( std::io::ErrorKind::InvalidInput, "This function only works with HTTP URIs.", ))); } let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); let addr = format!("{}:{}", host, port); let stream = TcpStream::connect(addr).await?; let io = TokioIo::new(stream); let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; tokio::task::spawn(async move { if let Err(err) = conn.await { println!("Connection failed: {:?}", err); } }); let authority = url.authority().unwrap().clone(); let path = url.path(); let req = Request::builder() .uri(path) .header(hyper::header::HOST, authority.as_str()) .body(Empty::::new())?; Ok(sender.send_request(req).await?) } fn get_request_target_uri() -> Uri { env::var("REQUEST_TO") .unwrap_or("http://10.0.0.77:8080/".to_string()) .parse::() .unwrap() } async fn get_is_open() -> Result { let mut res = fetch_url(get_request_target_uri()).await?; let mut out = "".to_string(); while let Some(next) = res.frame().await { let frame = next?; if let Some(chunk) = frame.data_ref() { out = format!("{out}{}", std::str::from_utf8(chunk).unwrap()); } } let state = out.trim().starts_with("a"); Ok(state) } #[tokio::main] async fn main() -> Result<()> { { let state = { let mut parsed_json = PARSED_JSON.write().await; if !parsed_json.is_object() { panic!("api.json must have top-level object!"); } let parsed_json_obj = parsed_json.as_object_mut().unwrap(); let state = parsed_json_obj.get_mut("state"); let blank_map = Map::new(); let state = if state.is_some() { state.unwrap() } else { &mut Value::Object(blank_map) }; let state_obj = state .as_object_mut() .expect("State is non-undefined non-object."); state_obj.remove_entry("open"); state.clone() }; // ensure state is present let mut parsed_json = PARSED_JSON.write().await; let parsed_json_obj = parsed_json.as_object_mut().unwrap(); parsed_json_obj.insert("state".to_string(), state); } task::spawn(async { loop { { let is_open = get_is_open().await; let mut parsed_json = PARSED_JSON.write().await; let state = parsed_json.get_mut("state"); if state.is_none() { panic!("State is none!"); }; let state = state.unwrap().as_object_mut(); if state.is_none() { panic!("State was turned into non-object!"); } let state = state.unwrap(); if is_open.is_ok() { let is_open = is_open.unwrap(); state.insert("open".to_string(), serde_json::Value::Bool(is_open)); } else { state.remove("open"); eprintln!("Failed to fetch open status: {:#?}", is_open.unwrap_err()); } } time::sleep(Duration::from_secs(1)).await; } }); let addr: SocketAddr = env::var("LISTEN_ON") .unwrap_or("127.0.0.1:3000".to_string()) .parse()?; // We create a TcpListener and bind it to 127.0.0.1:3000 let listener = TcpListener::bind(addr).await?; println!( "Listening on: LISTEN_ON={addr:#?} Sending fetch requests to: REQUEST_TO={:#?}", get_request_target_uri() ); // We start a loop to continuously accept incoming connections loop { let (stream, _) = listener.accept().await?; // Use an adapter to access something implementing `tokio::io` traits as if they implement // `hyper::rt` IO traits. let io = TokioIo::new(stream); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { // Finally, we bind the incoming connection to our `hello` service if let Err(err) = http1::Builder::new() // `service_fn` converts our function in a `Service` .serve_connection(io, service_fn(respond)) .await { eprintln!("Error serving connection: {:?}", err); } }); } }