diff --git a/Cargo.toml b/Cargo.toml index 55a0ed8..1ea2eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "httpbin" -version = "0.1.0" +version = "0.10.0" edition = "2021" license = "Apache-2.0" @@ -12,6 +12,7 @@ rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.68" tokio = { version = "1.0", features = ["full"] } +tokio-util = { version = "0.7.8", features = ["io"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/README.md b/README.md index 8012998..f4d35f5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is a reimplementation of `httpbin` for two purposes: 1. To demonstrate (and test) the abilities of an http library for rust -2. To make a static binary (1.6MB) providing all the httpbin functionality +2. To make a static binary (4.6MB) providing all the httpbin functionality (not affiliated to the original httpbin) diff --git a/src/routes/images.rs b/src/routes/images.rs new file mode 100644 index 0000000..74f91b8 --- /dev/null +++ b/src/routes/images.rs @@ -0,0 +1,113 @@ +use axum::{ + body::StreamBody, + http::{header::{self}, StatusCode, header::{ACCEPT, HeaderMap}}, + response::{Html, IntoResponse}, + routing::get, + {Router} +}; + +use tokio_util::io::ReaderStream; + +const SVG_LOGO: &str = include_str!("../templates/images/svg_logo.svg"); + +pub fn routes() -> Router { + Router::new().route("/image/svg", get(svg)) + .route("/image/jpeg", get(jpeg)) + .route("/image/png", get(png)) + .route("/image/webp", get(webp)) + .route("/image", get(image)) + .route("/favicon.ico", get(favicon)) +} + +async fn svg() -> Html<&'static str> { + SVG_LOGO.into() +} + +async fn favicon() -> impl IntoResponse { + // `File` implements `AsyncRead` + let file = match tokio::fs::File::open("static/favicon.ico").await { + Ok(file) => file, + Err(err) => return Err((StatusCode::NOT_FOUND, format!("File not found: {}", err))), + }; + // convert the `AsyncRead` into a `Stream` + let stream = ReaderStream::new(file); + // convert the `Stream` into an `axum::body::HttpBody` + let body = StreamBody::new(stream); + + Ok((StatusCode::OK, [(header::CONTENT_TYPE, "image/vnd.microsoft.icon")], body).into_response()) +} + +async fn jpeg() -> impl IntoResponse { + let file = match tokio::fs::File::open("src/templates/images/jackal.jpg").await { + Ok(file) => file, + Err(err) => return Err((StatusCode::NOT_FOUND, format!("File not found: {}", err))), + }; + let stream = ReaderStream::new(file); + let body = StreamBody::new(stream); + Ok((StatusCode::OK, [(header::CONTENT_TYPE, "image/jpeg")], body).into_response()) +} + +async fn png() -> impl IntoResponse { + let file = match tokio::fs::File::open("src/templates/images/pig_icon.png").await { + Ok(file) => file, + Err(err) => return Err((StatusCode::NOT_FOUND, format!("File not found: {}", err))), + }; + let stream = ReaderStream::new(file); + let body = StreamBody::new(stream); + Ok((StatusCode::OK, [(header::CONTENT_TYPE, "image/png")], body).into_response()) +} + +async fn webp() -> impl IntoResponse { + let file = match tokio::fs::File::open("src/templates/images/wolf_1.webp").await { + Ok(file) => file, + Err(err) => return Err((StatusCode::NOT_FOUND, format!("File not found: {}", err))), + }; + let stream = ReaderStream::new(file); + let body = StreamBody::new(stream); + Ok((StatusCode::OK, [(header::CONTENT_TYPE, "image/webp")], body).into_response()) +} + +async fn image(headers: HeaderMap) -> impl IntoResponse { + match headers.get(ACCEPT).map(|x| x.as_bytes()) { + Some(b"image/svg+xml") => svg().await.into_response(), + Some(b"image/jpeg") => jpeg().await.into_response(), + Some(b"image/webp") => webp().await.into_response(), + Some(b"image/*") => png().await.into_response(), + _ => png().await.into_response(), // Python implementation returns status 406 for all other + // types (except if no Accept header is present) + }.into_response() +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{header, HeaderValue, Request, StatusCode}, + }; + use tower::ServiceExt; + + #[tokio::test] + async fn svg() { + let app = routes(); + + let response = app + .oneshot( + Request::builder() + .uri("/image/svg") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static(mime::IMAGE_SVG.as_ref())) + ); + + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert!(std::str::from_utf8(&body).is_ok()) + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 8c91dac..ac20261 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,3 +1,4 @@ +pub mod images; pub mod request_inspection; pub mod response_formats; pub mod root; diff --git a/src/routes/response_formats.rs b/src/routes/response_formats.rs index 32e9e16..376de2e 100644 --- a/src/routes/response_formats.rs +++ b/src/routes/response_formats.rs @@ -1,15 +1,26 @@ -use axum::{response::Html, routing::get, Router}; +use axum::{response::Html, response::IntoResponse, routing::get, Router, + http::{StatusCode, header::{self}}}; const UTF8_PAGE: &str = include_str!("../templates/utf8.html"); +const JSON_PAGE: &str = include_str!("../templates/json.json"); pub fn routes() -> Router { Router::new().route("/encoding/utf8", get(utf8)) + .route("/json", get(json)) } async fn utf8() -> Html<&'static str> { UTF8_PAGE.into() } +async fn json() -> impl IntoResponse { +( + StatusCode::OK, + [(header::CONTENT_TYPE, mime::APPLICATION_JSON.essence_str())], + JSON_PAGE, +) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/routes/root.rs b/src/routes/root.rs index a1c0807..83b11cf 100644 --- a/src/routes/root.rs +++ b/src/routes/root.rs @@ -8,15 +8,23 @@ use axum::{ use minijinja::render; const INDEX_TEMPLATE: &str = include_str!("../templates/index.html"); +const HTML_TEMPLATE: &str = include_str!("../templates/moby.html"); const OPENAPI_SPECIFICATION: &str = include_str!("../templates/openapi.yaml"); +const ROBOTS_TEMPLATE: &str = include_str!("../templates/robots.txt"); +const HUMANS_TEMPLATE: &str = include_str!("../templates/humans.txt"); +const PLUGIN_TEMPLATE: &str = include_str!("../templates/ai-plugin.json"); const NOT_FOUND_PAGE: &str = include_str!("../templates/not_found.html"); const API_DOCS_LOCATION: &str = "https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/postman-open-technologies/httpbin-rs/main/src/templates/openapi.yaml&nocors"; pub fn routes() -> Router { Router::new() .route("/", get(index)) + .route("/html", get(html)) .route("/api-docs", get(api_docs)) .route("/openapi.yaml", get(openapi)) + .route("/robots.txt", get(robots)) + .route("/.well-known/humans.txt", get(humans)) + .route("/.well-known/ai-plugin.json", get(plugin)) .fallback(not_found) } @@ -39,6 +47,31 @@ async fn api_docs() -> impl IntoResponse { ) } +async fn robots() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/plain")], + ROBOTS_TEMPLATE, + ) +} + +async fn humans() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/plain")], + HUMANS_TEMPLATE, + ) +} + +async fn plugin() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "application/json")], + PLUGIN_TEMPLATE, + ) +} + +async fn html() -> Html { + render!(HTML_TEMPLATE, prefix => "").into() +} + async fn not_found() -> impl IntoResponse { (StatusCode::NOT_FOUND, Html(NOT_FOUND_PAGE)) } diff --git a/src/server.rs b/src/server.rs index b538aa6..cae0cea 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,4 @@ -use crate::routes::{request_inspection, response_formats, root, status_codes}; +use crate::routes::{images, request_inspection, response_formats, root, status_codes}; use axum::{ http::{header, HeaderValue, Method, Request, StatusCode}, middleware::{from_fn, Next}, @@ -13,6 +13,7 @@ pub fn app() -> Router { .merge(request_inspection::routes()) .merge(response_formats::routes()) .merge(status_codes::routes()) + .merge(images::routes()) .layer(from_fn(inject_server_header)) .layer(from_fn(inject_cors_headers)) } diff --git a/src/templates/ai-plugin.json b/src/templates/ai-plugin.json new file mode 100644 index 0000000..6c14527 --- /dev/null +++ b/src/templates/ai-plugin.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_human": "HTTPBin Plugin", + "name_for_model": "httpbin", + "description_for_human": "Plugin for accessing HTTPBin functionality.", + "description_for_model": "Plugin for accessing HTTPBin functionality. This plugin can provide information on network requests such as IP addresses, request headers, cookies, user-agents etc. It can also respond with various data such as JSON, XML, images, favicons, robots.txt, .well-known/humans.txt etc.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "http://httpbin.org/openapi.yaml", + "is_user_authenticated": false + }, + "logo_url": "http://httpbin.org/image/svg", + "contact_email": "mike.ralphson@gmail.com", + "legal_info_url": "http://httpbin.org/terms" +} diff --git a/src/templates/humans.txt b/src/templates/humans.txt new file mode 100644 index 0000000..37c0f9a --- /dev/null +++ b/src/templates/humans.txt @@ -0,0 +1,4 @@ +Pascal Heus +Doc Jones +Mike Ralphson +Kevin Swiber diff --git a/src/templates/images/jackal.jpg b/src/templates/images/jackal.jpg new file mode 100644 index 0000000..a4e824c Binary files /dev/null and b/src/templates/images/jackal.jpg differ diff --git a/src/templates/images/pig_icon.png b/src/templates/images/pig_icon.png new file mode 100644 index 0000000..cc0c128 Binary files /dev/null and b/src/templates/images/pig_icon.png differ diff --git a/src/templates/images/svg_logo.svg b/src/templates/images/svg_logo.svg new file mode 100644 index 0000000..1b59410 --- /dev/null +++ b/src/templates/images/svg_logo.svg @@ -0,0 +1,259 @@ + + + SVG Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVG + + + + + + + + diff --git a/src/templates/images/wolf_1.webp b/src/templates/images/wolf_1.webp new file mode 100644 index 0000000..37f5392 Binary files /dev/null and b/src/templates/images/wolf_1.webp differ diff --git a/src/templates/index.html b/src/templates/index.html index cd17911..b023b20 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -3,6 +3,7 @@ httpbin-rs: HTTP Client Testing Service +