From 462a21604b798b2ca1e5a6e722c43f183766d702 Mon Sep 17 00:00:00 2001 From: Rafael Bradley Date: Fri, 2 Jan 2026 00:29:56 -0300 Subject: [PATCH] Add reverse proxy endpoint --- Cargo.lock | 122 +++++++++++++++++++++++++++++++++++------ Cargo.toml | 5 +- src/handlers/mod.rs | 14 ++++- src/handlers/v1/mod.rs | 47 +++++++++++++++- 4 files changed, 166 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c42c7b7..b5264cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,12 @@ dependencies = [ "clap", "dedent", "dotenvy", + "futures-util", + "hyper", "reqwest", "serde", "tokio", + "tokio-util", "turbostore", "urlencoding", ] @@ -123,7 +126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", - "bytes", + "bytes 1.10.1", "form_urlencoded", "futures-util", "http", @@ -156,7 +159,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ - "bytes", + "bytes 1.10.1", "futures-core", "http", "http-body", @@ -233,6 +236,22 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + [[package]] name = "bytes" version = "1.10.1" @@ -419,6 +438,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures-channel" version = "0.3.31" @@ -434,6 +459,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -453,9 +495,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", + "tokio-io", ] [[package]] @@ -500,7 +548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", - "bytes", + "bytes 1.10.1", "fnv", "futures-core", "futures-sink", @@ -530,7 +578,7 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes", + "bytes 1.10.1", "fnv", "itoa", ] @@ -541,7 +589,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes", + "bytes 1.10.1", "http", ] @@ -551,7 +599,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes", + "bytes 1.10.1", "futures-core", "http", "http-body", @@ -572,13 +620,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ - "bytes", + "atomic-waker", + "bytes 1.10.1", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -586,6 +635,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -613,7 +663,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "bytes", + "bytes 1.10.1", "http-body-util", "hyper", "hyper-util", @@ -630,7 +680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ "base64", - "bytes", + "bytes 1.10.1", "futures-channel", "futures-core", "futures-util", @@ -801,6 +851,15 @@ dependencies = [ "libc", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1056,9 +1115,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", - "bytes", + "bytes 1.10.1", "encoding_rs", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -1080,12 +1140,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -1396,7 +1458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", - "bytes", + "bytes 1.10.1", "io-uring", "libc", "mio", @@ -1407,6 +1469,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures", + "log", +] + [[package]] name = "tokio-macros" version = "2.5.0" @@ -1440,11 +1513,11 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ - "bytes", + "bytes 1.10.1", "futures-core", "futures-sink", "pin-project-lite", @@ -1474,7 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags", - "bytes", + "bytes 1.10.1", "futures-util", "http", "http-body", @@ -1676,6 +1749,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" diff --git a/Cargo.toml b/Cargo.toml index e6205a9..b0d6e66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,11 @@ bitcode = "0.6.6" clap = { version = "4.5.41", features = ["derive", "env"] } dedent = "0.1.1" dotenvy = "0.15.7" -reqwest = { version = "0.12.22", features = ["json"] } +futures-util = { version = "0.3.31", features = ["tokio-io"] } +hyper = "1.8.1" +reqwest = { version = "0.12.22", features = ["json", "stream"] } serde = { version = "1.0.219", features = ["derive"] } tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } +tokio-util = { version = "0.7.17", features = ["io"] } turbostore = "0.2.0" urlencoding = "2.1.3" diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 071eb25..e9f481e 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -18,8 +18,9 @@ async fn docs() -> &'static str { dedent!( " Welcome to Nyeki's API! This is a tiny API I built to provide some basic information I - found myself needing in more than one place. This at the moment only includes Discord and - GitHub profiles, but I may add more stuff as I see myself needing it. + found myself needing in more than one place. This at the moment only includes Discord + profiles, GitHub profiles, and a reverse proxy, but I may add more stuff as I see myself + needing it. The API is versioned, so API endpoints will always be prefixed with `/v{version}`. The current version is 1 (i.e. stuff is prefixed by `/v1`). All responses are JSON (or @@ -88,6 +89,15 @@ async fn docs() -> &'static str { for supported sources). - `source`: The source of the profile (either `discord` or `github`). + GET /v1/proxy + Proxies a request to the given URL. + + Note that headers are not forwarded and neither is the request body. This is a + simple GET proxy. + + Query parameters: + - `url`: The URL to proxy the request to. + They all take 2 query parameters: `id` and `source`. The `id` is the ID of the user you want to get the profile of, and the `source` is the source of the profile. The source can be either `discord` or `github`. diff --git a/src/handlers/v1/mod.rs b/src/handlers/v1/mod.rs index f8a0f50..f87f77c 100644 --- a/src/handlers/v1/mod.rs +++ b/src/handlers/v1/mod.rs @@ -4,10 +4,12 @@ use std::sync::Arc; use axum::{ Json, Router, + body::{Body, Bytes}, extract::{Query, State as AxumState}, - response::Redirect, + response::{Redirect, Response}, routing::get, }; +use futures_util::StreamExt; use reqwest::Client; use serde::{Deserialize, Serialize}; use turbostore::{Decode, Duration, Encode}; @@ -21,6 +23,7 @@ pub fn router() -> Router> { .route("/profile/avatar", get(get_profile_avatar)) .route("/profile/banner", get(get_profile_banner)) .route("/profile/url", get(get_profile_url)) + .route("/proxy", get(proxy_with_cors)) .fallback(error_404) .method_not_allowed_fallback(error_405) } @@ -300,3 +303,45 @@ async fn get_profile_url( Ok(Redirect::to(&profile.url)) } + +#[derive(Debug, Deserialize)] +struct ProxyQuery { + url: String, +} + +async fn proxy_with_cors(Query(params): Query) -> Result { + let client = Client::new(); + + match client.get(¶ms.url).send().await { + Ok(resp) => { + let status = resp.status(); + let headers = resp.headers().clone(); + + // Turn the reqwest response into a streaming body + let stream = resp.bytes_stream().map(|chunk_result| match chunk_result { + Ok(chunk) => Ok::(chunk), + Err(_) => Err(std::io::Error::other("stream error")), + }); + + // Convert the stream into a hyper Body + let body = Body::from_stream(stream); + + // Build response + let mut response = Response::builder().status(status); + let headers_mut = response.headers_mut().unwrap(); + for (key, value) in headers.iter() { + // Skip certain headers that hyper will manage itself + if key.as_str().eq_ignore_ascii_case("content-length") { + continue; + } + headers_mut.insert(key, value.clone()); + } + + Ok(response.body(body).unwrap()) + } + Err(_) => Err(Error { + status: 500, + message: "Failed to fetch the requested URL".into(), + }), + } +}