Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "httpbin"
version = "0.1.0"
version = "0.10.0"
edition = "2021"
license = "Apache-2.0"

Expand All @@ -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"] }

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
113 changes: 113 additions & 0 deletions src/routes/images.rs
Original file line number Diff line number Diff line change
@@ -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())
}
}
1 change: 1 addition & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod images;
pub mod request_inspection;
pub mod response_formats;
pub mod root;
Expand Down
13 changes: 12 additions & 1 deletion src/routes/response_formats.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down
33 changes: 33 additions & 0 deletions src/routes/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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<String> {
render!(HTML_TEMPLATE, prefix => "").into()
}

async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, Html(NOT_FOUND_PAGE))
}
Expand Down
3 changes: 2 additions & 1 deletion src/server.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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))
}
Expand Down
18 changes: 18 additions & 0 deletions src/templates/ai-plugin.json
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 4 additions & 0 deletions src/templates/humans.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Pascal Heus
Doc Jones
Mike Ralphson
Kevin Swiber
Binary file added src/templates/images/jackal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/templates/images/pig_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading