manserve/src/main.rs

203 lines
5.8 KiB
Rust
Raw Normal View History

2024-08-30 12:25:53 -04:00
use std::{
2024-08-31 17:59:04 -04:00
env, io,
2024-08-30 12:25:53 -04:00
process::{exit, Command},
2024-08-31 17:59:04 -04:00
string::FromUtf8Error,
2024-08-30 12:25:53 -04:00
time::Duration,
};
use axum::{
2024-08-31 17:59:04 -04:00
error_handling::HandleErrorLayer,
extract::Path,
2024-09-02 12:17:22 -04:00
http::{header, HeaderValue, Method, StatusCode},
2024-08-31 17:59:04 -04:00
response::IntoResponse,
routing::get,
BoxError, Router,
2024-08-30 12:25:53 -04:00
};
2024-08-31 17:59:04 -04:00
use base64::{engine::general_purpose::STANDARD, Engine};
2024-08-30 12:25:53 -04:00
use tower::{buffer::BufferLayer, limit::rate::RateLimitLayer, ServiceBuilder};
2024-09-02 12:17:22 -04:00
use tower_http::cors::CorsLayer;
2024-08-30 12:25:53 -04:00
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Provide a port with manserve $PORT");
exit(1);
}
2024-08-31 17:59:04 -04:00
if args[1].parse::<u16>().is_err() {
2024-08-30 12:25:53 -04:00
println!("Could not parse port {}", args[1]);
exit(1);
}
2024-08-31 17:59:04 -04:00
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", args[1]))
.await
.unwrap();
println!("Starting server on port {}", args[1]);
axum::serve(listener, app()).await.unwrap();
2024-08-30 12:25:53 -04:00
}
2024-08-31 17:59:04 -04:00
fn app() -> Router {
Router::new()
2024-09-02 13:12:21 -04:00
.route("/:command", get(command))
2024-08-31 17:59:04 -04:00
.layer(
// Throttle incoming traffic
ServiceBuilder::new()
.layer(HandleErrorLayer::new(|err: BoxError| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled error: {err}"),
)
}))
.layer(BufferLayer::new(1024))
.layer(RateLimitLayer::new(5, Duration::from_secs(1))),
)
2024-09-02 12:17:22 -04:00
.layer(
CorsLayer::new()
.allow_origin("*".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET]),
)
2024-08-31 17:59:04 -04:00
}
#[derive(Debug)]
enum ManserveErr {
NotFound,
IOError,
InvalidUtf,
}
impl From<io::Error> for ManserveErr {
fn from(_value: io::Error) -> Self {
ManserveErr::IOError
}
}
impl From<FromUtf8Error> for ManserveErr {
fn from(_value: FromUtf8Error) -> Self {
ManserveErr::InvalidUtf
}
}
fn get_man_loc(command: &str) -> Result<String, ManserveErr> {
2024-09-02 12:17:22 -04:00
let output = match Command::new("man").args(["-w", command]).output() {
Ok(output) => output,
Err(error) => {
println!("{error}");
return Err(error.into());
}
};
2024-08-31 17:59:04 -04:00
if !output.status.success() {
return Err(ManserveErr::NotFound);
}
let mut loc = String::from_utf8(output.stdout)?;
loc.pop();
Ok(loc)
}
fn generate_html(command: &str) -> Result<String, ManserveErr> {
let man_page = get_man_loc(command)?;
let output = Command::new("mandoc")
2024-08-30 12:25:53 -04:00
.arg("-Thtml")
.arg("-O")
.arg("fragment")
2024-08-31 17:59:04 -04:00
.arg(man_page.as_str())
.output()?;
let encoded = STANDARD.encode(String::from_utf8(output.stdout)?);
Ok(encoded)
}
async fn command(Path(command): Path<String>) -> impl IntoResponse {
let command = command.as_str();
match generate_html(command) {
Ok(html) => {
let json = format!("{{\"command\": \"{command}\", \"html\": \"{html}\"}}");
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
2024-08-31 17:59:04 -04:00
json.into_response(),
)
}
Err(error) => match error {
ManserveErr::NotFound => {
let json = format!("{{\"command\": \"{command}\", \"error\": \"Could not find the requested command\"}}");
(
StatusCode::NOT_FOUND,
[(header::CONTENT_TYPE, "application/json")],
2024-08-31 17:59:04 -04:00
json.into_response(),
)
}
ManserveErr::IOError => {
let json = format!("{{\"command\": \"{command}\", \"error\": \"The server could not complete the request\"}}");
(
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, "application/json")],
2024-08-31 17:59:04 -04:00
json.into_response(),
)
}
ManserveErr::InvalidUtf => {
let json = format!("{{\"command\": \"{command}\", \"error\": \"There was invalid utf-8 in the request\"}}");
(
StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, "application/json")],
2024-08-31 17:59:04 -04:00
json.into_response(),
)
}
},
}
}
#[cfg(test)]
mod tests {
use axum::{body::Body, http::Request};
use http_body_util::BodyExt;
use serde::Deserialize;
2024-08-31 17:59:04 -04:00
use tower::Service;
use super::*;
#[tokio::test]
async fn json() {
let mut app = app();
let res = app
.call(Request::builder().uri("/ls").body(Body::default()).unwrap())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK)
}
#[tokio::test]
async fn bad_command() {
let mut app = app();
let res = app
.call(
Request::builder()
.uri("/lollll")
.body(Body::default())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND)
}
#[tokio::test]
async fn man_ls() {
#[derive(Deserialize, Debug)]
struct Manpage {
command: String,
html: String,
}
2024-08-31 17:59:04 -04:00
let mut app = app();
let res = app
.call(Request::builder().uri("/ls").body(Body::default()).unwrap())
.await
.unwrap();
let expected = generate_html("ls").unwrap();
let res_str =
String::from_utf8(res.into_body().collect().await.unwrap().to_bytes().into()).unwrap();
let manpage: Manpage = serde_json::from_str(&res_str).unwrap();
assert_eq!(manpage.html, expected)
2024-08-30 12:25:53 -04:00
}
}