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,
|
2024-08-31 18:04:01 -04:00
|
|
|
[(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,
|
2024-08-31 18:04:01 -04:00
|
|
|
[(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,
|
2024-08-31 18:04:01 -04:00
|
|
|
[(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,
|
2024-08-31 18:04:01 -04:00
|
|
|
[(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;
|
2024-08-31 18:04:01 -04:00
|
|
|
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() {
|
2024-08-31 18:04:01 -04:00
|
|
|
#[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
|
|
|
}
|
|
|
|
}
|