Encode html as base64 & return JSON

This commit is contained in:
august kline 2024-08-31 17:59:04 -04:00
parent 597baba21b
commit 266c16b9a8
3 changed files with 176 additions and 61 deletions

9
Cargo.lock generated
View File

@ -98,6 +98,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bytes"
version = "1.7.1"
@ -294,7 +300,10 @@ name = "manserve"
version = "0.1.0"
dependencies = [
"axum",
"base64",
"http-body-util",
"serde",
"serde_json",
"tokio",
"tower 0.5.0",
"tracing-subscriber",

View File

@ -5,7 +5,10 @@ edition = "2021"
[dependencies]
axum = "0.7.5"
base64 = "0.22.1"
http-body-util = "0.1.2"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["rt-multi-thread"] }
tower = { version = "0.5.0", features = ["limit", "buffer"] }
tracing-subscriber = "0.3.18"

View File

@ -1,88 +1,191 @@
use std::{
env,
env, io,
process::{exit, Command},
string::FromUtf8Error,
time::Duration,
};
use axum::{
error_handling::HandleErrorLayer, extract::Path, http::StatusCode, response::Html,
routing::get, BoxError, Router,
error_handling::HandleErrorLayer,
extract::Path,
http::{header, StatusCode},
response::IntoResponse,
routing::get,
BoxError, Router,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use serde::Deserialize;
use tower::{buffer::BufferLayer, limit::rate::RateLimitLayer, ServiceBuilder};
#[tokio::main]
async fn main() {
// initialize tracing
tracing_subscriber::fmt::init();
// idk how to have one rate limiting block apply to both of these but this works so it's fine
let app = Router::new()
.route(
"/:name",
get(command).layer(
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))),
),
)
.route(
"/",
get(StatusCode::BAD_REQUEST).layer(
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))),
),
);
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Provide a port with manserve $PORT");
exit(1);
}
if args[1].parse::<u16>().is_ok() {
if args[1].parse::<u16>().is_err() {
println!("Could not parse port {}", args[1]);
exit(1);
}
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();
} else {
println!("Could not parse port {}", args[1]);
exit(1);
axum::serve(listener, app()).await.unwrap();
}
fn app() -> Router {
Router::new()
.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))),
)
.route("/:command", get(command))
}
#[derive(Deserialize, Debug)]
struct Manpage {
command: String,
html: String,
}
#[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
}
}
async fn command(Path(name): Path<String>) -> Result<Html<String>, StatusCode> {
let mut manpage_loc = String::from_utf8(
Command::new("man")
.args(["-w", name.as_str()])
.output()
.unwrap()
.stdout,
)
.unwrap();
manpage_loc.pop();
let man = Command::new("mandoc")
fn get_man_loc(command: &str) -> Result<String, ManserveErr> {
let output = Command::new("man").args(["-w", command]).output()?;
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")
.arg("-Thtml")
.arg("-O")
.arg("fragment")
.arg(manpage_loc)
.output()
.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, "text/json")],
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, "text/json")],
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, "text/json")],
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, "text/json")],
json.into_response(),
)
}
},
}
}
#[cfg(test)]
mod tests {
use axum::{body::Body, http::Request};
use http_body_util::BodyExt;
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();
match man.status.success() {
true => Ok(Html(String::from_utf8(man.stdout).unwrap())),
false => Err(StatusCode::BAD_REQUEST),
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() {
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)
}
}