Encode html as base64 & return JSON
This commit is contained in:
parent
597baba21b
commit
266c16b9a8
|
@ -98,6 +98,12 @@ dependencies = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
|
@ -294,7 +300,10 @@ name = "manserve"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"base64",
|
||||||
|
"http-body-util",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.0",
|
"tower 0.5.0",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
|
@ -5,7 +5,10 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
http-body-util = "0.1.2"
|
||||||
serde = { version = "1.0.209", features = ["derive"] }
|
serde = { version = "1.0.209", features = ["derive"] }
|
||||||
|
serde_json = "1.0.127"
|
||||||
tokio = { version = "1.39.3", features = ["rt-multi-thread"] }
|
tokio = { version = "1.39.3", features = ["rt-multi-thread"] }
|
||||||
tower = { version = "0.5.0", features = ["limit", "buffer"] }
|
tower = { version = "0.5.0", features = ["limit", "buffer"] }
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
|
|
219
src/main.rs
219
src/main.rs
|
@ -1,88 +1,191 @@
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env, io,
|
||||||
process::{exit, Command},
|
process::{exit, Command},
|
||||||
|
string::FromUtf8Error,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
error_handling::HandleErrorLayer, extract::Path, http::StatusCode, response::Html,
|
error_handling::HandleErrorLayer,
|
||||||
routing::get, BoxError, Router,
|
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};
|
use tower::{buffer::BufferLayer, limit::rate::RateLimitLayer, ServiceBuilder};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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();
|
let args: Vec<String> = env::args().collect();
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
println!("Provide a port with manserve $PORT");
|
println!("Provide a port with manserve $PORT");
|
||||||
exit(1);
|
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]))
|
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", args[1]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("Starting server on port {}", args[1]);
|
println!("Starting server on port {}", args[1]);
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app()).await.unwrap();
|
||||||
} else {
|
}
|
||||||
println!("Could not parse port {}", args[1]);
|
|
||||||
exit(1);
|
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> {
|
fn get_man_loc(command: &str) -> Result<String, ManserveErr> {
|
||||||
let mut manpage_loc = String::from_utf8(
|
let output = Command::new("man").args(["-w", command]).output()?;
|
||||||
Command::new("man")
|
if !output.status.success() {
|
||||||
.args(["-w", name.as_str()])
|
return Err(ManserveErr::NotFound);
|
||||||
.output()
|
}
|
||||||
.unwrap()
|
let mut loc = String::from_utf8(output.stdout)?;
|
||||||
.stdout,
|
loc.pop();
|
||||||
)
|
Ok(loc)
|
||||||
.unwrap();
|
}
|
||||||
manpage_loc.pop();
|
|
||||||
let man = Command::new("mandoc")
|
fn generate_html(command: &str) -> Result<String, ManserveErr> {
|
||||||
|
let man_page = get_man_loc(command)?;
|
||||||
|
let output = Command::new("mandoc")
|
||||||
.arg("-Thtml")
|
.arg("-Thtml")
|
||||||
.arg("-O")
|
.arg("-O")
|
||||||
.arg("fragment")
|
.arg("fragment")
|
||||||
.arg(manpage_loc)
|
.arg(man_page.as_str())
|
||||||
.output()
|
.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();
|
.unwrap();
|
||||||
match man.status.success() {
|
assert_eq!(res.status(), StatusCode::OK)
|
||||||
true => Ok(Html(String::from_utf8(man.stdout).unwrap())),
|
}
|
||||||
false => Err(StatusCode::BAD_REQUEST),
|
|
||||||
|
#[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