From bfb995563f41b86de2f0a82999c99391f7f00582 Mon Sep 17 00:00:00 2001 From: memdmp Date: Sun, 18 May 2025 11:41:01 +0200 Subject: feat: initial commit --- src/main.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/main.rs (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7c63c09 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,176 @@ +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::json; +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 state = if state.is_some() { + state.unwrap() + } else { + &mut json!("{}") + }; + let state_obj = state.as_object_mut().unwrap(); + 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); + } + }); + } +} -- cgit v1.2.3