Encode html as base64 & return JSON
This commit is contained in:
parent
597baba21b
commit
266c16b9a8
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
225
src/main.rs
225
src/main.rs
|
@ -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() {
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
.unwrap();
|
||||
match man.status.success() {
|
||||
true => Ok(Html(String::from_utf8(man.stdout).unwrap())),
|
||||
false => Err(StatusCode::BAD_REQUEST),
|
||||
.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();
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue