├── .gitignore ├── httpbin-site ├── .gitignore ├── Dockerfile ├── config.toml ├── content │ ├── _index.md │ └── logo.svg └── templates │ └── index.html ├── httpbin ├── src │ ├── lib.rs │ ├── data.rs │ ├── cli.rs │ └── data │ │ ├── base64.rs │ │ └── uuid.rs └── Cargo.toml ├── .cargo └── config.toml ├── .dockerignore ├── .vscode └── extensions.json ├── .gitmodules ├── httpbin-salvo ├── src │ ├── data.rs │ ├── main.rs │ ├── request_inspection.rs │ ├── data │ │ └── base64.rs │ └── http_method.rs └── Cargo.toml ├── httpbin-actix ├── src │ ├── data.rs │ ├── main.rs │ ├── request_inspection.rs │ ├── data │ │ ├── base64.rs │ │ └── uuid.rs │ └── http_method.rs └── Cargo.toml ├── httpbin-poem ├── src │ ├── data.rs │ ├── utils.rs │ ├── main.rs │ ├── request_inspection.rs │ ├── data │ │ ├── base64.rs │ │ └── uuid.rs │ └── http_method.rs └── Cargo.toml ├── httpbin-axum ├── src │ ├── data.rs │ ├── main.rs │ ├── request_inspection.rs │ ├── data │ │ ├── base64.rs │ │ └── uuid.rs │ └── http_method.rs └── Cargo.toml ├── httpbin-rocket ├── src │ ├── data.rs │ ├── main.rs │ ├── request_inspection.rs │ ├── http_method.rs │ └── data │ │ └── base64.rs └── Cargo.toml ├── httpbin-poem-openapi ├── src │ ├── data.rs │ ├── main.rs │ ├── request_inspection.rs │ ├── http_method.rs │ └── data │ │ ├── base64.rs │ │ └── uuid.rs └── Cargo.toml ├── httpbin.toml ├── README.md ├── Dockerfile ├── renovate.json ├── CHANGELOG.md ├── httpbin.prod.toml ├── LICENSE-MIT ├── Cargo.toml ├── .github └── workflows │ ├── site.yaml │ └── ci.yaml └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /httpbin-site/.gitignore: -------------------------------------------------------------------------------- 1 | public/ 2 | -------------------------------------------------------------------------------- /httpbin/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod data; 3 | -------------------------------------------------------------------------------- /httpbin/src/data.rs: -------------------------------------------------------------------------------- 1 | pub mod base64; 2 | pub mod uuid; 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "uuid_unstable"] 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .github/ 3 | .vscode/ 4 | httpbin-site/ 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "httpbin-site/themes/juice"] 2 | path = httpbin-site/themes/juice 3 | url = https://github.com/huhu/juice 4 | -------------------------------------------------------------------------------- /httpbin-salvo/src/data.rs: -------------------------------------------------------------------------------- 1 | use salvo::Router; 2 | 3 | mod base64; 4 | 5 | pub fn api() -> Router { 6 | Router::new().push(Router::with_path("/base64").push(base64::api())) 7 | } 8 | -------------------------------------------------------------------------------- /httpbin-actix/src/data.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::ServiceConfig; 2 | 3 | mod base64; 4 | mod uuid; 5 | 6 | pub fn api(cfg: &mut ServiceConfig) { 7 | cfg.configure(base64::api).configure(uuid::api); 8 | } 9 | -------------------------------------------------------------------------------- /httpbin-poem/src/data.rs: -------------------------------------------------------------------------------- 1 | use poem::Route; 2 | 3 | use crate::utils::RouteExt; 4 | 5 | mod base64; 6 | mod uuid; 7 | 8 | pub fn api(route: Route) -> Route { 9 | route.attach(base64::api).attach(uuid::api) 10 | } 11 | -------------------------------------------------------------------------------- /httpbin-axum/src/data.rs: -------------------------------------------------------------------------------- 1 | use axum::Router; 2 | 3 | pub mod base64; 4 | pub mod uuid; 5 | 6 | pub fn api() -> Router { 7 | Router::new() 8 | .nest("/base64", base64::api()) 9 | .nest("/uuid", uuid::api()) 10 | } 11 | -------------------------------------------------------------------------------- /httpbin-rocket/src/data.rs: -------------------------------------------------------------------------------- 1 | use rocket::{fairing::AdHoc, Build, Rocket}; 2 | 3 | mod base64; 4 | 5 | pub async fn api(rocket: Rocket) -> Rocket { 6 | rocket.attach(AdHoc::on_ignite("mount_data_base64", base64::api)) 7 | } 8 | -------------------------------------------------------------------------------- /httpbin-site/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/getzola/zola:v0.17.2 as builder 2 | COPY . /app 3 | WORKDIR /app 4 | RUN ["/bin/zola", "build"] 5 | 6 | FROM nginx:stable-alpine as site 7 | COPY --from=builder /app/public /usr/share/nginx/html 8 | EXPOSE 8080 -------------------------------------------------------------------------------- /httpbin-poem/src/utils.rs: -------------------------------------------------------------------------------- 1 | use poem::Route; 2 | 3 | pub(crate) trait RouteExt { 4 | fn attach(self, f: impl Fn(Self) -> Self) -> Self 5 | where 6 | Self: Sized, 7 | { 8 | f(self) 9 | } 10 | } 11 | 12 | impl RouteExt for Route {} 13 | -------------------------------------------------------------------------------- /httpbin-poem-openapi/src/data.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::{OpenApi, Tags}; 2 | 3 | pub mod base64; 4 | pub mod uuid; 5 | 6 | #[derive(Tags)] 7 | enum DataTag { 8 | /// Generates useful data 9 | Data, 10 | } 11 | 12 | pub fn api() -> impl OpenApi { 13 | (base64::Api, uuid::Api) 14 | } 15 | -------------------------------------------------------------------------------- /httpbin.toml: -------------------------------------------------------------------------------- 1 | # Public HTTPBIN-RS Config File 2 | 3 | # All servers are run in docker containers 4 | ip = "127.0.0.1" 5 | port = 8080 6 | 7 | # The following configures the OpenAPI documentation 8 | [openapi] 9 | contact = { name = "duskmoon (developer)", url = "https://duskmoon314.com", email = "kp.campbell.he@duskmoon314.com" } 10 | external_document = { description = "Source code (GitHub)", url = "https://github.com/duskmoon314/httpbin-rs" } 11 | 12 | [openapi.servers.poem-openapi] 13 | description = "poem-openapi implementation" 14 | url = "http://127.0.0.1:8080" 15 | -------------------------------------------------------------------------------- /httpbin-rocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin-rocket" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | httpbin = { workspace = true } 14 | infer = { workspace = true } 15 | rocket = { version = "0.5.0", features = ["json"] } 16 | rocket_cors = "0.6.0" 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | serde_qs = { workspace = true } 20 | -------------------------------------------------------------------------------- /httpbin-actix/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin-actix" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | actix-cors = "0.6.4" 13 | actix-web = "4.4.0" 14 | anyhow = { workspace = true } 15 | env_logger = { workspace = true } 16 | httpbin = { workspace = true } 17 | infer = { workspace = true } 18 | log = { workspace = true } 19 | serde = { workspace = true } 20 | serde_json = { workspace = true } 21 | serde_qs = { workspace = true } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpbin-rs 2 | 3 | HTTP Request & Response Service pretty like [httpbin](http://httpbin.org) but powered by Rust 4 | 5 | ## License 6 | 7 | Licensed under either of 8 | 9 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 10 | http://www.apache.org/licenses/LICENSE-2.0) 11 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 12 | 13 | at your option. 14 | 15 | ### Contribution 16 | 17 | Unless you explicitly state otherwise, any Contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 18 | -------------------------------------------------------------------------------- /httpbin-poem/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin-poem" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | httpbin = { workspace = true } 14 | infer = { workspace = true } 15 | poem = { version = "1.3.59", features = ["anyhow"] } 16 | serde = { workspace = true } 17 | serde_json = { workspace = true } 18 | serde_qs = { workspace = true } 19 | tokio = { workspace = true } 20 | tracing-subscriber = { workspace = true } 21 | -------------------------------------------------------------------------------- /httpbin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | base64 = "0.21.5" 13 | clap = { workspace = true } 14 | indexmap = { version = "2.1.0", features = ["serde"] } 15 | serde = { workspace = true } 16 | serde_with = { workspace = true } 17 | thiserror = "1.0.50" 18 | toml = "0.8.8" 19 | uuid = { version = "1.6.1", features = [ 20 | "v1", 21 | "v3", 22 | "v4", 23 | "v5", 24 | "v6", 25 | "v7", 26 | "v8", 27 | ] } 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-alpine AS chef 2 | WORKDIR /app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | ARG HTTPBIN_IMPL=httpbin-poem-openapi 10 | COPY --from=planner /app/recipe.json recipe.json 11 | RUN cargo chef cook --profile release-min --recipe-path recipe.json 12 | COPY . . 13 | RUN cargo build --profile release-min --bin $HTTPBIN_IMPL 14 | 15 | FROM alpine:latest AS runtime 16 | ARG HTTPBIN_IMPL=httpbin-poem-openapi 17 | COPY --from=builder /app/target/release-min/$HTTPBIN_IMPL /usr/local/bin/httpbin 18 | COPY httpbin.toml /etc/httpbin.toml 19 | EXPOSE 8080 20 | ENTRYPOINT httpbin --config /etc/httpbin.toml 21 | 22 | -------------------------------------------------------------------------------- /httpbin-salvo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin-salvo" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | httpbin = { workspace = true } 14 | infer = { workspace = true } 15 | salvo = { version = "0.37.9", features = [ 16 | "anyhow", 17 | "logging", 18 | "cors", 19 | "trailing-slash", 20 | ] } 21 | serde = { workspace = true } 22 | serde_json = { workspace = true } 23 | tokio = { workspace = true } 24 | tracing-subscriber = { workspace = true } 25 | tracing = "*" 26 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "labels": [ 7 | "dependencies", 8 | "no changelog" 9 | ], 10 | "packageRules": [ 11 | { 12 | "matchPackagePatterns": [ 13 | "^axum" 14 | ], 15 | "groupName": "axum crates" 16 | }, 17 | { 18 | "matchPackagePatterns": [ 19 | "^poem" 20 | ], 21 | "groupName": "poem crates" 22 | }, 23 | { 24 | "matchPackagePatterns": [ 25 | "^tower" 26 | ], 27 | "groupName": "tower crates" 28 | }, 29 | { 30 | "matchPackagePatterns": [ 31 | "^serde" 32 | ], 33 | "groupName": "serde crates" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /httpbin-poem-openapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin-poem-openapi" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | clap = { workspace = true } 14 | httpbin = { workspace = true } 15 | infer = { workspace = true } 16 | poem = "1.3.59" 17 | poem-openapi = { version = "3.0.6", features = [ 18 | "swagger-ui", 19 | "rapidoc", 20 | "redoc", 21 | "openapi-explorer", 22 | ] } 23 | serde_json = { workspace = true } 24 | serde_qs = { workspace = true } 25 | tokio = { workspace = true } 26 | tracing-subscriber = { workspace = true } 27 | -------------------------------------------------------------------------------- /httpbin-salvo/src/main.rs: -------------------------------------------------------------------------------- 1 | use httpbin::cli::Cli; 2 | use salvo::cors::Cors; 3 | use salvo::prelude::*; 4 | 5 | mod data; 6 | mod http_method; 7 | mod request_inspection; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let cfg = Cli::parse().load_config(); 12 | 13 | if std::env::var_os("RUST_LOG").is_none() { 14 | std::env::set_var("RUST_LOG", "info"); 15 | } 16 | tracing_subscriber::fmt::init(); 17 | 18 | let cors = Cors::builder().allow_any_origin().build(); 19 | 20 | let router = Router::new() 21 | .hoop(cors) 22 | .hoop(TrailingSlash::new_remove()) 23 | .hoop(Logger) 24 | .push(data::api()) 25 | .push(http_method::api()) 26 | .push(request_inspection::api()); 27 | 28 | Server::new(TcpListener::bind((cfg.ip, cfg.port))) 29 | .serve(router) 30 | .await 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | #### httpbin 13 | 14 | - Use `clap` and `toml` to parse config file 15 | - Add `base64::encode` and `base64::decode` functions 16 | - Add `uuid::new_v{1,3,4,5,6,7,8}` functions 17 | 18 | #### implementations 19 | 20 | - poem-openapi, poem, axum, actix, salvo, rocket 21 | - `HTTP Methods` support 22 | - `Request inspection` support 23 | - `Anything` support 24 | - `Data` support: base64 25 | 26 | #### chore 27 | 28 | - CI to check fmt, lint, build and test 29 | - CI to check CHANGELOG.md 30 | - CI to build and publish docker image 31 | - Add renovate to manage dependencies 32 | -------------------------------------------------------------------------------- /httpbin-site/config.toml: -------------------------------------------------------------------------------- 1 | # The URL the site will be built for 2 | base_url = "/" 3 | 4 | # Whether to automatically compile all Sass files in the sass directory 5 | compile_sass = true 6 | 7 | # Whether to build a search index to be used later on by a JavaScript library 8 | build_search_index = true 9 | 10 | theme = "juice" 11 | 12 | title = "httpbin-rs" 13 | description = "httpbin, but powered by Rust" 14 | 15 | minify_html = true 16 | 17 | [markdown] 18 | # Whether to do syntax highlighting 19 | # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola 20 | highlight_code = true 21 | 22 | [extra] 23 | # Put all your custom variables here 24 | juice_logo_name = "httpbin-rs" 25 | juice_logo_path = "logo.svg" 26 | juice_extra_menu = [ 27 | { title = "Github", link = "https://github.com/duskmoon314/httpbin-rs" }, 28 | ] 29 | repository_url = "https://github.com/duskmoon314/httpbin-rs" 30 | -------------------------------------------------------------------------------- /httpbin-actix/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_cors::Cors; 2 | use actix_web::{middleware, App, HttpServer}; 3 | use anyhow::Result; 4 | use httpbin::cli::Cli; 5 | 6 | mod data; 7 | mod http_method; 8 | mod request_inspection; 9 | 10 | #[actix_web::main] 11 | async fn main() -> Result<()> { 12 | let cfg = Cli::parse().load_config(); 13 | 14 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 15 | 16 | log::info!("Starting httpbin-actix on {}:{}", cfg.ip, cfg.port); 17 | 18 | Ok(HttpServer::new(|| { 19 | let cors = Cors::default().allowed_origin_fn(|_, _| true); 20 | 21 | App::new() 22 | .wrap(middleware::Logger::default()) 23 | .wrap(cors) 24 | .configure(data::api) 25 | .configure(http_method::api) 26 | .configure(request_inspection::api) 27 | }) 28 | .bind((cfg.ip, cfg.port))? 29 | .run() 30 | .await?) 31 | } 32 | -------------------------------------------------------------------------------- /httpbin-axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpbin-axum" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | axum = { version = "0.7.4", features = ["macros"] } 14 | axum-client-ip = "0.5.0" 15 | axum-extra = { version = "0.9.2", features = ["typed-header"] } 16 | clap = { workspace = true } 17 | httpbin = { workspace = true } 18 | infer = { workspace = true } 19 | serde = { workspace = true } 20 | serde_json = { workspace = true } 21 | serde_qs = { workspace = true } 22 | tokio = { workspace = true } 23 | tower-http = { version = "0.5.0", features = [ 24 | "cors", 25 | "normalize-path", 26 | "trace", 27 | ] } 28 | tower-layer = "0.3.2" 29 | tracing-subscriber = { workspace = true } 30 | -------------------------------------------------------------------------------- /httpbin-rocket/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use httpbin::cli::Cli; 3 | use rocket::{fairing::AdHoc, Config}; 4 | 5 | mod data; 6 | mod http_method; 7 | mod request_inspection; 8 | 9 | #[rocket::main] 10 | async fn main() -> Result<()> { 11 | let cfg = Cli::parse().load_config(); 12 | 13 | let rocket_config = Config { 14 | address: cfg.ip, 15 | port: cfg.port, 16 | ..Config::default() 17 | }; 18 | 19 | let cors = rocket_cors::CorsOptions::default() 20 | .allowed_origins(rocket_cors::AllowedOrigins::all()) 21 | .to_cors()?; 22 | 23 | let _ = rocket::custom(rocket_config) 24 | .attach(cors) 25 | .attach(AdHoc::on_ignite("mount_data", data::api)) 26 | .attach(AdHoc::on_ignite("mount_http_method", http_method::api)) 27 | .attach(AdHoc::on_ignite( 28 | "mount_request_inspection", 29 | request_inspection::api, 30 | )) 31 | .launch() 32 | .await?; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /httpbin-poem/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use httpbin::cli::Cli; 3 | use poem::{listener::TcpListener, middleware, EndpointExt, Route, Server}; 4 | 5 | mod data; 6 | mod http_method; 7 | mod request_inspection; 8 | mod utils; 9 | 10 | use utils::*; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let cfg = Cli::parse().load_config(); 15 | 16 | if std::env::var_os("RUST_LOG").is_none() { 17 | std::env::set_var("RUST_LOG", "poem=info"); 18 | } 19 | tracing_subscriber::fmt::init(); 20 | 21 | let app = Route::new() 22 | .attach(data::api) 23 | .attach(request_inspection::api) 24 | .attach(http_method::api) 25 | .with(middleware::Cors::new().allow_origins_fn(|_| true)) 26 | .with(middleware::NormalizePath::new( 27 | middleware::TrailingSlash::Trim, 28 | )) 29 | .with(middleware::Tracing); 30 | 31 | Ok(Server::new(TcpListener::bind((cfg.ip, cfg.port))) 32 | .run(app) 33 | .await?) 34 | } 35 | -------------------------------------------------------------------------------- /httpbin.prod.toml: -------------------------------------------------------------------------------- 1 | # Public HTTPBIN-RS Config File 2 | 3 | # All servers are run in docker containers 4 | ip = "0.0.0.0" 5 | port = 8080 6 | 7 | # The following configures the OpenAPI documentation 8 | [openapi] 9 | contact = { name = "duskmoon (developer)", url = "https://duskmoon314.com", email = "kp.campbell.he@duskmoon314.com" } 10 | external_document = { description = "Source code (GitHub)", url = "https://github.com/duskmoon314/httpbin-rs" } 11 | 12 | [openapi.servers.poem-openapi] 13 | description = "poem-openapi implementation" 14 | url = "https://httpbin.rs" 15 | 16 | [openapi.servers.axum] 17 | description = "axum implementation" 18 | url = "https://axum.httpbin.rs" 19 | 20 | [openapi.servers.actix] 21 | description = "actix implementation" 22 | url = "https://actix.httpbin.rs" 23 | 24 | [openapi.servers.salvo] 25 | description = "salvo implementation" 26 | url = "https://salvo.httpbin.rs" 27 | 28 | [openapi.servers.rocket] 29 | description = "rocket implementation" 30 | url = "https://rocket.httpbin.rs" 31 | 32 | [openapi.servers.poem] 33 | description = "poem implementation" 34 | url = "https://poem.httpbin.rs" 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Campbell He 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "httpbin-actix", 4 | "httpbin-axum", 5 | "httpbin-poem", 6 | "httpbin-poem-openapi", 7 | "httpbin-rocket", 8 | "httpbin-salvo", 9 | "httpbin", 10 | ] 11 | resolver = "2" 12 | 13 | [workspace.package] 14 | version = "0.1.0" 15 | authors = ["duskmoon (Campbell He) "] 16 | description = "HTTP Request & Response Service pretty like [httpbin](http://httpbin.org) but powered by Rust" 17 | license = "MIT OR Apache-2.0" 18 | edition = "2021" 19 | 20 | [workspace.dependencies] 21 | anyhow = "1.0.75" 22 | clap = { version = "4.4.10", features = ["derive"] } 23 | env_logger = "0.10.1" 24 | httpbin = { path = "httpbin" } 25 | infer = "0.15.0" 26 | log = "0.4.20" 27 | serde = { version = "1.0.193", features = ["derive"] } 28 | serde_json = "1.0.108" 29 | serde_qs = "0.12.0" 30 | serde_with = "3.1.0" 31 | tokio = { version = "1.34.0", features = ["full"] } 32 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 33 | 34 | [profile.release-min] 35 | inherits = "release" 36 | # REF: https://github.com/johnthagen/min-sized-rust 37 | strip = true 38 | opt-level = 3 39 | lto = "fat" 40 | codegen-units = 1 41 | -------------------------------------------------------------------------------- /httpbin-axum/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use axum::{extract::Request, Router, ServiceExt}; 3 | use httpbin::cli::Cli; 4 | use tower_http::{ 5 | cors::{AllowOrigin, CorsLayer}, 6 | normalize_path::NormalizePathLayer, 7 | trace::TraceLayer, 8 | }; 9 | use tower_layer::Layer; 10 | 11 | mod data; 12 | mod http_method; 13 | mod request_inspection; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | let cfg = Cli::parse().load_config(); 18 | 19 | if std::env::var_os("RUST_LOG").is_none() { 20 | std::env::set_var("RUST_LOG", "tower_http=debug"); 21 | } 22 | tracing_subscriber::fmt::init(); 23 | 24 | let app = ServiceExt::::into_make_service( 25 | NormalizePathLayer::trim_trailing_slash().layer( 26 | Router::new() 27 | .merge(data::api()) 28 | .merge(request_inspection::api()) 29 | .merge(http_method::api()) 30 | .layer(CorsLayer::new().allow_origin(AllowOrigin::mirror_request())) 31 | .layer(TraceLayer::new_for_http()), 32 | ), 33 | ); 34 | 35 | let listener = tokio::net::TcpListener::bind((cfg.ip, cfg.port)).await?; 36 | 37 | Ok(axum::serve(listener, app).await?) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/site.yaml: -------------------------------------------------------------------------------- 1 | name: Build site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: true 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v3 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | - name: Login to DockerHub 28 | uses: docker/login-action@v3 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Docker meta 34 | id: meta 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: duskmoon/httpbin-rs 38 | flavor: | 39 | suffix=-site,onlatest=true 40 | tags: | 41 | type=semver,pattern={{version}} 42 | type=semver,pattern={{major}} 43 | type=semver,pattern={{major}}.{{minor}} 44 | type=edge,branch=main 45 | 46 | - name: Build and push 47 | uses: docker/build-push-action@v5 48 | with: 49 | context: "./httpbin-site" 50 | push: ${{ github.event_name != 'pull_request' }} 51 | tags: ${{ steps.meta.outputs.tags }} 52 | labels: ${{ steps.meta.outputs.labels }} 53 | cache-from: type=registry,ref=duskmoon/httpbin-rs:${{ steps.meta.outputs.tags }} 54 | cache-to: type=inline 55 | -------------------------------------------------------------------------------- /httpbin-axum/src/request_inspection.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use axum::{http::HeaderMap, routing::get, Json, Router}; 4 | use axum_client_ip::InsecureClientIp; 5 | use axum_extra::{headers::UserAgent as UserAgentHeader, TypedHeader}; 6 | use serde::Serialize; 7 | 8 | #[derive(Serialize)] 9 | struct Headers { 10 | /// The incoming request's HTTP headers 11 | headers: HashMap, 12 | } 13 | 14 | #[derive(Serialize)] 15 | struct Ip { 16 | /// The incoming request's IP address 17 | origin: String, 18 | } 19 | 20 | #[derive(Serialize)] 21 | struct UserAgent { 22 | /// The incoming request's User-Agent header 23 | user_agent: String, 24 | } 25 | 26 | pub fn api() -> Router { 27 | Router::new() 28 | .route("/headers", get(headers)) 29 | .route("/ip", get(ip)) 30 | .route("/user-agent", get(user_agent)) 31 | } 32 | 33 | async fn headers(headers: HeaderMap) -> Json { 34 | let headers = headers 35 | .iter() 36 | .map(|(k, v)| { 37 | ( 38 | k.to_string(), 39 | v.to_str() 40 | .map(|v| v.to_string()) 41 | .unwrap_or_else(|err| err.to_string()), 42 | ) 43 | }) 44 | .collect(); 45 | Json(Headers { headers }) 46 | } 47 | 48 | async fn ip(origin: InsecureClientIp) -> Json { 49 | Json(Ip { 50 | origin: origin.0.to_string(), 51 | }) 52 | } 53 | 54 | async fn user_agent(TypedHeader(user_agent): TypedHeader) -> Json { 55 | Json(UserAgent { 56 | user_agent: user_agent.to_string(), 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /httpbin-site/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "httpbin-rs" 3 | sort_by = "weight" 4 | template = "index.html" 5 | +++ 6 | 7 | Httpbin-rs provides a series of HTTP request & response services powered by Rust and its HTTP frameworks. 8 | 9 | # API docs 10 | 11 | Httpbin-rs provides many online API docs via features of [`poem-openapi`](https://crates.io/crates/poem-openapi): 12 | 13 | - [swagger](https://httpbin.rs/swagger) 14 | - [rapidoc](https://httpbin.rs/rapidoc) 15 | - [redoc](https://httpbin.rs/redoc) 16 | - [openapi-explorer](https://httpbin.rs/openapi-explorer) 17 | 18 | # Implementations 19 | 20 | Httpbin-rs is currently implemented by the following HTTP frameworks: 21 | 22 | | framework | url | 23 | | :------------------------------------------------------ | :------------------------------------------------- | 24 | | [`poem-openapi`](https://crates.io/crates/poem-openapi) | [httpbin.rs](https://httpbin.rs/get) | 25 | | [`poem`](https://crates.io/crates/poem) | [poem.httpbin.rs](https://poem.httpbin.rs/get) | 26 | | [`axum`](https://crates.io/crates/axum) | [axum.httpbin.rs](https://axum.httpbin.rs/get) | 27 | | [`actix-web`](https://crates.io/crates/actix-web) | [actix.httpbin.rs](https://actix.httpbin.rs/get) | 28 | | [`salvo`](https://crates.io/crates/salvo) | [salvo.httpbin.rs](https://salvo.httpbin.rs/get) | 29 | | [`rocket`](https://crates.io/crates/rocket) | [rocket.httpbin.rs](https://rocket.httpbin.rs/get) | 30 | 31 | # Contribution 32 | 33 | Httpbin-rs is an open-source project and welcomes contributions. If you are familiar with any HTTP framework, please feel free to create a PR to improve httpbin-rs. 34 | -------------------------------------------------------------------------------- /httpbin/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use clap::Parser; 4 | use indexmap::IndexMap; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(author, version, about)] 9 | pub struct Cli { 10 | // /// The IP to listen on. (default: 127.0.0.1) 11 | // #[clap(long, value_parser, default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))] 12 | // pub ip: IpAddr, 13 | 14 | // /// The port to listen on. (default: 8000) 15 | // #[clap(long, value_parser, default_value_t = 8000)] 16 | // pub port: u16, 17 | /// The config file to use. 18 | #[clap(long, value_parser, default_value_t = String::from("httpbin.toml"))] 19 | pub config: String, 20 | } 21 | 22 | impl Cli { 23 | pub fn parse() -> Self { 24 | ::parse() 25 | } 26 | 27 | pub fn load_config(&self) -> Config { 28 | let config = std::fs::read_to_string(&self.config).unwrap(); 29 | toml::from_str(&config).unwrap() 30 | } 31 | } 32 | 33 | #[derive(Debug, Deserialize, Serialize)] 34 | pub struct Config { 35 | pub ip: IpAddr, 36 | pub port: u16, 37 | pub openapi: OpenApiConfig, 38 | } 39 | 40 | #[derive(Debug, Deserialize, Serialize)] 41 | pub struct OpenApiConfig { 42 | pub contact: OpenApiContact, 43 | pub external_document: OpenApiExternalDocument, 44 | pub servers: IndexMap, 45 | } 46 | 47 | #[derive(Debug, Deserialize, Serialize)] 48 | pub struct OpenApiContact { 49 | pub name: String, 50 | pub url: String, 51 | pub email: String, 52 | } 53 | 54 | #[derive(Debug, Deserialize, Serialize)] 55 | pub struct OpenApiExternalDocument { 56 | pub description: String, 57 | pub url: String, 58 | } 59 | 60 | #[derive(Debug, Deserialize, Serialize)] 61 | pub struct OpenApiServer { 62 | pub url: String, 63 | pub description: String, 64 | } 65 | -------------------------------------------------------------------------------- /httpbin-poem/src/request_inspection.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::anyhow; 4 | use poem::{ 5 | get, handler, 6 | http::HeaderMap, 7 | web::{Json, RealIp, TypedHeader}, 8 | Result, Route, 9 | }; 10 | use serde::Serialize; 11 | 12 | #[derive(Serialize)] 13 | struct Headers { 14 | /// The incoming request's HTTP headers 15 | headers: HashMap, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct Ip { 20 | /// The incoming request's IP address 21 | origin: String, 22 | } 23 | 24 | #[derive(Serialize)] 25 | struct UserAgent { 26 | /// The incoming request's User-Agent header 27 | user_agent: String, 28 | } 29 | 30 | pub fn api(route: Route) -> Route { 31 | route 32 | .at("/headers", get(headers)) 33 | .at("/ip", get(ip)) 34 | .at("/user-agent", get(user_agent)) 35 | } 36 | 37 | #[handler] 38 | fn headers(headers: &HeaderMap) -> Json { 39 | let headers = headers 40 | .iter() 41 | .map(|(k, v)| { 42 | ( 43 | k.to_string(), 44 | v.to_str() 45 | .map(|v| v.to_string()) 46 | .unwrap_or_else(|err| err.to_string()), 47 | ) 48 | }) 49 | .collect(); 50 | Json(Headers { headers }) 51 | } 52 | 53 | #[handler] 54 | fn ip(RealIp(origin): RealIp) -> Result> { 55 | origin 56 | .map(|origin| { 57 | Json(Ip { 58 | origin: origin.to_string(), 59 | }) 60 | }) 61 | .ok_or_else(|| { 62 | anyhow!("Could not determine the IP address through headers and socket address").into() 63 | }) 64 | } 65 | 66 | #[handler] 67 | fn user_agent( 68 | TypedHeader(user_agent): TypedHeader, 69 | ) -> Json { 70 | Json(UserAgent { 71 | user_agent: user_agent.to_string(), 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /httpbin-site/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "juice/templates/index.html" %} 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | {% endblock head %} 8 | 9 | 10 | {% block hero %} 11 | 12 |
13 |

14 | httpbin, but powered by Rust 15 |

16 |

17 | Httpbin-rs is a series of HTTP Request & Response Service pretty like 18 | httpbin but powered by Rust 19 |

20 |
21 | Star 29 | Issue 37 |
38 |
39 | logo 45 | 46 |
50 | Explore More ⇩ 51 |
52 | 67 | {% endblock hero %} 68 | 69 | 70 | {% block footer %} 71 | 76 | {% endblock footer %} 77 | -------------------------------------------------------------------------------- /httpbin-actix/src/request_inspection.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use actix_web::{ 4 | dev::ConnectionInfo, 5 | get, 6 | http::header::USER_AGENT, 7 | web::{Json, ServiceConfig}, 8 | Either, HttpRequest, HttpResponse, Responder, 9 | }; 10 | use serde::Serialize; 11 | 12 | #[derive(Serialize)] 13 | struct Headers { 14 | /// The incoming request's HTTP headers 15 | headers: HashMap, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct Ip { 20 | /// The incoming request's IP address 21 | origin: String, 22 | } 23 | 24 | #[derive(Serialize)] 25 | struct UserAgent { 26 | /// The incoming request's User-Agent header 27 | user_agent: String, 28 | } 29 | 30 | pub fn api(cfg: &mut ServiceConfig) { 31 | cfg.service(headers).service(ip).service(user_agent); 32 | } 33 | 34 | #[get("/headers")] 35 | async fn headers(req: HttpRequest) -> Json { 36 | let headers = req.headers(); 37 | 38 | let headers = headers 39 | .iter() 40 | .map(|(k, v)| { 41 | ( 42 | k.to_string(), 43 | v.to_str() 44 | .map(|v| v.to_string()) 45 | .unwrap_or_else(|err| err.to_string()), 46 | ) 47 | }) 48 | .collect(); 49 | Json(Headers { headers }) 50 | } 51 | 52 | #[get("/ip")] 53 | async fn ip(conn: ConnectionInfo) -> Either, impl Responder> { 54 | match conn.realip_remote_addr() { 55 | Some(origin) => Either::Left(Json(Ip { 56 | origin: origin.to_string(), 57 | })), 58 | None => Either::Right( 59 | HttpResponse::BadRequest() 60 | .body("Could not determine the IP address through headers and socket address"), 61 | ), 62 | } 63 | } 64 | 65 | #[get("/user-agent")] 66 | async fn user_agent(req: HttpRequest) -> Either, impl Responder> { 67 | match req.headers().get(USER_AGENT) { 68 | Some(user_agent) => match user_agent.to_str() { 69 | Ok(user_agent) => Either::Left(Json(UserAgent { 70 | user_agent: user_agent.to_string(), 71 | })), 72 | Err(err) => Either::Right( 73 | HttpResponse::BadRequest() 74 | .body(format!("Could not parse the User-Agent header: {}", err)), 75 | ), 76 | }, 77 | None => Either::Right( 78 | HttpResponse::BadRequest() 79 | .body("The incoming request does not have a User-Agent header"), 80 | ), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /httpbin-poem-openapi/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use httpbin::cli::Cli; 3 | use poem::{listener::TcpListener, middleware, EndpointExt, Route, Server}; 4 | use poem_openapi::{ContactObject, ExternalDocumentObject, OpenApiService, ServerObject}; 5 | 6 | mod data; 7 | mod http_method; 8 | mod request_inspection; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | let cfg = Cli::parse().load_config(); 13 | 14 | if std::env::var_os("RUST_LOG").is_none() { 15 | std::env::set_var("RUST_LOG", "poem=debug"); 16 | } 17 | tracing_subscriber::fmt::init(); 18 | 19 | let mut api_service = OpenApiService::new( 20 | (http_method::Api, request_inspection::Api, data::api()), 21 | "httpbin-rs", 22 | env!("CARGO_PKG_VERSION"), 23 | ) 24 | .description(env!("CARGO_PKG_DESCRIPTION")) 25 | .license(env!("CARGO_PKG_LICENSE")) 26 | .contact( 27 | ContactObject::new() 28 | .name(cfg.openapi.contact.name) 29 | .url(cfg.openapi.contact.url) 30 | .email(cfg.openapi.contact.email), 31 | ) 32 | .external_document( 33 | ExternalDocumentObject::new(cfg.openapi.external_document.url) 34 | .description(cfg.openapi.external_document.description), 35 | ); 36 | 37 | for server in cfg.openapi.servers.values() { 38 | let server = ServerObject::new(server.url.clone()).description(server.description.clone()); 39 | api_service = api_service.server(server); 40 | } 41 | 42 | let swagger = api_service.swagger_ui(); 43 | let rapidoc = api_service.rapidoc(); 44 | let redoc = api_service.redoc(); 45 | let openapi_explorer = api_service.openapi_explorer(); 46 | let spec_json = api_service.spec_endpoint(); 47 | let spec_yaml = api_service.spec_endpoint_yaml(); 48 | 49 | Ok(Server::new(TcpListener::bind((cfg.ip, cfg.port))) 50 | .run( 51 | Route::new() 52 | .nest("/", api_service) 53 | .nest("/swagger", swagger) 54 | .nest("/rapidoc", rapidoc) 55 | .nest("/redoc", redoc) 56 | .nest("/openapi-explorer", openapi_explorer) 57 | .nest("/spec/json", spec_json) 58 | .nest("/spec/yaml", spec_yaml) 59 | .with(middleware::Cors::new().allow_origins_fn(|_| true)) 60 | .with(middleware::NormalizePath::new( 61 | middleware::TrailingSlash::Trim, 62 | )) 63 | .with(middleware::Tracing), 64 | ) 65 | .await?) 66 | } 67 | -------------------------------------------------------------------------------- /httpbin-salvo/src/request_inspection.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use salvo::{hyper::header::USER_AGENT, prelude::*}; 4 | use serde::Serialize; 5 | 6 | #[derive(Serialize)] 7 | struct Headers { 8 | /// The incoming request's HTTP headers 9 | headers: HashMap, 10 | } 11 | 12 | #[derive(Serialize)] 13 | struct Ip { 14 | /// The incoming request's IP address 15 | origin: String, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct UserAgent { 20 | /// The incoming request's User-Agent header 21 | user_agent: String, 22 | } 23 | 24 | pub fn api() -> Router { 25 | Router::new() 26 | .push(Router::with_path("/headers").get(headers)) 27 | .push(Router::with_path("/ip").get(ip)) 28 | .push(Router::with_path("/user-agent").get(user_agent)) 29 | } 30 | 31 | #[handler] 32 | async fn headers(req: &Request) -> Json { 33 | let headers_map = req 34 | .headers() 35 | .iter() 36 | .map(|(k, v)| { 37 | ( 38 | k.to_string(), 39 | v.to_str() 40 | .map(|v| v.to_string()) 41 | .unwrap_or_else(|err| err.to_string()), 42 | ) 43 | }) 44 | .collect(); 45 | Json(Headers { 46 | headers: headers_map, 47 | }) 48 | } 49 | 50 | #[handler] 51 | async fn ip(req: &Request, res: &mut Response) { 52 | match req.remote_addr() { 53 | Some(origin) => res.render(Json(Ip { 54 | origin: origin.to_string(), 55 | })), 56 | None => { 57 | res.set_status_error(StatusError::bad_request().with_summary( 58 | "Could not determine the IP address through headers and socket address", 59 | )) 60 | } 61 | } 62 | } 63 | 64 | #[handler] 65 | async fn user_agent(req: &Request, res: &mut Response) { 66 | match req.headers().get(USER_AGENT) { 67 | Some(user_agent_value) => match user_agent_value.to_str() { 68 | Ok(user_agent_value) => res.render(Json(UserAgent { 69 | user_agent: user_agent_value.to_string(), 70 | })), 71 | Err(err) => res.set_status_error( 72 | StatusError::bad_request() 73 | .with_summary("Could not parse the User-Agent header") 74 | .with_detail(err.to_string()), 75 | ), 76 | }, 77 | None => res.set_status_error( 78 | StatusError::bad_request() 79 | .with_summary("Could not determine the User-Agent through headers"), 80 | ), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Check then Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: cache rust 22 | uses: Swatinem/rust-cache@v2 23 | 24 | - name: check fmt 25 | run: cargo fmt --all --check 26 | - name: check clippy 27 | run: cargo clippy --no-deps -- -D warnings 28 | - name: check build 29 | run: cargo build 30 | - name: check test 31 | run: cargo test 32 | 33 | changelog: 34 | name: Changelog Check 35 | runs-on: ubuntu-latest 36 | if: github.event_name == 'pull_request' 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: Zomzog/changelog-checker@v1.3.0 41 | with: 42 | fileName: CHANGELOG.md 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | release: 47 | runs-on: ubuntu-latest 48 | needs: [test] 49 | strategy: 50 | matrix: 51 | impl: [poem-openapi, axum, actix, salvo, rocket, poem] 52 | 53 | steps: 54 | - uses: actions/checkout@v4 55 | 56 | - name: Set up QEMU 57 | uses: docker/setup-qemu-action@v3 58 | - name: Set up Docker Buildx 59 | uses: docker/setup-buildx-action@v3 60 | - name: Login to DockerHub 61 | uses: docker/login-action@v3 62 | with: 63 | username: ${{ secrets.DOCKERHUB_USERNAME }} 64 | password: ${{ secrets.DOCKERHUB_TOKEN }} 65 | 66 | - name: Docker meta 67 | id: meta 68 | uses: docker/metadata-action@v5 69 | with: 70 | images: duskmoon/httpbin-rs 71 | flavor: | 72 | suffix=-${{ matrix.impl }},onlatest=true 73 | tags: | 74 | type=semver,pattern={{version}} 75 | type=semver,pattern={{major}} 76 | type=semver,pattern={{major}}.{{minor}} 77 | type=edge,branch=main 78 | 79 | - name: Build and push 80 | uses: docker/build-push-action@v5 81 | with: 82 | context: . 83 | build-args: | 84 | HTTPBIN_IMPL=httpbin-${{ matrix.impl }} 85 | push: ${{ github.event_name != 'pull_request' }} 86 | tags: ${{ steps.meta.outputs.tags }} 87 | labels: ${{ steps.meta.outputs.labels }} 88 | cache-from: type=registry,ref=duskmoon/httpbin-rs:${{ steps.meta.outputs.tags }} 89 | cache-to: type=inline 90 | -------------------------------------------------------------------------------- /httpbin-rocket/src/request_inspection.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, net::IpAddr, ops::Deref}; 2 | 3 | use rocket::{get, request::FromRequest, routes, serde::json::Json, Build, Request, Rocket}; 4 | use serde::Serialize; 5 | 6 | struct HeaderMap<'r>(&'r rocket::http::HeaderMap<'r>); 7 | 8 | #[rocket::async_trait] 9 | impl<'r> FromRequest<'r> for HeaderMap<'r> { 10 | type Error = std::convert::Infallible; 11 | 12 | async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { 13 | rocket::request::Outcome::Success(HeaderMap(request.headers())) 14 | } 15 | } 16 | 17 | impl<'r> Deref for HeaderMap<'r> { 18 | type Target = rocket::http::HeaderMap<'r>; 19 | 20 | fn deref(&self) -> &Self::Target { 21 | self.0 22 | } 23 | } 24 | 25 | struct UserAgentHeader<'r>(&'r str); 26 | 27 | #[rocket::async_trait] 28 | impl<'r> FromRequest<'r> for UserAgentHeader<'r> { 29 | type Error = anyhow::Error; 30 | 31 | async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { 32 | match request.headers().get_one("User-Agent") { 33 | Some(user_agent) => rocket::request::Outcome::Success(UserAgentHeader(user_agent)), 34 | None => rocket::request::Outcome::Error(( 35 | rocket::http::Status::BadRequest, 36 | anyhow::anyhow!("The incoming request does not have a User-Agent header"), 37 | )), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Serialize)] 43 | struct Headers { 44 | /// The incoming request's HTTP headers 45 | headers: HashMap, 46 | } 47 | 48 | #[derive(Serialize)] 49 | struct Ip { 50 | /// The incoming request's IP address 51 | origin: String, 52 | } 53 | 54 | #[derive(Serialize)] 55 | struct UserAgent { 56 | /// The incoming request's User-Agent header 57 | user_agent: String, 58 | } 59 | 60 | pub async fn api(rocket: Rocket) -> Rocket { 61 | rocket.mount("/", routes![headers, ip, user_agent]) 62 | } 63 | 64 | #[get("/headers")] 65 | fn headers(headers: HeaderMap) -> Json { 66 | let headers = headers 67 | .iter() 68 | .map(|header| (header.name().to_string(), header.value().to_string())) 69 | .collect(); 70 | Json(Headers { headers }) 71 | } 72 | 73 | #[get("/ip")] 74 | fn ip(origin: IpAddr) -> Json { 75 | Json(Ip { 76 | origin: origin.to_string(), 77 | }) 78 | } 79 | 80 | #[get("/user-agent")] 81 | fn user_agent(user_agent: UserAgentHeader) -> Json { 82 | Json(UserAgent { 83 | user_agent: user_agent.0.to_string(), 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /httpbin-poem/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use httpbin::data::base64::Base64Engine; 3 | use poem::{ 4 | handler, post, 5 | web::{Path, Query}, 6 | IntoResponse, Result, Route, 7 | }; 8 | use serde::Deserialize; 9 | 10 | #[derive(Deserialize)] 11 | struct Base64Config { 12 | alphabet: Option, 13 | pad: Option, 14 | } 15 | 16 | enum Base64Res { 17 | OkText(String), 18 | OkBinary(Vec, String), 19 | } 20 | 21 | impl IntoResponse for Base64Res { 22 | fn into_response(self) -> poem::Response { 23 | match self { 24 | Base64Res::OkText(s) => s.into_response(), 25 | Base64Res::OkBinary(b, content_type) => poem::Response::builder() 26 | .header("Content-Type", content_type) 27 | .body(b), 28 | } 29 | } 30 | } 31 | 32 | pub fn api(route: Route) -> Route { 33 | route.nest( 34 | "/base64", 35 | Route::new() 36 | .at("/encode/:engine", post(base64_encode)) 37 | .at("/decode/:engine", post(base64_decode)), 38 | ) 39 | } 40 | 41 | #[handler] 42 | fn base64_encode( 43 | data: Vec, 44 | Path(engine): Path, 45 | Query(Base64Config { alphabet, pad }): Query, 46 | ) -> Result { 47 | let config = match engine { 48 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 49 | alphabet: alphabet.unwrap_or_default(), 50 | pad: pad.unwrap_or_default(), 51 | }), 52 | _ => None, 53 | }; 54 | 55 | let encoded = httpbin::data::base64::encode(&data, engine, config).map_err(|e| anyhow!(e))?; 56 | 57 | Ok(Base64Res::OkText(encoded)) 58 | } 59 | 60 | #[handler] 61 | fn base64_decode( 62 | data: String, 63 | Path(engine): Path, 64 | Query(Base64Config { alphabet, pad }): Query, 65 | ) -> Result { 66 | let config = match engine { 67 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 68 | alphabet: alphabet.unwrap_or_default(), 69 | pad: pad.unwrap_or_default(), 70 | }), 71 | _ => None, 72 | }; 73 | 74 | let decoded = httpbin::data::base64::decode(&data, engine, config).map_err(|e| anyhow!(e))?; 75 | 76 | match String::from_utf8(decoded.clone()) { 77 | Ok(s) => Ok(Base64Res::OkText(s)), 78 | Err(_) => { 79 | let kind = infer::get(&decoded); 80 | 81 | Ok(Base64Res::OkBinary( 82 | decoded, 83 | kind.map(|k| k.mime_type()) 84 | .unwrap_or("application/octet-stream") 85 | .to_string(), 86 | )) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /httpbin-axum/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body, 3 | extract::{Path, Query}, 4 | http::StatusCode, 5 | response::{IntoResponse, Response}, 6 | routing::post, 7 | Router, 8 | }; 9 | use httpbin::data::base64::Base64Engine; 10 | use serde::Deserialize; 11 | 12 | #[derive(Deserialize)] 13 | struct Base64Config { 14 | pub alphabet: Option, 15 | pub pad: Option, 16 | } 17 | 18 | enum Base64Res { 19 | OkText(String), 20 | OkBinary { data: Vec, content_type: String }, 21 | } 22 | 23 | impl IntoResponse for Base64Res { 24 | fn into_response(self) -> Response { 25 | match self { 26 | Base64Res::OkText(text) => (StatusCode::OK, text).into_response(), 27 | Base64Res::OkBinary { data, content_type } => { 28 | (StatusCode::OK, [("Content-Type", content_type)], data).into_response() 29 | } 30 | } 31 | } 32 | } 33 | 34 | pub fn api() -> Router { 35 | Router::new() 36 | .route("/encode/:engine", post(base64_encode)) 37 | .route("/decode/:engine", post(base64_decode)) 38 | } 39 | 40 | async fn base64_encode( 41 | Path(engine): Path, 42 | Query(config): Query, 43 | data: body::Bytes, 44 | ) -> Result { 45 | let config = match engine { 46 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 47 | alphabet: config.alphabet.unwrap_or_default(), 48 | pad: config.pad.unwrap_or_default(), 49 | }), 50 | _ => None, 51 | }; 52 | 53 | let encoded = 54 | httpbin::data::base64::encode(&data, engine, config).map_err(|e| e.to_string())?; 55 | 56 | Ok(Base64Res::OkText(encoded)) 57 | } 58 | 59 | async fn base64_decode( 60 | Path(engine): Path, 61 | Query(config): Query, 62 | data: String, 63 | ) -> Result { 64 | let config = match engine { 65 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 66 | alphabet: config.alphabet.unwrap_or_default(), 67 | pad: config.pad.unwrap_or_default(), 68 | }), 69 | _ => None, 70 | }; 71 | 72 | let decoded = 73 | httpbin::data::base64::decode(&data, engine, config).map_err(|e| e.to_string())?; 74 | 75 | match String::from_utf8(decoded.clone()) { 76 | Ok(decoded) => Ok(Base64Res::OkText(decoded)), 77 | Err(_) => { 78 | let kind = infer::get(&decoded); 79 | 80 | Ok(Base64Res::OkBinary { 81 | data: decoded, 82 | content_type: kind 83 | .map(|k| k.mime_type()) 84 | .unwrap_or("application/octet-stream") 85 | .to_string(), 86 | }) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /httpbin-actix/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use actix_web::{ 4 | web::{post, resource, scope, Bytes, Path, Query, ServiceConfig}, 5 | HttpResponse, ResponseError, Result, 6 | }; 7 | use httpbin::data::base64::Base64Engine; 8 | use serde::Deserialize; 9 | 10 | #[derive(Deserialize)] 11 | struct Base64Config { 12 | pub alphabet: Option, 13 | pub pad: Option, 14 | } 15 | 16 | #[derive(Debug)] 17 | struct Base64Error(pub httpbin::data::base64::Base64Error); 18 | 19 | impl Display for Base64Error { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | self.0.fmt(f) 22 | } 23 | } 24 | 25 | impl ResponseError for Base64Error { 26 | fn error_response(&self) -> HttpResponse { 27 | HttpResponse::BadRequest().body(self.0.to_string()) 28 | } 29 | } 30 | 31 | pub fn api(cfg: &mut ServiceConfig) { 32 | cfg.service( 33 | scope("/base64") 34 | .service(resource("/encode/{engine}").route(post().to(base64_encode))) 35 | .service(resource("/decode/{engine}").route(post().to(base64_decode))), 36 | ); 37 | } 38 | 39 | async fn base64_encode( 40 | data: Bytes, 41 | engine: Path, 42 | Query(config): Query, 43 | ) -> Result { 44 | let engine = engine.into_inner(); 45 | 46 | let config = match engine { 47 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 48 | alphabet: config.alphabet.unwrap_or_default(), 49 | pad: config.pad.unwrap_or_default(), 50 | }), 51 | _ => None, 52 | }; 53 | 54 | let encoded = httpbin::data::base64::encode(&data, engine, config).map_err(Base64Error)?; 55 | 56 | Ok(HttpResponse::Ok().body(encoded)) 57 | } 58 | 59 | async fn base64_decode( 60 | data: String, 61 | engine: Path, 62 | Query(config): Query, 63 | ) -> Result { 64 | let engine = engine.into_inner(); 65 | 66 | let config = match engine { 67 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 68 | alphabet: config.alphabet.unwrap_or_default(), 69 | pad: config.pad.unwrap_or_default(), 70 | }), 71 | _ => None, 72 | }; 73 | 74 | let decoded = httpbin::data::base64::decode(&data, engine, config).map_err(Base64Error)?; 75 | 76 | match String::from_utf8(decoded.clone()) { 77 | Ok(text) => Ok(HttpResponse::Ok().body(text)), 78 | Err(_) => { 79 | let kind = infer::get(&decoded); 80 | 81 | Ok(HttpResponse::Ok() 82 | .content_type( 83 | kind.map(|k| k.mime_type()) 84 | .unwrap_or("application/octet-stream"), 85 | ) 86 | .body(decoded)) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /httpbin-poem/src/http_method.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use poem::{ 4 | delete, get, handler, 5 | http::{HeaderMap, Method, Uri}, 6 | patch, post, put, 7 | web::{ 8 | headers::{ContentType, HeaderMapExt}, 9 | Json, RealIp, 10 | }, 11 | Route, 12 | }; 13 | use serde::Serialize; 14 | 15 | #[derive(Serialize)] 16 | struct Http { 17 | method: String, 18 | uri: String, 19 | headers: HashMap, 20 | origin: Option, 21 | query: Option>, 22 | body_string: String, 23 | json: Option, 24 | } 25 | 26 | pub fn api(route: Route) -> Route { 27 | route 28 | .at("/get", get(anything)) 29 | .at("/post", post(anything)) 30 | .at("/put", put(anything)) 31 | .at("/delete", delete(anything)) 32 | .at("/patch", patch(anything)) 33 | .at( 34 | "/anything", 35 | get(anything) 36 | .post(anything) 37 | .put(anything) 38 | .delete(anything) 39 | .patch(anything), 40 | ) 41 | .at( 42 | "/anything/*anything", 43 | get(anything) 44 | .post(anything) 45 | .put(anything) 46 | .delete(anything) 47 | .patch(anything), 48 | ) 49 | } 50 | 51 | #[handler] 52 | fn anything( 53 | method: Method, 54 | uri: &Uri, 55 | header_map: &HeaderMap, 56 | origin: RealIp, 57 | body: Vec, 58 | ) -> Json { 59 | let headers = header_map 60 | .iter() 61 | .map(|(k, v)| { 62 | ( 63 | k.to_string(), 64 | v.to_str() 65 | .map(|v| v.to_string()) 66 | .unwrap_or_else(|err| err.to_string()), 67 | ) 68 | }) 69 | .collect(); 70 | 71 | let query = uri.query().map(|query_str| { 72 | serde_qs::from_str(query_str) 73 | .unwrap_or_else(|err| [("error".to_string(), err.to_string())].into()) 74 | }); 75 | 76 | let body_string = match String::from_utf8(body.clone()) { 77 | Ok(body) => body, 78 | Err(_) => { 79 | match httpbin::data::base64::encode( 80 | &body, 81 | httpbin::data::base64::Base64Engine::Standard, 82 | None, 83 | ) { 84 | Ok(body) => body, 85 | Err(err) => err.to_string(), 86 | } 87 | } 88 | }; 89 | 90 | let json = header_map 91 | .typed_get::() 92 | .and_then(|content_type| { 93 | if content_type == ContentType::json() { 94 | Some(serde_json::from_slice(&body).unwrap_or_else(|err| { 95 | serde_json::json!({ 96 | "error": err.to_string(), 97 | }) 98 | })) 99 | } else { 100 | None 101 | } 102 | }); 103 | 104 | Json(Http { 105 | method: method.to_string(), 106 | uri: uri.to_string(), 107 | headers, 108 | origin: origin.0.map(|origin| origin.to_string()), 109 | query, 110 | body_string, 111 | json, 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /httpbin-axum/src/http_method.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use axum::{ 4 | body::Bytes, 5 | extract::RawQuery, 6 | http::{HeaderMap, Method, Uri}, 7 | routing::{delete, get, patch, post, put}, 8 | Json, Router, 9 | }; 10 | use axum_client_ip::InsecureClientIp; 11 | use axum_extra::{headers::ContentType, TypedHeader}; 12 | use serde::Serialize; 13 | 14 | #[derive(Serialize)] 15 | struct Http { 16 | method: String, 17 | uri: String, 18 | headers: HashMap, 19 | origin: String, 20 | query: Option>, 21 | body_string: String, 22 | json: Option, 23 | } 24 | 25 | pub fn api() -> Router { 26 | Router::new() 27 | .route("/get", get(anything)) 28 | .route("/post", post(anything)) 29 | .route("/put", put(anything)) 30 | .route("/delete", delete(anything)) 31 | .route("/patch", patch(anything)) 32 | .route( 33 | "/anything", 34 | get(anything) 35 | .post(anything) 36 | .put(anything) 37 | .delete(anything) 38 | .patch(anything), 39 | ) 40 | .route( 41 | "/anything/*anything", 42 | get(anything) 43 | .post(anything) 44 | .put(anything) 45 | .delete(anything) 46 | .patch(anything), 47 | ) 48 | } 49 | 50 | async fn anything( 51 | method: Method, 52 | uri: Uri, 53 | query: RawQuery, 54 | header_map: HeaderMap, 55 | content_type: Option>, 56 | origin: InsecureClientIp, 57 | body: Bytes, 58 | ) -> Json { 59 | let headers = header_map 60 | .iter() 61 | .map(|(k, v)| { 62 | ( 63 | k.to_string(), 64 | v.to_str() 65 | .map(|v| v.to_string()) 66 | .unwrap_or_else(|err| err.to_string()), 67 | ) 68 | }) 69 | .collect(); 70 | 71 | let query = query.0.map(|query_str| { 72 | serde_qs::from_str(&query_str) 73 | .unwrap_or_else(|err| [("error".to_string(), err.to_string())].into()) 74 | }); 75 | 76 | let body_string = match String::from_utf8(body.to_vec()) { 77 | Ok(body) => body, 78 | Err(_) => { 79 | match httpbin::data::base64::encode( 80 | &body, 81 | httpbin::data::base64::Base64Engine::Standard, 82 | None, 83 | ) { 84 | Ok(body) => body, 85 | Err(err) => err.to_string(), 86 | } 87 | } 88 | }; 89 | 90 | let json = content_type.and_then(|TypedHeader(content_type)| { 91 | if content_type == ContentType::json() { 92 | Some(serde_json::from_slice(&body).unwrap_or_else(|err| { 93 | serde_json::json!({ 94 | "error": err.to_string(), 95 | }) 96 | })) 97 | } else { 98 | None 99 | } 100 | }); 101 | 102 | Json(Http { 103 | method: method.to_string(), 104 | uri: uri.to_string(), 105 | headers, 106 | origin: origin.0.to_string(), 107 | query, 108 | body_string, 109 | json, 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /httpbin-salvo/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use httpbin::data::base64::Base64Engine; 3 | use salvo::{ 4 | hyper::Body, 5 | hyper::{body::to_bytes, header::CONTENT_TYPE}, 6 | prelude::*, 7 | }; 8 | use serde::Deserialize; 9 | 10 | #[derive(Deserialize, Extractible, Debug)] 11 | struct Base64Req { 12 | #[extract(source(from = "param"))] 13 | pub engine: Base64Engine, 14 | 15 | #[extract(source(from = "query"))] 16 | pub alphabet: Option, 17 | 18 | #[extract(source(from = "query"))] 19 | pub pad: Option, 20 | } 21 | 22 | enum Base64Res { 23 | OkText(String), 24 | OkBinary { data: Vec, content_type: String }, 25 | } 26 | 27 | impl Piece for Base64Res { 28 | fn render(self, res: &mut Response) { 29 | match self { 30 | Base64Res::OkText(text) => { 31 | res.set_status_code(StatusCode::OK); 32 | res.render(text); 33 | } 34 | Base64Res::OkBinary { data, content_type } => { 35 | res.set_status_code(StatusCode::OK); 36 | // TODO: how to handle the error? 37 | let _ = res.add_header(CONTENT_TYPE, content_type, true); 38 | res.set_body(Body::from(data).into()); 39 | } 40 | } 41 | } 42 | } 43 | 44 | pub fn api() -> Router { 45 | Router::new() 46 | .push(Router::with_path("/encode/").post(base64_encode)) 47 | .push(Router::with_path("/decode/").post(base64_decode)) 48 | } 49 | 50 | #[handler] 51 | async fn base64_encode(req: &mut Request) -> Result { 52 | let data = req.payload().await?.clone(); 53 | let req = req.extract::().await?; 54 | 55 | let config = match req.engine { 56 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 57 | alphabet: req.alphabet.unwrap_or_default(), 58 | pad: req.pad.unwrap_or_default(), 59 | }), 60 | _ => None, 61 | }; 62 | 63 | let encoded = httpbin::data::base64::encode(&data, req.engine, config)?; 64 | 65 | Ok(Base64Res::OkText(encoded)) 66 | } 67 | 68 | #[handler] 69 | async fn base64_decode(req: &mut Request) -> Result { 70 | let data = match req.take_body() { 71 | Some(body) => { 72 | let bytes = to_bytes(body).await?; 73 | String::from_utf8(bytes.to_vec())? 74 | } 75 | None => "".to_string(), 76 | }; 77 | let req = req.extract::().await?; 78 | 79 | let config = match req.engine { 80 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 81 | alphabet: req.alphabet.unwrap_or_default(), 82 | pad: req.pad.unwrap_or_default(), 83 | }), 84 | _ => None, 85 | }; 86 | 87 | let decoded = httpbin::data::base64::decode(&data, req.engine, config)?; 88 | 89 | match String::from_utf8(decoded.clone()) { 90 | Ok(text) => Ok(Base64Res::OkText(text)), 91 | Err(_) => { 92 | let kind = infer::get(&decoded); 93 | 94 | Ok(Base64Res::OkBinary { 95 | data: decoded, 96 | content_type: kind 97 | .map(|k| k.mime_type()) 98 | .unwrap_or("application/octet-stream") 99 | .to_string(), 100 | }) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /httpbin-salvo/src/http_method.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use salvo::{http::mime::APPLICATION_JSON, prelude::*}; 4 | use serde::Serialize; 5 | 6 | #[derive(Serialize)] 7 | struct Http { 8 | method: String, 9 | uri: String, 10 | headers: HashMap, 11 | origin: Option, 12 | query: Option>, 13 | body_string: String, 14 | json: Option, 15 | } 16 | 17 | pub fn api() -> Router { 18 | Router::new() 19 | .push(Router::with_path("/get").get(anything)) 20 | .push(Router::with_path("/post").post(anything)) 21 | .push(Router::with_path("/put").put(anything)) 22 | .push(Router::with_path("/delete").delete(anything)) 23 | .push(Router::with_path("/patch").patch(anything)) 24 | .push( 25 | Router::with_path("/anything") 26 | .get(anything) 27 | .post(anything) 28 | .put(anything) 29 | .delete(anything) 30 | .patch(anything), 31 | ) 32 | .push( 33 | Router::with_path("/anything/<*anything>") 34 | .get(anything) 35 | .post(anything) 36 | .put(anything) 37 | .delete(anything) 38 | .patch(anything), 39 | ) 40 | } 41 | 42 | #[handler] 43 | async fn anything(req: &mut Request) -> Json { 44 | let headers = req 45 | .headers() 46 | .iter() 47 | .map(|(k, v)| { 48 | ( 49 | k.to_string(), 50 | v.to_str() 51 | .map(|v| v.to_string()) 52 | .unwrap_or_else(|err| err.to_string()), 53 | ) 54 | }) 55 | .collect(); 56 | 57 | let query = if req.queries().is_empty() { 58 | None 59 | } else { 60 | Some( 61 | req.queries() 62 | .iter() 63 | .map(|(k, v)| (k.to_string(), v.to_string())) 64 | .collect(), 65 | ) 66 | }; 67 | 68 | let body = req.payload().await; 69 | 70 | let (body_string, json) = match body { 71 | Ok(body) => { 72 | let body = body.clone(); 73 | let body_string = match String::from_utf8(body.clone()) { 74 | Ok(body) => body, 75 | Err(_) => match httpbin::data::base64::encode( 76 | &body, 77 | httpbin::data::base64::Base64Engine::Standard, 78 | None, 79 | ) { 80 | Ok(body) => body, 81 | Err(err) => err.to_string(), 82 | }, 83 | }; 84 | let json = if req.content_type() == Some(APPLICATION_JSON) { 85 | Some(serde_json::from_slice(&body).unwrap_or_else(|err| { 86 | serde_json::json!({ 87 | "error": err.to_string(), 88 | }) 89 | })) 90 | } else { 91 | None 92 | }; 93 | 94 | (body_string, json) 95 | } 96 | Err(err) => (err.to_string(), None), 97 | }; 98 | 99 | Json(Http { 100 | method: req.method().to_string(), 101 | uri: req.uri().to_string(), 102 | headers, 103 | origin: req.remote_addr().map(|addr| addr.to_string()), 104 | query, 105 | body_string, 106 | json, 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /httpbin-actix/src/http_method.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use actix_web::{ 4 | http::header::CONTENT_TYPE, 5 | web::{delete, get, patch, post, put, resource, scope, Bytes, Json, ServiceConfig}, 6 | HttpRequest, 7 | }; 8 | use serde::Serialize; 9 | 10 | #[derive(Serialize)] 11 | struct Http { 12 | method: String, 13 | uri: String, 14 | headers: HashMap, 15 | origin: String, 16 | query: Option>, 17 | body_string: String, 18 | json: Option, 19 | } 20 | 21 | pub fn api(cfg: &mut ServiceConfig) { 22 | cfg.service(resource("/get").route(get().to(anything))) 23 | .service(resource("/post").route(post().to(anything))) 24 | .service(resource("/put").route(put().to(anything))) 25 | .service(resource("/delete").route(delete().to(anything))) 26 | .service(resource("/patch").route(patch().to(anything))) 27 | .service( 28 | scope("/anything") 29 | .service( 30 | resource("") 31 | .route(get().to(anything)) 32 | .route(post().to(anything)) 33 | .route(put().to(anything)) 34 | .route(delete().to(anything)) 35 | .route(patch().to(anything)), 36 | ) 37 | .service( 38 | resource("/{anything}*") 39 | .route(get().to(anything)) 40 | .route(post().to(anything)) 41 | .route(put().to(anything)) 42 | .route(delete().to(anything)) 43 | .route(patch().to(anything)), 44 | ), 45 | ); 46 | } 47 | 48 | async fn anything(req: HttpRequest, data: Bytes) -> Json { 49 | let method = req.method().to_string(); 50 | 51 | let uri = req.uri().to_string(); 52 | 53 | let headers = req.headers(); 54 | let headers = headers 55 | .iter() 56 | .map(|(k, v)| { 57 | ( 58 | k.to_string(), 59 | v.to_str() 60 | .map(|v| v.to_string()) 61 | .unwrap_or_else(|err| err.to_string()), 62 | ) 63 | }) 64 | .collect(); 65 | 66 | let origin = req 67 | .connection_info() 68 | .realip_remote_addr() 69 | .unwrap_or("") 70 | .to_string(); 71 | 72 | let query = req.query_string(); 73 | let query = if query.is_empty() { 74 | None 75 | } else { 76 | Some( 77 | serde_qs::from_str(query) 78 | .unwrap_or_else(|err| [("error".to_string(), err.to_string())].into()), 79 | ) 80 | }; 81 | 82 | let body_string = match String::from_utf8(data.to_vec()) { 83 | Ok(body) => body, 84 | Err(_) => { 85 | match httpbin::data::base64::encode( 86 | &data, 87 | httpbin::data::base64::Base64Engine::Standard, 88 | None, 89 | ) { 90 | Ok(body) => body, 91 | Err(err) => err.to_string(), 92 | } 93 | } 94 | }; 95 | 96 | let json = req.headers().get(CONTENT_TYPE).and_then(|content_type| { 97 | if content_type == "application/json" { 98 | Some(serde_json::from_slice(&data).unwrap_or_else(|err| { 99 | serde_json::json!({ 100 | "error": err.to_string(), 101 | }) 102 | })) 103 | } else { 104 | None 105 | } 106 | }); 107 | 108 | Json(Http { 109 | method, 110 | uri, 111 | headers, 112 | origin, 113 | query, 114 | body_string, 115 | json, 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /httpbin-rocket/src/http_method.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rocket::{ 4 | data::ToByteUnit, 5 | http::{ 6 | ContentType, 7 | Method::{Delete, Get, Patch, Post, Put}, 8 | Status, 9 | }, 10 | route::{Handler, Outcome}, 11 | serde::json::Json, 12 | Build, Data, Request, Rocket, Route, 13 | }; 14 | use serde::Serialize; 15 | 16 | #[derive(Serialize)] 17 | struct Http { 18 | method: String, 19 | uri: String, 20 | headers: HashMap, 21 | origin: Option, 22 | query: Option>, 23 | body_string: String, 24 | json: Option, 25 | } 26 | 27 | pub async fn api(rocket: Rocket) -> Rocket { 28 | rocket.mount("/", Anything) 29 | } 30 | 31 | #[derive(Clone)] 32 | struct Anything; 33 | 34 | impl From for Vec { 35 | fn from(value: Anything) -> Vec { 36 | vec![ 37 | Route::new(Get, "/get", value.clone()), 38 | Route::new(Post, "/post", value.clone()), 39 | Route::new(Put, "/put", value.clone()), 40 | Route::new(Delete, "/delete", value.clone()), 41 | Route::new(Patch, "/patch", value.clone()), 42 | Route::new(Get, "/anything/", value.clone()), 43 | Route::new(Post, "/anything/", value.clone()), 44 | Route::new(Put, "/anything/", value.clone()), 45 | Route::new(Delete, "/anything/", value.clone()), 46 | Route::new(Patch, "/anything/", value), 47 | ] 48 | } 49 | } 50 | 51 | #[rocket::async_trait] 52 | impl Handler for Anything { 53 | async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> { 54 | let method = req.method().to_string(); 55 | 56 | let uri = req.uri().to_string(); 57 | 58 | let headers = req 59 | .headers() 60 | .iter() 61 | .map(|header| (header.name().to_string(), header.value().to_string())) 62 | .collect(); 63 | 64 | let origin = req.client_ip().map(|origin| origin.to_string()); 65 | 66 | let query = req.uri().query().map(|query_str| { 67 | serde_qs::from_str(query_str.as_str()) 68 | .unwrap_or_else(|err| [("error".to_string(), err.to_string())].into()) 69 | }); 70 | 71 | let body = match data.open(512.kibibytes()).into_bytes().await { 72 | // TODO: Handle incomplete body 73 | Ok(body) => body.into_inner(), 74 | Err(_) => return Outcome::error(Status::InternalServerError), 75 | }; 76 | 77 | let body_string = match String::from_utf8(body.clone()) { 78 | Ok(body) => body, 79 | Err(_) => { 80 | match httpbin::data::base64::encode( 81 | &body, 82 | httpbin::data::base64::Base64Engine::Standard, 83 | None, 84 | ) { 85 | Ok(body) => body, 86 | Err(err) => err.to_string(), 87 | } 88 | } 89 | }; 90 | 91 | let json = if req.content_type() == Some(&ContentType::JSON) { 92 | Some(serde_json::from_slice(&body).unwrap_or_else(|err| { 93 | serde_json::json!({ 94 | "error": err.to_string(), 95 | }) 96 | })) 97 | } else { 98 | None 99 | }; 100 | 101 | Outcome::from( 102 | req, 103 | Json(Http { 104 | method, 105 | uri, 106 | headers, 107 | origin, 108 | query, 109 | body_string, 110 | json, 111 | }), 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /httpbin-axum/src/data/uuid.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::Query, routing::get, Router}; 2 | use httpbin::data::uuid::{UuidBuffer, UuidFormat, UuidNamespace, UuidNodeId}; 3 | use serde::Deserialize; 4 | 5 | #[derive(Deserialize)] 6 | struct Format { 7 | format: Option, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | struct TimestampCounter { 12 | timestamp: Option, 13 | counter: Option, 14 | } 15 | 16 | #[derive(Deserialize)] 17 | struct NodeId { 18 | node_id: UuidNodeId, 19 | } 20 | 21 | #[derive(Deserialize)] 22 | struct NamespaceName { 23 | namespace: UuidNamespace, 24 | name: String, 25 | } 26 | 27 | #[derive(Deserialize)] 28 | struct Buf { 29 | buf: UuidBuffer, 30 | } 31 | 32 | pub fn api() -> Router { 33 | Router::new() 34 | .route("/v1", get(uuid_v1)) 35 | .route("/v3", get(uuid_v3)) 36 | .route("/v4", get(uuid_v4)) 37 | .route("/v5", get(uuid_v5)) 38 | .route("/v6", get(uuid_v6)) 39 | .route("/v7", get(uuid_v7)) 40 | .route("/v8", get(uuid_v8)) 41 | } 42 | 43 | async fn uuid_v1( 44 | Query(TimestampCounter { timestamp, counter }): Query, 45 | Query(NodeId { node_id }): Query, 46 | Query(Format { format }): Query, 47 | ) -> Result { 48 | let timestamp = timestamp.and_then(|timestamp| { 49 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 50 | }); 51 | let format = format.unwrap_or_default(); 52 | 53 | let uuid = 54 | httpbin::data::uuid::new_v1(timestamp, node_id, format).map_err(|e| e.to_string())?; 55 | 56 | Ok(uuid) 57 | } 58 | 59 | async fn uuid_v3( 60 | Query(NamespaceName { namespace, name }): Query, 61 | Query(Format { format }): Query, 62 | ) -> Result { 63 | let format = format.unwrap_or_default(); 64 | 65 | let uuid = httpbin::data::uuid::new_v3(namespace, &name, format).map_err(|e| e.to_string())?; 66 | 67 | Ok(uuid) 68 | } 69 | 70 | async fn uuid_v4(Query(Format { format }): Query) -> Result { 71 | let format = format.unwrap_or_default(); 72 | 73 | let uuid = httpbin::data::uuid::new_v4(format); 74 | 75 | Ok(uuid) 76 | } 77 | 78 | async fn uuid_v5( 79 | Query(NamespaceName { namespace, name }): Query, 80 | Query(Format { format }): Query, 81 | ) -> Result { 82 | let format = format.unwrap_or_default(); 83 | 84 | let uuid = httpbin::data::uuid::new_v5(namespace, &name, format).map_err(|e| e.to_string())?; 85 | 86 | Ok(uuid) 87 | } 88 | 89 | async fn uuid_v6( 90 | Query(TimestampCounter { timestamp, counter }): Query, 91 | Query(NodeId { node_id }): Query, 92 | Query(Format { format }): Query, 93 | ) -> Result { 94 | let timestamp = timestamp.and_then(|timestamp| { 95 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 96 | }); 97 | let format = format.unwrap_or_default(); 98 | 99 | let uuid = 100 | httpbin::data::uuid::new_v6(timestamp, node_id, format).map_err(|e| e.to_string())?; 101 | 102 | Ok(uuid) 103 | } 104 | 105 | async fn uuid_v7( 106 | Query(TimestampCounter { timestamp, counter }): Query, 107 | Query(Format { format }): Query, 108 | ) -> Result { 109 | let timestamp = timestamp.and_then(|timestamp| { 110 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 111 | }); 112 | let format = format.unwrap_or_default(); 113 | 114 | let uuid = httpbin::data::uuid::new_v7(timestamp, format); 115 | 116 | Ok(uuid) 117 | } 118 | 119 | async fn uuid_v8( 120 | Query(Buf { buf }): Query, 121 | Query(Format { format }): Query, 122 | ) -> Result { 123 | let format = format.unwrap_or_default(); 124 | 125 | let uuid = httpbin::data::uuid::new_v8(buf, format).map_err(|e| e.to_string())?; 126 | 127 | Ok(uuid) 128 | } 129 | -------------------------------------------------------------------------------- /httpbin-poem/src/data/uuid.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use httpbin::data::uuid::{UuidBuffer, UuidFormat, UuidNamespace, UuidNodeId}; 3 | use poem::{get, handler, web::Query, Result, Route}; 4 | use serde::Deserialize; 5 | 6 | #[derive(Deserialize)] 7 | struct Format { 8 | format: Option, 9 | } 10 | 11 | #[derive(Deserialize)] 12 | struct TimestampCounter { 13 | timestamp: Option, 14 | counter: Option, 15 | } 16 | 17 | #[derive(Deserialize)] 18 | struct NodeId { 19 | node_id: UuidNodeId, 20 | } 21 | 22 | #[derive(Deserialize)] 23 | struct NamespaceName { 24 | namespace: UuidNamespace, 25 | name: String, 26 | } 27 | 28 | #[derive(Deserialize)] 29 | struct Buf { 30 | buf: UuidBuffer, 31 | } 32 | 33 | pub fn api(route: Route) -> Route { 34 | route.nest( 35 | "/uuid", 36 | Route::new() 37 | .at("/v1", get(uuid_v1)) 38 | .at("/v3", get(uuid_v3)) 39 | .at("/v4", get(uuid_v4)) 40 | .at("/v5", get(uuid_v5)) 41 | .at("/v6", get(uuid_v6)) 42 | .at("/v7", get(uuid_v7)) 43 | .at("/v8", get(uuid_v8)), 44 | ) 45 | } 46 | 47 | #[handler] 48 | fn uuid_v1( 49 | Query(TimestampCounter { timestamp, counter }): Query, 50 | Query(NodeId { node_id }): Query, 51 | Query(Format { format }): Query, 52 | ) -> Result { 53 | let timestamp = timestamp.and_then(|timestamp| { 54 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 55 | }); 56 | let format = format.unwrap_or_default(); 57 | 58 | let uuid = httpbin::data::uuid::new_v1(timestamp, node_id, format).map_err(|e| anyhow!(e))?; 59 | 60 | Ok(uuid) 61 | } 62 | 63 | #[handler] 64 | fn uuid_v3( 65 | Query(NamespaceName { namespace, name }): Query, 66 | Query(Format { format }): Query, 67 | ) -> Result { 68 | let format = format.unwrap_or_default(); 69 | 70 | let uuid = httpbin::data::uuid::new_v3(namespace, &name, format).map_err(|e| anyhow!(e))?; 71 | 72 | Ok(uuid) 73 | } 74 | 75 | #[handler] 76 | fn uuid_v4(Query(Format { format }): Query) -> Result { 77 | let format = format.unwrap_or_default(); 78 | 79 | let uuid = httpbin::data::uuid::new_v4(format); 80 | 81 | Ok(uuid) 82 | } 83 | 84 | #[handler] 85 | fn uuid_v5( 86 | Query(NamespaceName { namespace, name }): Query, 87 | Query(Format { format }): Query, 88 | ) -> Result { 89 | let format = format.unwrap_or_default(); 90 | 91 | let uuid = httpbin::data::uuid::new_v5(namespace, &name, format).map_err(|e| anyhow!(e))?; 92 | 93 | Ok(uuid) 94 | } 95 | 96 | #[handler] 97 | fn uuid_v6( 98 | Query(TimestampCounter { timestamp, counter }): Query, 99 | Query(NodeId { node_id }): Query, 100 | Query(Format { format }): Query, 101 | ) -> Result { 102 | let timestamp = timestamp.and_then(|timestamp| { 103 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 104 | }); 105 | let format = format.unwrap_or_default(); 106 | 107 | let uuid = httpbin::data::uuid::new_v6(timestamp, node_id, format).map_err(|e| anyhow!(e))?; 108 | 109 | Ok(uuid) 110 | } 111 | 112 | #[handler] 113 | fn uuid_v7( 114 | Query(TimestampCounter { timestamp, counter }): Query, 115 | Query(Format { format }): Query, 116 | ) -> Result { 117 | let timestamp = timestamp.and_then(|timestamp| { 118 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 119 | }); 120 | let format = format.unwrap_or_default(); 121 | 122 | let uuid = httpbin::data::uuid::new_v7(timestamp, format); 123 | 124 | Ok(uuid) 125 | } 126 | 127 | #[handler] 128 | fn uuid_v8( 129 | Query(Buf { buf }): Query, 130 | Query(Format { format }): Query, 131 | ) -> Result { 132 | let format = format.unwrap_or_default(); 133 | 134 | let uuid = httpbin::data::uuid::new_v8(buf, format).map_err(|e| anyhow!(e))?; 135 | 136 | Ok(uuid) 137 | } 138 | -------------------------------------------------------------------------------- /httpbin-poem-openapi/src/request_inspection.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use poem::{ 4 | http::HeaderMap, 5 | web::{ 6 | headers::{self, HeaderMapExt}, 7 | RealIp, 8 | }, 9 | }; 10 | use poem_openapi::{ 11 | payload::{Json, PlainText}, 12 | types::Example, 13 | ApiResponse, Object, OpenApi, Tags, 14 | }; 15 | 16 | #[derive(Tags)] 17 | enum ReqInspTag { 18 | /// Inspect the request data 19 | #[oai(rename = "Request Inspection")] 20 | RequestInspection, 21 | } 22 | 23 | #[derive(Debug, Clone, Object)] 24 | #[oai(example)] 25 | struct Headers { 26 | /// The incoming request's HTTP headers 27 | headers: HashMap, 28 | } 29 | 30 | impl Example for Headers { 31 | fn example() -> Self { 32 | let mut headers = HashMap::new(); 33 | headers.insert("accept".to_string(), "*/*".to_string()); 34 | headers.insert("host".to_string(), "httpbin.rs".to_string()); 35 | headers.insert("user-agent".to_string(), "curl/7.86.0".to_string()); 36 | Self { headers } 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone, Object)] 41 | #[oai(example)] 42 | struct Ip { 43 | /// The incoming request's IP address 44 | origin: String, 45 | } 46 | 47 | impl Example for Ip { 48 | fn example() -> Self { 49 | Self { 50 | origin: "1.2.3.4".to_string(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(ApiResponse)] 56 | enum IpRes { 57 | /// The incoming request's IP address 58 | #[oai(status = 200)] 59 | Ok(Json), 60 | 61 | /// Could not determine the IP address through headers and socket address 62 | #[oai(status = 400)] 63 | BadRequest(PlainText), 64 | } 65 | 66 | #[derive(Debug, Clone, Object)] 67 | #[oai(example)] 68 | struct UserAgent { 69 | /// The incoming request's User-Agent header 70 | user_agent: String, 71 | } 72 | 73 | impl Example for UserAgent { 74 | fn example() -> Self { 75 | Self { 76 | user_agent: "curl/7.86.0".to_string(), 77 | } 78 | } 79 | } 80 | 81 | #[derive(ApiResponse)] 82 | enum UserAgentRes { 83 | /// The incoming request's User-Agent header 84 | #[oai(status = 200)] 85 | Ok(Json), 86 | 87 | /// The incoming request does not have a User-Agent header 88 | #[oai(status = 400)] 89 | BadRequest(PlainText), 90 | } 91 | 92 | pub struct Api; 93 | 94 | #[OpenApi(tag = "ReqInspTag::RequestInspection")] 95 | impl Api { 96 | /// Return the incoming request's HTTP headers. 97 | #[oai(path = "/headers", method = "get")] 98 | async fn headers(&self, headers: &HeaderMap) -> Json { 99 | let headers = headers 100 | .iter() 101 | .map(|(k, v)| { 102 | ( 103 | k.to_string(), 104 | v.to_str() 105 | .map(|v| v.to_string()) 106 | .unwrap_or_else(|err| err.to_string()), 107 | ) 108 | }) 109 | .collect(); 110 | Json(Headers { headers }) 111 | } 112 | 113 | /// Return the incoming request's IP address. 114 | #[oai(path = "/ip", method = "get")] 115 | async fn ip(&self, origin: RealIp) -> IpRes { 116 | match origin.0 { 117 | Some(origin) => IpRes::Ok(Json(Ip { 118 | origin: origin.to_string(), 119 | })), 120 | None => IpRes::BadRequest(PlainText( 121 | "Could not determine the IP address through headers and socket address".to_string(), 122 | )), 123 | } 124 | } 125 | 126 | /// Return the incoming request's User-Agent header. 127 | #[oai(path = "/user-agent", method = "get")] 128 | async fn user_agent(&self, headers: &HeaderMap) -> UserAgentRes { 129 | match headers.typed_get::() { 130 | Some(ua) => UserAgentRes::Ok(Json(UserAgent { 131 | user_agent: ua.to_string(), 132 | })), 133 | None => UserAgentRes::BadRequest(PlainText( 134 | "The incoming request does not have a User-Agent header".to_string(), 135 | )), 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /httpbin-rocket/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use rocket::{ 4 | http::{ContentType, Header}, 5 | post, 6 | request::FromParam, 7 | routes, Build, FromForm, Responder, Rocket, 8 | }; 9 | 10 | struct Base64Engine(httpbin::data::base64::Base64Engine); 11 | 12 | impl<'r> FromParam<'r> for Base64Engine { 13 | type Error = &'r str; 14 | 15 | fn from_param(param: &'r str) -> Result { 16 | match param { 17 | "standard" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::Standard)), 18 | "standard_no_pad" => Ok(Base64Engine( 19 | httpbin::data::base64::Base64Engine::StandardNoPad, 20 | )), 21 | "url_safe" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::UrlSafe)), 22 | "url_safe_no_pad" => Ok(Base64Engine( 23 | httpbin::data::base64::Base64Engine::UrlSafeNoPad, 24 | )), 25 | "bcrypt" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::Bcrypt)), 26 | "bin_hex" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::BinHex)), 27 | "crypt" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::Crypt)), 28 | "imap_mutf7" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::ImapMutf7)), 29 | "custom" => Ok(Base64Engine(httpbin::data::base64::Base64Engine::Custom)), 30 | _ => Err(param), 31 | } 32 | } 33 | } 34 | 35 | #[derive(FromForm)] 36 | struct Base64Config { 37 | pub alphabet: Option, 38 | pub pad: Option, 39 | } 40 | 41 | struct ContentTypeHeader(Box); 42 | 43 | impl<'r> From for Header<'r> { 44 | fn from(content_type: ContentTypeHeader) -> Self { 45 | Header::new("Content-Type", content_type.0.to_string()) 46 | } 47 | } 48 | 49 | #[derive(Responder)] 50 | enum Base64Res { 51 | OkText(String), 52 | OkBinary(Vec, ContentTypeHeader), 53 | } 54 | 55 | pub async fn api(rocket: Rocket) -> Rocket { 56 | rocket.mount("/base64", routes![base64_encode, base64_decode]) 57 | } 58 | 59 | #[post("/encode/?", data = "")] 60 | fn base64_encode( 61 | engine: Base64Engine, 62 | config: Base64Config, 63 | data: Vec, 64 | ) -> Result { 65 | let config = match engine { 66 | Base64Engine(httpbin::data::base64::Base64Engine::Custom) => { 67 | Some(httpbin::data::base64::Base64Config { 68 | alphabet: config.alphabet.unwrap_or_default(), 69 | pad: config.pad.unwrap_or_default(), 70 | }) 71 | } 72 | _ => None, 73 | }; 74 | 75 | let encoded = 76 | httpbin::data::base64::encode(&data, engine.0, config).map_err(|e| e.to_string())?; 77 | 78 | Ok(Base64Res::OkText(encoded)) 79 | } 80 | 81 | #[post("/decode/?", data = "")] 82 | fn base64_decode( 83 | engine: Base64Engine, 84 | config: Base64Config, 85 | data: String, 86 | ) -> Result { 87 | let config = match engine { 88 | Base64Engine(httpbin::data::base64::Base64Engine::Custom) => { 89 | Some(httpbin::data::base64::Base64Config { 90 | alphabet: config.alphabet.unwrap_or_default(), 91 | pad: config.pad.unwrap_or_default(), 92 | }) 93 | } 94 | _ => None, 95 | }; 96 | 97 | let decoded = 98 | httpbin::data::base64::decode(&data, engine.0, config).map_err(|e| e.to_string())?; 99 | 100 | match String::from_utf8(decoded.clone()) { 101 | Ok(decoded) => Ok(Base64Res::OkText(decoded)), 102 | Err(_) => { 103 | let kind = infer::get(&decoded); 104 | 105 | Ok(Base64Res::OkBinary( 106 | decoded, 107 | ContentTypeHeader(Box::new( 108 | ContentType::from_str( 109 | kind.map(|k| k.mime_type()) 110 | .unwrap_or("application/octet-stream"), 111 | ) 112 | .unwrap_or(ContentType::Binary), 113 | )), 114 | )) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /httpbin/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use base64::alphabet; 2 | use base64::engine::general_purpose::{self, STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD}; 3 | use base64::engine::GeneralPurpose; 4 | use base64::Engine; 5 | use serde::Deserialize; 6 | use thiserror::Error; 7 | 8 | #[derive(Deserialize, Debug)] 9 | #[serde(rename_all = "lowercase")] 10 | pub enum Base64Engine { 11 | Standard, 12 | StandardNoPad, 13 | UrlSafe, 14 | UrlSafeNoPad, 15 | Bcrypt, 16 | BinHex, 17 | Crypt, 18 | ImapMutf7, 19 | Custom, 20 | } 21 | 22 | pub struct Base64Config { 23 | pub alphabet: String, 24 | pub pad: bool, 25 | } 26 | 27 | #[derive(Error, Debug)] 28 | pub enum Base64Error { 29 | #[error("unknown engine: {0}")] 30 | UnknownEngine(String), 31 | #[error("the config is empty")] 32 | EmptyConfig, 33 | #[error("the alphabet is invalid: {0}")] 34 | InvalidAlphabet(#[from] alphabet::ParseAlphabetError), 35 | #[error("the base64 string is invalid: {0}")] 36 | InvalidBase64(#[from] base64::DecodeError), 37 | } 38 | 39 | /// Encode a `&[u8]` to a base64 string 40 | pub fn encode( 41 | data: &[u8], 42 | engine: Base64Engine, 43 | config: Option, 44 | ) -> Result { 45 | let encoded = match engine { 46 | Base64Engine::Standard => STANDARD.encode(data), 47 | Base64Engine::StandardNoPad => STANDARD_NO_PAD.encode(data), 48 | Base64Engine::UrlSafe => URL_SAFE.encode(data), 49 | Base64Engine::UrlSafeNoPad => URL_SAFE_NO_PAD.encode(data), 50 | Base64Engine::Bcrypt => BCRYPT.encode(data), 51 | Base64Engine::BinHex => BIN_HEX.encode(data), 52 | Base64Engine::Crypt => CRYPT.encode(data), 53 | Base64Engine::ImapMutf7 => IMAP_MUTF7.encode(data), 54 | Base64Engine::Custom => { 55 | if config.is_none() { 56 | return Err(Base64Error::EmptyConfig); 57 | } 58 | 59 | let config = config.unwrap(); 60 | 61 | let alphabet = alphabet::Alphabet::new(&config.alphabet)?; 62 | 63 | let engine = GeneralPurpose::new( 64 | &alphabet, 65 | if config.pad { 66 | general_purpose::PAD 67 | } else { 68 | general_purpose::NO_PAD 69 | }, 70 | ); 71 | 72 | engine.encode(data) 73 | } 74 | }; 75 | 76 | Ok(encoded) 77 | } 78 | 79 | /// Decode a base64 string to a `Vec` 80 | pub fn decode( 81 | data: &str, 82 | engine: Base64Engine, 83 | config: Option, 84 | ) -> Result, Base64Error> { 85 | let decoded = match engine { 86 | Base64Engine::Standard => STANDARD.decode(data)?, 87 | Base64Engine::StandardNoPad => STANDARD_NO_PAD.decode(data)?, 88 | Base64Engine::UrlSafe => URL_SAFE.decode(data)?, 89 | Base64Engine::UrlSafeNoPad => URL_SAFE_NO_PAD.decode(data)?, 90 | Base64Engine::Bcrypt => BCRYPT.decode(data)?, 91 | Base64Engine::BinHex => BIN_HEX.decode(data)?, 92 | Base64Engine::Crypt => CRYPT.decode(data)?, 93 | Base64Engine::ImapMutf7 => IMAP_MUTF7.decode(data)?, 94 | Base64Engine::Custom => { 95 | if config.is_none() { 96 | return Err(Base64Error::EmptyConfig); 97 | } 98 | 99 | let config = config.unwrap(); 100 | 101 | let alphabet = alphabet::Alphabet::new(&config.alphabet)?; 102 | 103 | let engine = GeneralPurpose::new( 104 | &alphabet, 105 | if config.pad { 106 | general_purpose::PAD 107 | } else { 108 | general_purpose::NO_PAD 109 | }, 110 | ); 111 | 112 | engine.decode(data)? 113 | } 114 | }; 115 | 116 | Ok(decoded) 117 | } 118 | 119 | const BCRYPT: GeneralPurpose = GeneralPurpose::new(&alphabet::BCRYPT, general_purpose::NO_PAD); 120 | const BIN_HEX: GeneralPurpose = GeneralPurpose::new(&alphabet::BIN_HEX, general_purpose::NO_PAD); 121 | const CRYPT: GeneralPurpose = GeneralPurpose::new(&alphabet::CRYPT, general_purpose::NO_PAD); 122 | const IMAP_MUTF7: GeneralPurpose = 123 | GeneralPurpose::new(&alphabet::IMAP_MUTF7, general_purpose::NO_PAD); 124 | -------------------------------------------------------------------------------- /httpbin-actix/src/data/uuid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use actix_web::{ 4 | web::{get, resource, scope, Query, ServiceConfig}, 5 | HttpResponse, ResponseError, Result, 6 | }; 7 | use httpbin::data::uuid::{UuidBuffer, UuidFormat, UuidNamespace, UuidNodeId}; 8 | use serde::Deserialize; 9 | 10 | #[derive(Deserialize)] 11 | struct Format { 12 | format: Option, 13 | } 14 | 15 | #[derive(Deserialize)] 16 | struct TimestampCounter { 17 | timestamp: Option, 18 | counter: Option, 19 | } 20 | 21 | #[derive(Deserialize)] 22 | struct NodeId { 23 | node_id: UuidNodeId, 24 | } 25 | 26 | #[derive(Deserialize)] 27 | struct NamespaceName { 28 | namespace: UuidNamespace, 29 | name: String, 30 | } 31 | 32 | #[derive(Deserialize)] 33 | struct Buf { 34 | buf: UuidBuffer, 35 | } 36 | 37 | #[derive(Debug)] 38 | struct UuidError(pub httpbin::data::uuid::UuidError); 39 | 40 | impl Display for UuidError { 41 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | self.0.fmt(f) 43 | } 44 | } 45 | 46 | impl ResponseError for UuidError { 47 | fn error_response(&self) -> actix_web::HttpResponse { 48 | HttpResponse::BadRequest().body(self.0.to_string()) 49 | } 50 | } 51 | 52 | pub fn api(cfg: &mut ServiceConfig) { 53 | cfg.service( 54 | scope("/uuid") 55 | .service(resource("/v1").route(get().to(uuid_v1))) 56 | .service(resource("/v3").route(get().to(uuid_v3))) 57 | .service(resource("/v4").route(get().to(uuid_v4))) 58 | .service(resource("/v5").route(get().to(uuid_v5))) 59 | .service(resource("/v6").route(get().to(uuid_v6))) 60 | .service(resource("/v7").route(get().to(uuid_v7))) 61 | .service(resource("/v8").route(get().to(uuid_v8))), 62 | ); 63 | } 64 | 65 | async fn uuid_v1( 66 | Query(TimestampCounter { timestamp, counter }): Query, 67 | Query(NodeId { node_id }): Query, 68 | Query(Format { format }): Query, 69 | ) -> Result { 70 | let timestamp = timestamp.and_then(|timestamp| { 71 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 72 | }); 73 | let format = format.unwrap_or_default(); 74 | 75 | let uuid = httpbin::data::uuid::new_v1(timestamp, node_id, format).map_err(UuidError)?; 76 | 77 | Ok(uuid) 78 | } 79 | 80 | async fn uuid_v3( 81 | Query(NamespaceName { namespace, name }): Query, 82 | Query(Format { format }): Query, 83 | ) -> Result { 84 | let format = format.unwrap_or_default(); 85 | 86 | let uuid = httpbin::data::uuid::new_v3(namespace, &name, format).map_err(UuidError)?; 87 | 88 | Ok(uuid) 89 | } 90 | 91 | async fn uuid_v4(Query(Format { format }): Query) -> Result { 92 | let format = format.unwrap_or_default(); 93 | 94 | let uuid = httpbin::data::uuid::new_v4(format); 95 | 96 | Ok(uuid) 97 | } 98 | 99 | async fn uuid_v5( 100 | Query(NamespaceName { namespace, name }): Query, 101 | Query(Format { format }): Query, 102 | ) -> Result { 103 | let format = format.unwrap_or_default(); 104 | 105 | let uuid = httpbin::data::uuid::new_v5(namespace, &name, format).map_err(UuidError)?; 106 | 107 | Ok(uuid) 108 | } 109 | 110 | async fn uuid_v6( 111 | Query(TimestampCounter { timestamp, counter }): Query, 112 | Query(NodeId { node_id }): Query, 113 | Query(Format { format }): Query, 114 | ) -> Result { 115 | let timestamp = timestamp.and_then(|timestamp| { 116 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 117 | }); 118 | let format = format.unwrap_or_default(); 119 | 120 | let uuid = httpbin::data::uuid::new_v6(timestamp, node_id, format).map_err(UuidError)?; 121 | 122 | Ok(uuid) 123 | } 124 | 125 | async fn uuid_v7( 126 | Query(TimestampCounter { timestamp, counter }): Query, 127 | Query(Format { format }): Query, 128 | ) -> Result { 129 | let timestamp = timestamp.and_then(|timestamp| { 130 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 131 | }); 132 | let format = format.unwrap_or_default(); 133 | 134 | let uuid = httpbin::data::uuid::new_v7(timestamp, format); 135 | 136 | Ok(uuid) 137 | } 138 | 139 | async fn uuid_v8( 140 | Query(Buf { buf }): Query, 141 | Query(Format { format }): Query, 142 | ) -> Result { 143 | let format = format.unwrap_or_default(); 144 | 145 | let uuid = httpbin::data::uuid::new_v8(buf, format).map_err(UuidError)?; 146 | 147 | Ok(uuid) 148 | } 149 | -------------------------------------------------------------------------------- /httpbin-poem-openapi/src/http_method.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use poem::{ 4 | http::{HeaderMap, Method, Uri}, 5 | web::{ 6 | headers::{ContentType, HeaderMapExt}, 7 | RealIp, 8 | }, 9 | }; 10 | use poem_openapi::{payload::Json, Object, OpenApi, Tags}; 11 | 12 | #[derive(Tags)] 13 | enum HttpMethodTag { 14 | /// Testing different HTTP verbs 15 | #[oai(rename = "HTTP Methods")] 16 | HttpMethod, 17 | 18 | /// Returns anything that is passed to request 19 | Anything, 20 | } 21 | 22 | #[derive(Object)] 23 | struct Http { 24 | method: String, 25 | uri: String, 26 | headers: HashMap, 27 | origin: Option, 28 | query: Option>, 29 | body_string: String, 30 | json: Option, 31 | } 32 | 33 | pub struct Api; 34 | 35 | #[OpenApi] 36 | impl Api { 37 | /// The request's GET parameters 38 | #[oai(path = "/get", method = "get", tag = "HttpMethodTag::HttpMethod")] 39 | async fn get( 40 | &self, 41 | method: Method, 42 | uri: &Uri, 43 | header_map: &HeaderMap, 44 | origin: RealIp, 45 | body: Vec, 46 | ) -> Json { 47 | self.anything(method, uri, header_map, origin, body).await 48 | } 49 | 50 | /// The request's POST parameters 51 | #[oai(path = "/post", method = "post", tag = "HttpMethodTag::HttpMethod")] 52 | async fn post( 53 | &self, 54 | method: Method, 55 | uri: &Uri, 56 | header_map: &HeaderMap, 57 | origin: RealIp, 58 | body: Vec, 59 | ) -> Json { 60 | self.anything(method, uri, header_map, origin, body).await 61 | } 62 | 63 | /// The request's PUT parameters 64 | #[oai(path = "/put", method = "put", tag = "HttpMethodTag::HttpMethod")] 65 | async fn put( 66 | &self, 67 | method: Method, 68 | uri: &Uri, 69 | header_map: &HeaderMap, 70 | origin: RealIp, 71 | body: Vec, 72 | ) -> Json { 73 | self.anything(method, uri, header_map, origin, body).await 74 | } 75 | 76 | /// The request's DELETE parameters 77 | #[oai(path = "/delete", method = "delete", tag = "HttpMethodTag::HttpMethod")] 78 | async fn delete( 79 | &self, 80 | method: Method, 81 | uri: &Uri, 82 | header_map: &HeaderMap, 83 | origin: RealIp, 84 | body: Vec, 85 | ) -> Json { 86 | self.anything(method, uri, header_map, origin, body).await 87 | } 88 | 89 | /// The request's PATCH parameters 90 | #[oai(path = "/patch", method = "patch", tag = "HttpMethodTag::HttpMethod")] 91 | async fn patch( 92 | &self, 93 | method: Method, 94 | uri: &Uri, 95 | header_map: &HeaderMap, 96 | origin: RealIp, 97 | body: Vec, 98 | ) -> Json { 99 | self.anything(method, uri, header_map, origin, body).await 100 | } 101 | 102 | /// Returns anything passed in request data. 103 | #[oai( 104 | path = "/anything", 105 | method = "get", 106 | method = "post", 107 | method = "put", 108 | method = "delete", 109 | method = "patch", 110 | tag = "HttpMethodTag::Anything" 111 | )] 112 | async fn anything_no_path( 113 | &self, 114 | method: Method, 115 | uri: &Uri, 116 | header_map: &HeaderMap, 117 | origin: RealIp, 118 | body: Vec, 119 | ) -> Json { 120 | self.anything(method, uri, header_map, origin, body).await 121 | } 122 | 123 | /// Returns anything passed in request data. 124 | #[oai( 125 | path = "/anything/*anything", 126 | method = "get", 127 | method = "post", 128 | method = "put", 129 | method = "delete", 130 | method = "patch", 131 | tag = "HttpMethodTag::Anything" 132 | )] 133 | async fn anything( 134 | &self, 135 | method: Method, 136 | uri: &Uri, 137 | header_map: &HeaderMap, 138 | origin: RealIp, 139 | body: Vec, 140 | ) -> Json { 141 | let headers = header_map 142 | .iter() 143 | .map(|(k, v)| { 144 | ( 145 | k.to_string(), 146 | v.to_str() 147 | .map(|v| v.to_string()) 148 | .unwrap_or_else(|err| err.to_string()), 149 | ) 150 | }) 151 | .collect(); 152 | 153 | let query = uri.query().map(|query_str| { 154 | serde_qs::from_str(query_str) 155 | .unwrap_or_else(|err| [("error".to_string(), err.to_string())].into()) 156 | }); 157 | 158 | let body_string = match String::from_utf8(body.clone()) { 159 | Ok(body) => body, 160 | Err(_) => { 161 | match httpbin::data::base64::encode( 162 | &body, 163 | httpbin::data::base64::Base64Engine::Standard, 164 | None, 165 | ) { 166 | Ok(body) => body, 167 | Err(err) => err.to_string(), 168 | } 169 | } 170 | }; 171 | 172 | let json = header_map 173 | .typed_get::() 174 | .and_then(|content_type| { 175 | if content_type == ContentType::json() { 176 | Some(serde_json::from_slice(&body).unwrap_or_else(|err| { 177 | serde_json::json!({ 178 | "error": err.to_string(), 179 | }) 180 | })) 181 | } else { 182 | None 183 | } 184 | }); 185 | 186 | Json(Http { 187 | method: method.to_string(), 188 | uri: uri.to_string(), 189 | headers, 190 | origin: origin.0.map(|origin| origin.to_string()), 191 | query, 192 | body_string, 193 | json, 194 | }) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /httpbin-poem-openapi/src/data/base64.rs: -------------------------------------------------------------------------------- 1 | use poem::Result; 2 | use poem_openapi::{ 3 | param::{Path, Query}, 4 | payload::Binary, 5 | payload::PlainText, 6 | ApiRequest, ApiResponse, Enum, OpenApi, 7 | }; 8 | 9 | use super::DataTag; 10 | 11 | /// Base64 engines for encoding and decoding 12 | /// 13 | /// `bcrypt` `binhex` `crypt` `imap-mutf7` are not using padding 14 | /// 15 | /// The `custom` engine allows you to specify your own alphabet and padding 16 | #[derive(Debug, Enum)] 17 | #[oai(rename_all = "snake_case")] 18 | enum Base64Engine { 19 | Standard, 20 | StandardNoPad, 21 | UrlSafe, 22 | UrlSafeNoPad, 23 | Bcrypt, 24 | BinHex, 25 | Crypt, 26 | ImapMutf7, 27 | Custom, 28 | } 29 | 30 | impl From for httpbin::data::base64::Base64Engine { 31 | fn from(engine: Base64Engine) -> Self { 32 | match engine { 33 | Base64Engine::Standard => httpbin::data::base64::Base64Engine::Standard, 34 | Base64Engine::StandardNoPad => httpbin::data::base64::Base64Engine::StandardNoPad, 35 | Base64Engine::UrlSafe => httpbin::data::base64::Base64Engine::UrlSafe, 36 | Base64Engine::UrlSafeNoPad => httpbin::data::base64::Base64Engine::UrlSafeNoPad, 37 | Base64Engine::Bcrypt => httpbin::data::base64::Base64Engine::Bcrypt, 38 | Base64Engine::BinHex => httpbin::data::base64::Base64Engine::BinHex, 39 | Base64Engine::Crypt => httpbin::data::base64::Base64Engine::Crypt, 40 | Base64Engine::ImapMutf7 => httpbin::data::base64::Base64Engine::ImapMutf7, 41 | Base64Engine::Custom => httpbin::data::base64::Base64Engine::Custom, 42 | } 43 | } 44 | } 45 | 46 | #[derive(ApiRequest, Debug)] 47 | enum Base64Req { 48 | Binary(Binary>), 49 | Text(PlainText), 50 | } 51 | 52 | #[derive(ApiResponse)] 53 | enum Base64Res { 54 | /// The encoded or decoded data 55 | #[oai(status = 200)] 56 | Ok( 57 | Binary>, 58 | /// Content-Type is `application/octet-stream` by default and is set to 59 | /// the actual type inferred by [infer](https://crates.io/crates/infer) 60 | #[oai(header = "Content-Type")] 61 | String, 62 | ), 63 | 64 | /// Bad request 65 | #[oai(status = 400)] 66 | BadRequest(PlainText), 67 | } 68 | 69 | pub struct Api; 70 | 71 | #[OpenApi(tag = "DataTag::Data")] 72 | impl Api { 73 | /// Encode data to a base64 string 74 | #[oai(path = "/base64/encode/:engine", method = "post")] 75 | async fn base64_encode( 76 | &self, 77 | /// The data to encode 78 | /// 79 | /// The data can be binary or text 80 | data: Base64Req, 81 | 82 | /// Base64 engines for encoding and decoding 83 | /// 84 | /// `bcrypt` `binhex` `crypt` `imap-mutf7` are not using padding 85 | /// 86 | /// The `custom` engine allows you to specify your own alphabet and padding 87 | engine: Path, 88 | 89 | /// The alphabet to use for encoding 90 | alphabet: Query>, 91 | 92 | /// Whether to use padding 93 | pad: Query>, 94 | ) -> Result { 95 | let config = match engine.0 { 96 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 97 | alphabet: alphabet.0.unwrap_or_default(), 98 | pad: pad.0.unwrap_or_default(), 99 | }), 100 | _ => None, 101 | }; 102 | 103 | match data { 104 | Base64Req::Binary(data) => { 105 | let encoded = httpbin::data::base64::encode(&data.0, engine.0.into(), config) 106 | .map_err(|e| Base64Res::BadRequest(PlainText(e.to_string())))?; 107 | 108 | Ok(Base64Res::Ok( 109 | Binary(encoded.into_bytes()), 110 | "text/plain; charset=utf-8".to_string(), 111 | )) 112 | } 113 | Base64Req::Text(data) => { 114 | let encoded = 115 | httpbin::data::base64::encode(data.0.as_bytes(), engine.0.into(), config) 116 | .map_err(|e| Base64Res::BadRequest(PlainText(e.to_string())))?; 117 | 118 | Ok(Base64Res::Ok( 119 | Binary(encoded.into_bytes()), 120 | "text/plain; charset=utf-8".to_string(), 121 | )) 122 | } 123 | } 124 | } 125 | 126 | /// Decode data from a base64 string 127 | #[oai(path = "/base64/decode/:engine", method = "post")] 128 | async fn base64_decode( 129 | &self, 130 | /// The string to decode 131 | data: PlainText, 132 | 133 | /// Base64 engines for encoding and decoding 134 | /// 135 | /// `bcrypt` `binhex` `crypt` `imap-mutf7` are not using padding 136 | /// 137 | /// The `custom` engine allows you to specify your own alphabet and padding 138 | engine: Path, 139 | 140 | /// The alphabet to use for decoding 141 | alphabet: Query>, 142 | 143 | /// Whether to use padding 144 | pad: Query>, 145 | ) -> Result { 146 | let config = match engine.0 { 147 | Base64Engine::Custom => Some(httpbin::data::base64::Base64Config { 148 | alphabet: alphabet.0.unwrap_or_default(), 149 | pad: pad.0.unwrap_or_default(), 150 | }), 151 | _ => None, 152 | }; 153 | 154 | let decoded = httpbin::data::base64::decode(&data.0, engine.0.into(), config) 155 | .map_err(|e| Base64Res::BadRequest(PlainText(e.to_string())))?; 156 | 157 | match String::from_utf8(decoded.clone()) { 158 | Ok(_) => Ok(Base64Res::Ok( 159 | Binary(decoded), 160 | "text/plain; charset=utf-8".to_string(), 161 | )), 162 | Err(_) => { 163 | let kind = infer::get(&decoded); 164 | 165 | Ok(Base64Res::Ok( 166 | Binary(decoded), 167 | kind.map(|k| k.mime_type()) 168 | .unwrap_or("application/octet-stream") 169 | .to_string(), 170 | )) 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /httpbin-site/content/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 58 | 67 | 76 | HTTP 92 | 93 | 94 | -------------------------------------------------------------------------------- /httpbin/src/data/uuid.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, str::FromStr}; 2 | 3 | use serde::Deserialize; 4 | use serde_with::{formats::CommaSeparator, serde_as, StringWithSeparator}; 5 | use thiserror::Error; 6 | 7 | #[derive(Deserialize, Debug)] 8 | #[serde(rename_all = "lowercase")] 9 | pub enum UuidConstNamespace { 10 | Dns, 11 | Oid, 12 | Url, 13 | X500, 14 | } 15 | 16 | #[derive(Deserialize, Debug)] 17 | #[serde(untagged)] 18 | pub enum UuidNamespace { 19 | Const(UuidConstNamespace), 20 | Custom(String), 21 | } 22 | 23 | impl FromStr for UuidNamespace { 24 | type Err = Infallible; 25 | 26 | fn from_str(s: &str) -> Result { 27 | Ok(match s { 28 | "dns" => Self::Const(UuidConstNamespace::Dns), 29 | "oid" => Self::Const(UuidConstNamespace::Oid), 30 | "url" => Self::Const(UuidConstNamespace::Url), 31 | "x500" => Self::Const(UuidConstNamespace::X500), 32 | _ => Self::Custom(s.to_owned()), 33 | }) 34 | } 35 | } 36 | 37 | #[derive(Deserialize, Debug, Default)] 38 | #[serde(rename_all = "lowercase")] 39 | pub enum UuidFormat { 40 | #[default] 41 | Hyphenated, 42 | Simple, 43 | Urn, 44 | Braced, 45 | } 46 | 47 | #[serde_as] 48 | #[derive(Deserialize, Debug)] 49 | pub struct UuidNodeId(#[serde_as(as = "StringWithSeparator::")] pub Vec); 50 | 51 | impl From> for UuidNodeId { 52 | fn from(node_id: Vec) -> Self { 53 | Self(node_id) 54 | } 55 | } 56 | 57 | impl TryInto<[u8; 6]> for UuidNodeId { 58 | type Error = (); 59 | 60 | fn try_into(self) -> Result<[u8; 6], Self::Error> { 61 | let node_id = self.0.try_into().map_err(|_| ())?; 62 | 63 | Ok(node_id) 64 | } 65 | } 66 | 67 | #[serde_as] 68 | #[derive(Deserialize, Debug)] 69 | pub struct UuidBuffer(#[serde_as(as = "StringWithSeparator::")] pub Vec); 70 | 71 | impl From> for UuidBuffer { 72 | fn from(buffer: Vec) -> Self { 73 | Self(buffer) 74 | } 75 | } 76 | 77 | impl TryInto<[u8; 16]> for UuidBuffer { 78 | type Error = (); 79 | 80 | fn try_into(self) -> Result<[u8; 16], Self::Error> { 81 | let buffer = self.0.try_into().map_err(|_| ())?; 82 | 83 | Ok(buffer) 84 | } 85 | } 86 | 87 | #[derive(Error, Debug)] 88 | pub enum UuidError { 89 | #[error("the uuid string is invalid: {0}")] 90 | InvalidUuid(#[from] uuid::Error), 91 | #[error("the length of node_id must be 6")] 92 | InvalidNodeIdLength, 93 | #[error("the length of buffer must be 16")] 94 | InvalidBufferLength, 95 | } 96 | 97 | /// Generate a v1 UUID with the given timestamp and node_id 98 | pub fn new_v1( 99 | timestamp: Option<(u64, u16)>, 100 | node_id: UuidNodeId, 101 | format: UuidFormat, 102 | ) -> Result { 103 | let node_id: [u8; 6] = node_id 104 | .try_into() 105 | .map_err(|_| UuidError::InvalidNodeIdLength)?; 106 | 107 | let uuid = match timestamp { 108 | Some((ticks, counter)) => { 109 | uuid::Uuid::new_v1(uuid::Timestamp::from_rfc4122(ticks, counter), &node_id) 110 | } 111 | None => uuid::Uuid::now_v1(&node_id), 112 | }; 113 | 114 | Ok(match format { 115 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 116 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 117 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 118 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 119 | }) 120 | } 121 | 122 | /// Generate a v3 UUID with the given namespace and name 123 | pub fn new_v3( 124 | namespace: UuidNamespace, 125 | name: &str, 126 | format: UuidFormat, 127 | ) -> Result { 128 | let ns = match namespace { 129 | UuidNamespace::Const(UuidConstNamespace::Dns) => uuid::Uuid::NAMESPACE_DNS, 130 | UuidNamespace::Const(UuidConstNamespace::Oid) => uuid::Uuid::NAMESPACE_OID, 131 | UuidNamespace::Const(UuidConstNamespace::Url) => uuid::Uuid::NAMESPACE_URL, 132 | UuidNamespace::Const(UuidConstNamespace::X500) => uuid::Uuid::NAMESPACE_X500, 133 | UuidNamespace::Custom(ns) => uuid::Uuid::parse_str(&ns)?, 134 | }; 135 | let uuid = uuid::Uuid::new_v3(&ns, name.as_bytes()); 136 | Ok(match format { 137 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 138 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 139 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 140 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 141 | }) 142 | } 143 | 144 | /// Generate a v4 UUID 145 | pub fn new_v4(format: UuidFormat) -> String { 146 | let uuid = uuid::Uuid::new_v4(); 147 | match format { 148 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 149 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 150 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 151 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 152 | } 153 | } 154 | 155 | /// Generate a v5 UUID with the given namespace and name 156 | pub fn new_v5( 157 | namespace: UuidNamespace, 158 | name: &str, 159 | format: UuidFormat, 160 | ) -> Result { 161 | let ns = match namespace { 162 | UuidNamespace::Const(UuidConstNamespace::Dns) => uuid::Uuid::NAMESPACE_DNS, 163 | UuidNamespace::Const(UuidConstNamespace::Oid) => uuid::Uuid::NAMESPACE_OID, 164 | UuidNamespace::Const(UuidConstNamespace::Url) => uuid::Uuid::NAMESPACE_URL, 165 | UuidNamespace::Const(UuidConstNamespace::X500) => uuid::Uuid::NAMESPACE_X500, 166 | UuidNamespace::Custom(ns) => uuid::Uuid::parse_str(&ns)?, 167 | }; 168 | let uuid = uuid::Uuid::new_v5(&ns, name.as_bytes()); 169 | Ok(match format { 170 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 171 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 172 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 173 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 174 | }) 175 | } 176 | 177 | /// Generate a v6 UUID with the given timestamp and node_id 178 | pub fn new_v6( 179 | timestamp: Option<(u64, u16)>, 180 | node_id: UuidNodeId, 181 | format: UuidFormat, 182 | ) -> Result { 183 | let node_id: [u8; 6] = node_id 184 | .try_into() 185 | .map_err(|_| UuidError::InvalidNodeIdLength)?; 186 | 187 | let uuid = match timestamp { 188 | Some((ticks, counter)) => { 189 | uuid::Uuid::new_v6(uuid::Timestamp::from_rfc4122(ticks, counter), &node_id) 190 | } 191 | None => uuid::Uuid::now_v6(&node_id), 192 | }; 193 | 194 | Ok(match format { 195 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 196 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 197 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 198 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 199 | }) 200 | } 201 | 202 | /// Generate a v7 UUID with the given timestamp 203 | pub fn new_v7(timestamp: Option<(u64, u16)>, format: UuidFormat) -> String { 204 | let uuid = match timestamp { 205 | Some((ticks, counter)) => uuid::Uuid::new_v7(uuid::Timestamp::from_rfc4122(ticks, counter)), 206 | None => uuid::Uuid::now_v7(), 207 | }; 208 | 209 | match format { 210 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 211 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 212 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 213 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 214 | } 215 | } 216 | 217 | pub fn new_v8(buf: UuidBuffer, format: UuidFormat) -> Result { 218 | let buf = buf.try_into().map_err(|_| UuidError::InvalidBufferLength)?; 219 | 220 | let uuid = uuid::Uuid::new_v8(buf); 221 | Ok(match format { 222 | UuidFormat::Hyphenated => format!("{}", uuid.as_hyphenated()), 223 | UuidFormat::Simple => format!("{}", uuid.as_simple()), 224 | UuidFormat::Urn => format!("{}", uuid.as_urn()), 225 | UuidFormat::Braced => format!("{}", uuid.as_braced()), 226 | }) 227 | } 228 | -------------------------------------------------------------------------------- /httpbin-poem-openapi/src/data/uuid.rs: -------------------------------------------------------------------------------- 1 | use poem::Result; 2 | use poem_openapi::{param::Query, payload::PlainText, ApiResponse, Enum, NewType, OpenApi}; 3 | 4 | use super::DataTag; 5 | 6 | /// The namespace to use for the UUID 7 | /// 8 | /// The following namespaces are supported: 9 | /// 10 | /// - `dns` - 6ba7b810-9dad-11d1-80b4-00c04fd430c8 11 | /// - `url` - 6ba7b811-9dad-11d1-80b4-00c04fd430c8 12 | /// - `oid` - 6ba7b812-9dad-11d1-80b4-00c04fd430c8 13 | /// - `x500` - 6ba7b814-9dad-11d1-80b4-00c04fd430c8 14 | /// - custom - any UUID in string form 15 | #[derive(NewType)] 16 | struct UuidNamespace(String); 17 | 18 | impl From for httpbin::data::uuid::UuidNamespace { 19 | fn from(namespace: UuidNamespace) -> Self { 20 | // This is safe because the target will be validated by the UUID library 21 | namespace.0.parse().unwrap() 22 | } 23 | } 24 | 25 | /// The format to use for the UUID 26 | /// 27 | /// The default format is `hyphenated`. 28 | /// 29 | /// The following formats are supported: 30 | /// 31 | /// - `hyphenated` - 8-4-4-4-12 32 | /// - `simple` - 32 hex digits 33 | /// - `urn` - urn:uuid:8-4-4-4-12 34 | /// - `braced` - {8-4-4-4-12} 35 | #[derive(Enum, Default)] 36 | #[oai(rename_all = "snake_case")] 37 | enum UuidFormat { 38 | /// 8-4-4-4-12 39 | #[default] 40 | Hyphenated, 41 | 42 | /// 32 hex digits 43 | Simple, 44 | 45 | /// urn:uuid:8-4-4-4-12 46 | Urn, 47 | 48 | /// {8-4-4-4-12} 49 | Braced, 50 | } 51 | 52 | impl From for httpbin::data::uuid::UuidFormat { 53 | fn from(format: UuidFormat) -> Self { 54 | match format { 55 | UuidFormat::Hyphenated => httpbin::data::uuid::UuidFormat::Hyphenated, 56 | UuidFormat::Simple => httpbin::data::uuid::UuidFormat::Simple, 57 | UuidFormat::Urn => httpbin::data::uuid::UuidFormat::Urn, 58 | UuidFormat::Braced => httpbin::data::uuid::UuidFormat::Braced, 59 | } 60 | } 61 | } 62 | 63 | #[derive(ApiResponse)] 64 | enum UuidRes { 65 | /// The generated UUID 66 | #[oai(status = 200)] 67 | Ok(PlainText), 68 | 69 | /// Bad request 70 | #[oai(status = 400)] 71 | BadRequest(PlainText), 72 | } 73 | 74 | pub struct Api; 75 | 76 | #[OpenApi(prefix_path = "/uuid", tag = "DataTag::Data")] 77 | impl Api { 78 | /// Generate a v1 UUID 79 | #[oai(path = "/v1", method = "get")] 80 | async fn uuid_v1( 81 | &self, 82 | /// An optional timestamp to use for the UUID. If not provided, the current time will be used. 83 | timestamp: Query>, 84 | 85 | /// An optional counter to use for the UUID. If not provided, 0 will be used. 86 | counter: Query>, 87 | 88 | /// The node ID to use for the UUID. The length must be 6. 89 | #[oai(explode = false)] 90 | node_id: Query>, 91 | 92 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 93 | format: Query>, 94 | ) -> Result { 95 | let timestamp = timestamp.and_then(|timestamp| { 96 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 97 | }); 98 | let format = format.0.unwrap_or_default(); 99 | 100 | let uuid = httpbin::data::uuid::new_v1(timestamp, node_id.0.into(), format.into()) 101 | .map_err(|e| UuidRes::BadRequest(PlainText(e.to_string())))?; 102 | 103 | Ok(UuidRes::Ok(PlainText(uuid))) 104 | } 105 | 106 | /// Generate a v3 UUID 107 | #[oai(path = "/v3", method = "get")] 108 | async fn uuid_v3( 109 | &self, 110 | /// The namespace to use for the UUID. 111 | /// 112 | /// The following namespaces are supported: 113 | /// 114 | /// - `dns` - 6ba7b810-9dad-11d1-80b4-00c04fd430c8 115 | /// - `url` - 6ba7b811-9dad-11d1-80b4-00c04fd430c8 116 | /// - `oid` - 6ba7b812-9dad-11d1-80b4-00c04fd430c8 117 | /// - `x500` - 6ba7b814-9dad-11d1-80b4-00c04fd430c8 118 | /// - custom - any UUID in string form 119 | namespace: Query, 120 | 121 | /// The name to use for the UUID. 122 | name: Query, 123 | 124 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 125 | format: Query>, 126 | ) -> Result { 127 | let format = format.0.unwrap_or_default(); 128 | 129 | let uuid = httpbin::data::uuid::new_v3(namespace.0.into(), &name, format.into()) 130 | .map_err(|e| UuidRes::BadRequest(PlainText(e.to_string())))?; 131 | 132 | Ok(UuidRes::Ok(PlainText(uuid))) 133 | } 134 | 135 | /// Generate a v4 UUID 136 | #[oai(path = "/v4", method = "get")] 137 | async fn uuid_v4( 138 | &self, 139 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 140 | format: Query>, 141 | ) -> Result { 142 | let format = format.0.unwrap_or_default(); 143 | 144 | let uuid = httpbin::data::uuid::new_v4(format.into()); 145 | 146 | Ok(UuidRes::Ok(PlainText(uuid))) 147 | } 148 | 149 | /// Generate a v5 UUID 150 | #[oai(path = "/v5", method = "get")] 151 | async fn uuid_v5( 152 | &self, 153 | /// The namespace to use for the UUID 154 | /// 155 | /// The following namespaces are supported: 156 | /// 157 | /// - `dns` - 6ba7b810-9dad-11d1-80b4-00c04fd430c8 158 | /// - `url` - 6ba7b811-9dad-11d1-80b4-00c04fd430c8 159 | /// - `oid` - 6ba7b812-9dad-11d1-80b4-00c04fd430c8 160 | /// - `x500` - 6ba7b814-9dad-11d1-80b4-00c04fd430c8 161 | /// - custom - any UUID in string form 162 | namespace: Query, 163 | 164 | /// The name to use for the UUID. 165 | name: Query, 166 | 167 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 168 | format: Query>, 169 | ) -> Result { 170 | let format = format.0.unwrap_or_default(); 171 | 172 | let uuid = httpbin::data::uuid::new_v5(namespace.0.into(), &name, format.into()) 173 | .map_err(|e| UuidRes::BadRequest(PlainText(e.to_string())))?; 174 | 175 | Ok(UuidRes::Ok(PlainText(uuid))) 176 | } 177 | 178 | /// Generate a v6 UUID 179 | #[oai(path = "/v6", method = "get")] 180 | async fn uuid_v6( 181 | &self, 182 | /// An optional timestamp to use for the UUID. If not provided, the current time will be used. 183 | timestamp: Query>, 184 | 185 | /// An optional counter to use for the UUID. If not provided, 0 will be used. 186 | counter: Query>, 187 | 188 | /// The node ID to use for the UUID. The length must be 6. 189 | #[oai(explode = false)] 190 | node_id: Query>, 191 | 192 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 193 | format: Query>, 194 | ) -> Result { 195 | let timestamp = timestamp.and_then(|timestamp| { 196 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 197 | }); 198 | let format = format.0.unwrap_or_default(); 199 | 200 | let uuid = httpbin::data::uuid::new_v6(timestamp, node_id.0.into(), format.into()) 201 | .map_err(|e| UuidRes::BadRequest(PlainText(e.to_string())))?; 202 | 203 | Ok(UuidRes::Ok(PlainText(uuid))) 204 | } 205 | 206 | /// Generate a v7 UUID 207 | #[oai(path = "/v7", method = "get")] 208 | async fn uuid_v7( 209 | &self, 210 | /// An optional timestamp to use for the UUID. If not provided, the current time will be used. 211 | timestamp: Query>, 212 | 213 | /// An optional counter to use for the UUID. If not provided, 0 will be used. 214 | counter: Query>, 215 | 216 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 217 | format: Query>, 218 | ) -> Result { 219 | let timestamp = timestamp.and_then(|timestamp| { 220 | counter.map_or(Some((timestamp, 0)), |counter| Some((timestamp, counter))) 221 | }); 222 | let format = format.0.unwrap_or_default(); 223 | 224 | let uuid = httpbin::data::uuid::new_v7(timestamp, format.into()); 225 | 226 | Ok(UuidRes::Ok(PlainText(uuid))) 227 | } 228 | 229 | /// Generate a v8 UUID 230 | #[oai(path = "/v8", method = "get")] 231 | async fn uuid_v8( 232 | &self, 233 | /// The buffer to use for the UUID. The length must be 16. 234 | #[oai(explode = false)] 235 | buf: Query>, 236 | 237 | /// An optional format to use for the UUID. If not provided, the default format (hyphenated) will be used. 238 | format: Query>, 239 | ) -> Result { 240 | let format = format.0.unwrap_or_default(); 241 | 242 | let uuid = httpbin::data::uuid::new_v8(buf.0.into(), format.into()) 243 | .map_err(|e| UuidRes::BadRequest(PlainText(e.to_string())))?; 244 | 245 | Ok(UuidRes::Ok(PlainText(uuid))) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Campbell He 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------